Below is my playthrough for the version of the game that I have now:
You may download the executable of the game here:
This game started off as a means for me to get more familiar with Unity and C#, but has turned into me wanting to make the most well designed code structure I can come up with at the time, despite this being a simple game. For example my animation and AI state machines consist of separate classes for the different states (and of course, Unity’s excellent component system helps with this). This massively improves portability, flexibility and maintainability, and actually isn’t much work (when its up and running, it cuts down the work needed!).
Regarding the animation state machine, I have an abstract class (GLAnimState, GL for Green Lantern, the character) that is inherited by all of the other animation states, containing an abstract function VOnUpdate() that is overridden by all subclasses (in C++, this would be called a pure virtual function):
My motivation for this is that there are many different types of animations. We have some that will loop forever until a different state is implemented, such as the idle and running animation states. Then we have some that will play once and return to a different state, such as the ‘hit’ or ‘attack/shoot’ state which is followed immediately by idle, and then some that will loop forever but on the last frame (or the last few frames), such as the jumping animation. Rather than a tangled mess of if/else statements, which is not easily updated or maintained, separate subclasses make the code much tidier, and easily allow code to be swapped in or out.
As you can see, the constructor of each animation state class takes in an animator object, containing arrays of all the sprites (as passed in by Unity editor) and an enumerated type, defined below:
The enumerated type changes according to game logic. Then all animation states are stored in an array as type <Superclass> and simply looped through. The animation state that is active is then called.
I did consider having subclasses instead named “Play once”, “Play looping”, “Play last frames” etc, which would have been more generic. But I opted to not follow that strategy, as specific game logic can be called by these subclasses (with references to player controllers, animation controllers etc in the superclass). For example the “CreateBullet()” function is called at the end of the “Attack” state class, creating a bullet at the end of the animation.** For better encapsulaion an event system could be used, but for now this seems sufficient. An alternative could be to gave generic classes wrapped in more specific class names, such as “Idle” containing a call to “Play Looping”. But this seems convoluted.
** Update – Actually ‘PlayLooping’, ‘PlayOnce’ etc would be the most portable option. For end of animation logic, a generic function in class CharacterAnimator taking as parameter an object of type AnimState. Within the function I could have a switch statement based on the AnimState’s enumerated value, allowing for the different end of animation logic to be called from there .
The AI and player controller were derived from a generic CharacterController as follows:
Where GLEntity would contain physics data such as the rigid body and AABB components, with GLEntity containing transformation information. CharacterController contained code that could be common to both AI and the player, such as shooting bullets. The AI states themselves were constructed and implemented in a similar way to the animation states and processed in GLAIController:
As before GLAIState would have an abstract function AOnUpdate() (and an AOnReset()) and enumerated types denoting the individual AI States. They were updated in the same way in the class AIController :
However this time I made the AIState base class derive from Unity’s MonoBehaviour class, e.g., they were custom components that could be added to prefabs selectively according to the desired AI behaviour.
The nice thing about this approach is these AI state components can be copied into other platformer games easily. Also as the PlayerController class is derived from the AIController class, the AI states can be used by the player too, making behaviour where control is taken away from the player, e.g., knocking over, being hit, or level beginning/level end states, (In this case when Green Lantern flies in and out of the level) easily added and implemented.