Something very important in this project was analyzing the development premises; it may seem basic, but it was crucial to the project development and its architecture, therefore I believe it should be considered and used before any project starts.
At the beginning of the engineering design process of Black River Studios' Dead Body Falls game construction, the following premises were raised:
- It would be an story driven game with linear gameplay.
- Game design and art changes would happen constantly because of the game mood’s high importance.
- More than one scene would be loaded at the same time.
- The same scene could be reused for another state.
Therefore, the following project decisions were made:
- Each game state should be malleable as to the number of scenes and their settings.
- It will be possible to go to a state from any other.
- The scene and the logic should be decoupled as possible in order to avoid merge conflicts.
- We would use the PlayMaker Plugin to implement events to obtain greater flexibility in changes and adjustments.
- Everything other than events should be implemented by scripts.
The FSMs, the scene structure and the states were modeled based on these decisions.
In the following topics, we will see how each of them was implemented in the game.
Movement and FSMs structuring
The free movement was found to be uncomfortable and not advantageous in the pre-production phase; in this way, we opted for the more traditional teleport mechanics with positions given by waypoints; this movement system was well suited to the scope and proposal of the project.
This definition helped solidify how the FSMs would be implemented in the game.
Based on the premise of decoupling the logic from the scene, all FSMs would be prefabs created in runtime; Recalling that it would be an event game with locomotion by waypoints and with a linear narrative, it was established that the FSMs would be linked to the waypoints themselves.
Based on the premise that each scene could execute more than one state, it was defined that each state would have its waypoints and these would only be active in their respective state, like in the image below:
A variables dictionary was used to uncouple the scene objects from the FSMs prefabs; this dictionary was a singleton present in the initial game scene and was filled automatically by the variables themselves. Each gameobject was responsible for adding and removing itself from the dictionary, and, for this purpose, each dictionary item had a script with an configuration path and gameobject name that would be used to generate the dictionary hash key.
The dictionary had a int type as key and a gameobject type as value, thus simplifying the search algorithm. However, on the editor’s side the keys were assembled from a group of strings - populated by a list of scriptable objects present in a path previously configured in the editor - that corresponded to Gameobject type, state, and location.
The script could, by design, add itself or add its children recursively, which resulted in specific cases of gameobjects with the same component attached twice with different settings. For future reuse, it would be interesting to aggregate these two behaviors in the same component.
FSM variables should have names equal to their corresponding dictionary key; editors were made to facilitate this insertion and maintenance.
In this way, the FSM logic could be made totally independent from the scene, since it was only be necessary to add scripts with the correct path settings in the scene, making it much easier to share resources.
The management of state and scene variables, including waypoints, was as follows:
- There was a setup FSM in each scene that was responsible for resetting the scene, allowing any state to be initialized.
- There was a configuration FSM for each state that was responsible for setting up all necessary variables, scenes objects and configurations to initialize that state.
- All scene FSMs should run before the state FSM.
- The waypoint FSMs could only load their dictionary variables after this setup was over.
It was established that game states would inherit from the ScriptableObject class; the purpose would be to provide each state with the possibility to have exposed variables references without being related to any scene. Any scene references that might be needed would have their path stored as strings - displayed as gameobjects through CustomDrawers - and loaded in runtime from the dictionary.
The image below exemplifies this behaviour; in it we can see the variable stored as string and displayed as Gameobject.
If the variable reference was not found - this could happen, in the editor, when the scene containing the target gameobject was not loaded - the output would be:
The basic game states configuration included:
- The scenes:
- Each scene had a name and the value corresponding to AsyncOperation allowSceneActivation property.
- The prefab list that should be created before loading the scenes.
- Usually contained state-only audio configuration prefabs
- The prefab list that should be created after the scene is loaded.
- The state configuration FSM was always here
- A fake inner state called TitleCards.
- This was not originally planned; the architecture was not intended to be used with inner states and this adaptation was made to contemplate this requirement.
The state change was controlled by a state machine that initiated the loading state, which carried out the prefabs’ creation and the scenes’ loading and/or unloading.
The decision of not having an inner state (on each state) allowed greater flexibility to the project, since it was possible to go to any state at any time; however, also it brought limitations, which resulted in the creation of the fake inner state, an initial state that major game states could had. It had to be implemented along with loading state logic, which is obviously not advisable.
An improvement for upcoming projects would be to implement a more robust state machine that not only allowed inner states, but also provide the decoupling of states.
The assertive project premises contributed greatly to less rework and greater strength of the architecture; which made it possible to respond faster and more precisely to changes. This was demonstrated, in practice, by the architecture withstanding more than four dense restructurings of game design and art requirements while remaining practically the same. Therefore, we consider this architecture a success case for similar projects that can undergo many changes and adjustments.