This link will take you to the itch.io page for Cola Corpses where you can find the available builds and project info.
This game is a passion project that I have been working on for a while now. The inspiration for the game comes from 2 of my favorite genres of games, those being roguelikes and Call of Duty Zombies. These games inspired me to try my hand at creating an experience that feels familiar, but has its own mechanics and goals that are distinct from previously made games. This game was also a way for me to demonstrate my abilities with various coding techniques such as digging algorithms, data saving and interface management. In addition to coding I learned a lot about how to make 3D models as well as how to make the textures and UV maps that are used throughout the game. Everything in this game was made by me except for the sounds which I found on a site which offered free sound effects.
Thank you for taking the time to play my game, additional breakdowns of noteworthy mechanics can be found below.
The player controller featured in this game is fairly basic, but it suits the needs that I have and certainly the needs of the project. The controller has fully customizable controls which are set within a custom keybind controller and can be adjusted during gameplay as the user sees fit. The controller also allows for the adjustment of the speed at which the player accelerates and decelerates to make the player feel more human and less like a rock that simply glides across the ground at the push of a button. This controller also has an auto jump feature which will allow the player to jump while holding down the jump button or not depending on which they would like.
This game uses a procedural generation system to create a map of varying sizes for the player to walk through (examples can be seen on the right).
The script which creates this map uses a digging algorithm in order to make all of the rooms. Once the layout for the room has been determined, the walls and doors are placed on each tile depending on how many sides are exposed to the open or to other rooms. While choosing where the walls should go, any walls which could potentially become a door to another room are noted down in a 2D List (List[Room][Tile]) and then one wall is chosen at random to become a door from each connecting room. The information for which side should be a door or wall is stored within an array in the Tile class, each side having it's own index in the array (0 -> Up, 1 -> Down, 2 -> Right, 3 -> Left). Each wall and door written into this array is represented as an integer, 0 being a wall, 1 being a door, -1 being nothing, and -2 being nothing due to the existence of a door in another room. I chose to use this approach because an array of integers is easily saved and loaded to and from a document which I use to hold the data that represents each map.
The prototype version of the map generation. Doors are green and the red represents walls.
In addition to the tile placement on the map being randomly generated, the map also randomly generates with various decorations. the current decorations consist of arcade machines, tables and trash cans. Various other interactable objects are added to the map, some are used for general gameplay and some are used for the easter egg which allows the player to finish the game. Regardless of the type of decoration, each of these are saved in either the furnitureArray or the partsArray which are 2 different arrays of integers which denote what goes in the space. the furniture array controls the large objects such as arcade cabinets and tables while the part array dictates where the smaller objects such as presents and targets are supposed to go.
Given that this game is randomly generated and thus can look pretty confusing at times, I felt it was necessary to create a minimap for the player in order for easier navigation through this maze. To create the minimap I created a second camera and placed it above the player, the camera then follows the player and mimics any rotation on the Y axis in order to always have the minimap facing the direction the player is facing. The camera is set to orthographic mode in order to ensure that nothing looks 3D and it is set to only render things with the layer"MinimapItem". After making the camera I simply had to place the output into a render texture and have that texture display on the HUD and then the minimap camera was complete. In order to make the objects show up on the minimap each one has a separate part on it that contains an object that is on the "MinimapItem" layer so that it is only rendered by the minimap camera. This object is toggled on and off based on if the tile has been explored yet.
Most Unity games which utilize NavMesh in some capacity have the scenes baked before building the game, but this would not work for my game as the map would be random every time the game started. To fix this issue I had to delve into the world of runtime NavMesh generation which is not as complex as it would seem. The biggest issue was the lack of proper documentation of the methods involved with the system. Doors are represented as NavMesh Obstacles so that they can be taken away without producing a large amount of lag that comes with redoing the rest of the NavMesh.
Throughout the development of this game many concepts for how the player's weapon would function have come and gone, but the one that I finally chose was a system based around the mixing of sodas to create new sodas that the player throws at enemies. Each soda part has its own stats and effects which carry over into the main weapon and allow for full customization of the weapon by the player.
The pause menu which showcases the current soda stats on the left side of the screen
The player is given a randomly created soda weapon at the beginning of the game. This soda is of the lowest rarity, that being household rarity (or common). Each rarity tier assigns a set amount of points to the weapon's four stats, those stats are: damage, throw rate, throw speed and explosion radius. The amount of points allotted to each part increases with the rarity thus making the weapon parts more potent as the increase in rarity. Each stat is gauged on a scale of 0 - 100, 0 being the worst it can be and 100 being the best. These numbers are then put through different formulas to turn them into numbers that the computer can use to influence the soda's performance.
In addition to the basic stats, once a part has a rarity of commercial (blue / rare) the parts begin to have effects tied to them and once the part is Collector (yellow / legendary) rarity they have 2 soda effects. Each effect is shown as an adjective which could apply to a soda that also vaguely hints at what it does. The current effects are:
Sticky: Slows the enemy movement speed
Iced: Gives the enemies ice physics and reduced their turning speed
Fizzy: Burns the enemy for a set amount of damage over time
Sour: Poisons the enemy for a percentage of their total health for a certain duration
Healthy: Heals the player for a certain amount per activation
Flat: Lowers the damage output of the enemy for a certain duration
Each soda part contributes to the effects on the soda weapon that is being created, this also means that the effects can stack on eachother to create an even more powerful version of the base effect. Sometimes the duration increases and sometimes it increases the potency, each change is determined for the effect individually
One key factor that needed to be in the game was soda drinking and what better way to drink soda than to have that soda also impart a temporary buff upon you after drinking it? Each Soda Part has a certain flavor, these flavors are Cherry, Orange, Blue Raz, Cola, Root beer, Grapefruit and Banana. Each of these flavors has a certain buff that it will apply to the player based on the amount of duplicate flavors as well as the variety of flavors that are present on the weapon. Each buff also has a downside such as increasing the dmaage of the player, but decreasing their movement speed. These soda flavors give the players even more control over what kind of weapons they can create, but it also allows for a higher ceiling within the game's meta given that some player would want to grind for the perfect roll on a given soda part
When initially beginning the development of this game I started using the built in Unity Input Manager, but I came across a few restrictions that I just didn't want to deal with. Due to these restrictions I decided to use the KeyCode system which is built into Unity in order to take inputs from the player. Due to this it was incredibly easy to implement a Key Bind menu in the home screen.
This menu allows the player to bind the keys of each action to (almost) any key that they would like to bind to. All the user must do is click the button which shows the key they would like to change and then press the button which they want to bind it to. This feature also has a built in system which removes duplicate key assignments so that the user doesn't accidentally assign two actions to the same key.
It can be noted that the settings can also be changed during gameplay and are put into effect as soon as they are changed. The keybinds are stored as a static variable inside the ValueStoreController static class and they are referenced througout the scripts.
While simply being able to play the game in one sitting was enjoyable enough, I became annoyed that I was unable to come back to the game at a later point, I then decided that I would implement saving systems into the game to make the experience more smooth for the players. After working on the saving systems for a while I decided that it would be best to have a few kinds of files where data was stored: Profile Data, Game Save Data, Game Reset Data, Game Seed Data. Each of these files contains information which would be used for different purposes in the game.
While not as game changing as the GameSaveData file, the profile data file is still important as it enables the user to save their keybinds, setting preferences and profiles records thus making the experience feel more engaging over a longer play session. Each item was stored using either the JsonUtility or a simple value to string converter that I made for it. These files are all rather small so the data is not super complex or hard to read or write. Each section of data would be separated by a denoted "section end marker" which would tell the reader to switch to the next section, the reader would know which section it was because each section also has a section header that is stored as a static variable and is compared to the section header being read. Overall this data was not hard to save at all.
This save data is the largest file that is saved in this game. The game save data is responsible for saving everything that goes into making the game run.
Items that are saved through GameSaveData:
Player Data
Enemy Data
Map Data
Game Controller Data
Vending Machine Data
Some other smaller things
All of this data is stored neatly inside one file and is separated similarly to how the profile data store separates it's data. While some data was harder to store than others, the simpler data such as the player data (only position and health) as well as the Game Controller data were saved largely using JsonUtility as the data was simple enough that it had no problem converting the data to strings. Some of the more noteworthy data saving is explained below.
The map data was the hardest data to save by far. In order to find a way to save an entire randomly generated map's data I had to make several new classes which could hold various basic values which could then represent 3D objects that would be placed at the start of the game.
Tile:
Distance Number: int - This tile's distance from the starting tile
Position: Vector2 - This tile's position on the map 2D array
Minimap Visible: bool - is this tile visible on the minimap yet?
Spawner: The data for the spawner if this tile has one
Border Type Array: int[] - contains 4 integers each representing a side of the tile. These integers represent something different that can be on each border of the tile (such as walls, doors, open doors, nothing, or invalid space)
Furn Array: Tilefurn[] - Contains any information pertaining to the large furniture items that are present on the tile such as arcade machines, vending machines or crafting tables.
Part Array: TilePart[] - Contains information about the easter egg parts that are stored within the array and where they are positioned within the tile.
Additional variables exist on the tiles to hold the GameObjects that are placed at runtime for referencing later
Tiles are the smallest relevant piece of the map. Each tile contains the variables listed above in addition to a few to store the actual gameobjects. All information stored in the tiles was created specifically so that it would be easy to convert it into a string for saving.
Room:
Room Number: int - The number assigned to the room to denoted the order in which it was created, but also to differentiate one room from another
Tile List: List<Tile> - The list of tiles in the room
While they do not have nearly as much data to store as tiles, rooms are very important as they organize the tiles into groups which can then have functions performed upon them at any given time such as de-rendering rooms that are too far away.
Map:
Map Size: int - The size of the 2D array representing the map
Tile Size: float - The size (in Unity base units) of the tile object to be placed
Wall Height: float - The height of the walls in the game
Current Room: int - The current room number the player is in, used for loading and rendering
Room Tile Count: int - The amount of tiles needed for a room to be considered fully complete (used for map generation)
Map Tile Count: int - The total amount of tiles used in the map creation process
Room List: List<Room> - The list of rooms within the map
The map contains many of the variables that are used to reference how the map was made and how the rooms should look when they are being placed. In that sense it is more like a big settings storage, but these variables help to make sure everything looks and feels right in the generated map.