Having read comments and questions, we thought it would be fun to talk about the things we feel like we would change, and the things we're happy with.
Obviously much of the code could simply be cleaner had we known exactly what everything was going to do from the beginning, or had time to do a major refactor (which wasn't ever a big priority).
We would have liked all Animation related code to be its own system that was more data driven. It's the way it is because we never implemented more than a simple frame-by-frame sprite component. We completely agree that having if frame == whatever
inside the player class is ugly.
There are a large number of states that simply shouldn't exist within the player. Everything regarding the Intro
states and the Dummy
state should be an entirely different entity used for cutscenes that get swapped with the real player during gameplay.
This also goes for the ChaserState
. This could likely be abstracted into a Component and removed entirely from the player class.
We wouldn't have moved states into their own classes. To us, due to how much interaction there is between states and the nuance in how the player moves, this would turn into a giant messy web of references between classes. If we were to make a tactics game then yes - a more modular system makes sense.
One reason we like having one big file with some huge methods is because we like to keep the code sequential for maintainability. If the player behavior was split across several files needlessly, or methods like Update() were split up into many smaller methods needlessly, this will often just make it harder to parse the order of operations. In a platformer like Celeste, the player behavior code needs to be very tightly ordered and tuned, and this style of code was a conscious choice to fit the project and team.
We don't. We wrote unit tests for various other parts of the game (ex. making sure the dialog files across languages all match, trigger the correct events, and that their font files have all the appropriate characters). Writing unit tests for the player in an action game with highly nuanced movement that is constantly being refined feels pointless, and would cause more trouble than they're worth. Unit Tests are great tools, but like every tool they have advantages and disadvantages. Unit tests could be useful for making sure input is still triggering, collision functionality behaves as expected, and so on - but none of that should exist in the Player class.
We do use a Scene->Entity->Component system, which may not entirely be clear from the player class alone. She inherits "Actor" which inherits "Entity". Actor has generic code for movement and collisions. Anything that was re-used was put into a component (player sprite, player hair, state machine, mirror reflections, and so on). Things that the player only ever did were left in the player.
The reason that "ChaserState" struct has a switch statement instead of an array is to save on creating garbage. There's no way to have an array in a struct in C# with a predefined size, (ex. int[4] fourValues;
) so every struct would be creating a new array instance. In the end this probably didn't matter, but we were trying to save on creating garbage during levels as we weren't sure how the GC would perform cross-platform.
Yes, it is! We use XNA because we're comfortable in it, like C#, it's very stable on Windows, and is easy to make cross platforms with open source ports such as FNA and MonoGame. If you're playing on macOS or Linux you're on FNA, and on consoles you're on MonoGame. Will we use it for future projects? Maybe, maybe not.
Not right now, but Matt wrote this overview of the TowerFall physics a few years back: https://mattmakesgames.tumblr.com/post/127890619821/towerfall-physics
Celeste's physics system is very similar to TowerFall's.
We had 2 programmers working on Celeste (Noel and Matt).