This past week was insane for project planning. I started the week thinking I was going to burn the week on solving my problem with Data Persistence. 30 minutes later I solved it. At a loss at what to do next, I slowly cranked through my old project plan.
What I Planned
I planned for a lot to be done by the end of last week. The first of five levels were supposed to be done according to this Gantt Chart I made two weeks ago.
This did not happen. My previous blog post hinted as much. The problem comes down to this: I encountered significant difficulty in solving the problem of making my game have persistent data. I could create new save slots and then save data to the save slot. But I could not load the data. I went in circles for a week on this, trying to solve the problem. In the end, this Gantt Chart became meaningless as I failed to meet any goals that week. To be honest, this might have been a blessing in disguise, but more on that towards the end.
What I Did
After a week of pounding my head into a wall to no avail, I decided to take a day off to relax and let the problem simmer for a bit. Then after simmering, the solution hit me.
Creating a Save Slot, Saving to a Save Slot, and Loading from a Save Slot
There are two approaches I see to handling the Creation, Modification, and Loading of Save Data. The first approach is the Centralized Approach. All the data gets worked on from a single function call. This seems like a good idea, and in some ways it is. Being able to call a single function to work on the Save Data is very convenient. For Creating a Save Slot and Saving data to the Save Slot, this makes sense. I write a single function that then gets called from any number of objects when the Player saves the game or passes through a Checkpoint.
This was not working for Loading the Save Data though. I really wanted it too. But no matter how many times I tried a different and unique solution, they all failed. After a week, it became obvious that my assumptions of how it should work were wrong. So, I sat on it for a day and let it simmer. During that time, I figured out, maybe I should try a completely different way of architecting it?
The approach that I took was a Distributed Approach. Instead of having all the data worked on from a single function, each object would work on only their own data in isolation. This plan got me past this roadblock.
So how does it work? Let’s start from when the Save Data is first created.
To begin, there are two different Save Slots being worked from. The first is named SettingsSave. It holds the names for all the Game Save Slots. Later, I plan to add in variables for game volume and screen size. The SettingsSave Slot also holds two other key variables: CurrentSaveSlot and bPlayerCharacterCanLoad. CurrentSaveSlot is the name of the Save Slot the Player is currently in. bPlayerCharacterCanLoad is a Boolean that controls when the Player Character’s Data loads. Its default value is False.
When the Player creates a New Save Slot, a Save Slot Name is created. The Save Slot Name is assigned to a slot in an array of Save Slot Names and to CurrentSaveSlot. Once this done, the Save Slot is created via a function call. The player then loads into the start of the first level.
The player can then play around to their heart’s content, saving from time to time. The Save Slot is opened, and various data is then assigned into the Save Slot. This includes the Current Level Name, the Player’s Transform, Player’s Health, and their Inventory. Once all the data is assigned, a function is called to save to the Save Slot. This is all done from a single function in the Player Controller.
The player can then leave the game and come back and load the game from a save. All the Player must do is select the save slot to load and hit play. Super simple. On the back end, bPlayerCharacterCanLoad is set to True and the SettingsSave Slot is saved. The Selected Save Slot is loaded, and the level is loaded using the Current Level Name in the Save Slot File. Then, on the Player Character, if bPlayerCharacterCanLoad is true on BeginPlay (An Unreal Engine Function Call that all Actors have and is called when a Game Level is opened), the data for the Player’s Transform, Health, and Inventory is all loaded correctly. Once the Player Character Data is loaded, bPlayerCharacterCanLoad is set to False and SettingsSave is saved.
Loading the Save Data wasn’t important for just loading a game from the Main Menu, but also for moving the player between levels. Whenever the Player collides with an object called BP_LevelTrigger, they will go to the level designated from within the Blueprint. To be able to load the Player’s Data in the new level, bPlayerCharacterCanLoad gets saved as True to SettingsSave. Turns out, the Distributed Approach for loading save data is more loosely coupled and cohesive than the Centralized Approach. This makes the Persistence of Player Data throughout gameplay far more resilient to changes in the code.
Inventory Component
I finished the Save Data problem rather quickly. I built my final solution in about 30 minutes. Having spent a week bashing my head into walls, I thought it would take a week. Now what do I do? I decided to take it easy, and slowly grind through the now irrelevant Gantt Chart I had.
First up, the Inventory Component. This component will hold and manage all the inventory information for the Player. So far, I have added functionality for picking up Health Drops. The plan right now is to keep the Health Drops simple. There will be a single type of health drop that the player can easily use with the press of a button. The value of health in the health drop gets added to an array every time a health drop is picked up. This then leads into the next section, Healing.
Healing
Healing is simple and is built into the Health Component of the Player Character. The player hits the heal button and they heal. The value of health added is taken from the array of health drop values in the Inventory component. Once the health value is applied to the Player’s health, it is popped off the array.
Autosave Point Class
I also added a simple collision box for creating checkpoints in levels. Whenever the player collides with the box, a function on the Player Controller is called to save the game.
Different Attack Types
I have begun working on different Attack Types for the Player. I have created five unique patterns so far. The patterns so far are: A Simple Single Shot, Exploding Fan, Imploding Fan, Exploding Spiral Fan, Imploding Spiral Fan. Currently I have only one type of color bullets, Red. In the game, Red Bullets are Damage Type Bullets. They will carry the most Damage and only do damage to a character.
Bullet Explosions
Next up was Bullet Explosions. I decided to keep these super simple and light weight. With potentially thousands of bullets live in the game, I don’t want to slow down the game with expensive explosion effects. The explosions are built from a Material that is then placed onto a sphere mesh. The Material uses a Vertex Normal Node and a noise texture combined with a sine wave to create a burst-like effect. This sphere is then bundled into a Blueprint with a point light to create a pleasing effect. Then with a quick edit to the bullet hit, on all bullet hits an explosion is spawned.
Attack Switching
To end the week, I added bindings for cycling through the Attack Types Array in the Attack Handler Component. By pressing a single button, the player easily cycles through the index.
What I Learned
I managed to accomplish a lot this past week. In that same week, I have learned a lot too. My first lesson I learned is Persistence is Key to Problem Solving (And Data Persistence!). I am not going insane if each attempt to solve the problem is new and unique. It was also important to break down each process, stepping through it at each point. This quickly got me through solutions that didn’t work and find out why my code was broken.
I have also learned how I might want the actual gameplay to work. Originally, I was planning to have 5 different and unique levels. Each Level would have been very much a Runner Style level, with the player running down a hallway. Once I started making the different bullet patterns though, I quickly realized that might not work out well. I am thinking that maybe massive landscapes that encourage 3D movement of the player will be more interesting.
Having gotten past my problem with Data Persistence, I now feel like I am getting into the fun part of game development: Adding fun and unique gameplay features. Until next time!