I also have no idea, but just for the sake of discussion here’s my list of best practices that I try to follow. For reference, this is the repo where I’m coming up with and following these rules: https://github.com/snowbillr/archer-adventure
Some of these are probably more useful as games get more complex. For simple games, it might not be worth it to take all of these rules as absolute.
Typescript > Javascript
This isn’t really a Phaser rule, but for any kind of development that involves lots of files and data structures being passed across them, this helps immensely.
Use a file for constants
Phaser is very string key based for loading and using assets, as well as scenes. I find that its easy to make typos with these keys and not have a good clue why things aren’t working. For example, I have a constants file for my scene keys so I don’t need to worry about making typos.
Extend Phaser.Scene
for each scene
One file per Scene
Have a TestScene to try out new things before slotting them into your real scene
When I’m creating animations, or dealing with sprite bounds, I have a prefab test scene I load up and work in so I don’t have to worry about everything else going on in my game scene. Once I get things to the place where I want them, I’ll start actually adding whatever I’m doing into my real game scene.
Keep the Scene’s update method as empty as possible
I think this one is probably controversial.
I have plugins that listen for the update event (post update technically) and run code there. I feel like this gives me a way to separate out my logic nicely, so that my scene’s update method doesn’t get crowded and hard to reason about.
I have an ECS architecture that I’ve implemented in my repo that allows me to extract logic for individual systems into their own files.
Use finite state machines rather than a state variable with a big if/else statement
This also goes a long way for keeping logic separated and the update method clean. Its hard to navigate your code when you’re dealing with a giant if/else statement, rather than clean separated files that have implementations for each state of the FSM.
I also wrote a finite state machine in my repo (that I use primarily for game objects) that lets me isolate the logic for each state to its own file.
Use an ECS architecture
This is something that I also find helps me to organize my logic, rather than having a class that extends Phaser.GameObjects.Sprite
with a giant update method. I also built something out in my repo that lets me do this.
Input is a piece of scene state, it doesn’t belong to the player
This took me a while to realize, but was kind of an epiphany when I did. It’s also just my opinion, so this might be controversial as well.
I originally built out my game such that the player’s entity had all the logic around dealing with input. But then I needed to build out a conversation system and had to awkwardly reach into my player entity so that I could get at the input to advance the conversation.
I realized that by putting my input controls at the scene level rather than on the player entity, I could access it from any system that needed to deal with input. Phaser definitely pushes you in this direction itself by having the input plugin at the scene level, but I always thought that was just for API reasons. But really, it’s telling you to use input as scene state rather than entity state.
Create custom plugins liberally
Whenever I have a piece of functionality that is responsible for something at a scene level (e.g. playing sound effects, loading an area from a tilemap, controls available to my scene) I make it a scene plugin.
Whenever I have a piece of functionality that’s responsible for something at a global level (e.g. data persistence) I make it a global plugin.