After finishing my first game Hex Sweeper I started thinking about how to approach my next game. Originally, my thinking was to just clone the game into a new repo and start refactoring it into the new one. This would work coming from a simple game like Hex Sweeper but I wanted a more rapid style of game development where I could build out games with similar mechanics more quickly. It was clear I would need some base reusable framework to use as a consistent and clean starting point for future games.
This led to the creation of a framework with the working name of PL4X, short for Phaser Light 4X. The next game was going to require an initial refactor and clean up anyway, so this was the natural next step. It would give me a central point to incorporate lessons learned from each new game I work on which in turn improves the foundation upon which to continue building future games from.
Since my next game was going to be some kind of hex tile, turn-based take on the desktop tower defense genre, I knew I would need cleaner separation between views and data with the scenes being the glue between them. After a bit of brainstorming and grilling of best practices with Claude, I ended up restructuring the code into three fundamental parts with three separate folders under src named engine, scenes, and ui.
Once I had a decent base I got to task on my next game which ended up being named Button Breach. If you want to read more about how that game came to be you can read my article on that here: Button Breach: My First Original Game, a Turn-Based DTD. I ended up building a large part of this game out and then back porting the features into PL4X, the sections below describe the current architecture at that point.
The engine is basically just a state machine. It receives "actions" from the views via the scenes, processes them, and outputs "effects" back to the views, again via the scenes. This means you have a single entry point for the actions to funnel through which are then delegated accordingly: is this an attack, a move, a unit selection, etc.
The actual actions themselves will generally come in three varieties. The first is from interface components like buttons:
this.on(EVENTS.GAME_COMMAND.TURN_END, () => this.#dispatch({ type: 'TURN_END' }));
this.on(EVENTS.GAME_COMMAND.SELECTION_CANCEL, () => this.#dispatch({ type: 'SELECTION_CANCEL' }));
this.on(EVENTS.GAME_COMMAND.TOWER_SELECT, (key: EntityBuildingKey) => this.#dispatch({ type: 'TOWER_PLACEMENT_SELECT', towerKey: key }));
The second will be your mouse events on the board:
this.on(EVENTS.GAME_BOARD.POINTER_LEFT_DOWN, (tile: { x: number, y: number }) => this.#dispatch({ type: 'POINTER_LEFT_DOWN', x: tile.x, y: tile.y }));
this.on(EVENTS.GAME_BOARD.POINTER_RIGHT_DOWN, (tile: { x: number, y: number }) => this.#dispatch({ type: 'POINTER_RIGHT_DOWN', x: tile.x, y: tile.y }));
Then maybe some keyboard shortcuts:
this.input.keyboard!.on('keydown-ENTER', () => this.#dispatch({ type: 'TURN_END' }));
The question then is how to delegate these, in particular with the very generic mouse events which don't really tell us much. This is where the core feature of the engine comes in. For lack of a better term, I've named them "phases". Phases just let you redirect logic accordingly to where it's needed. For the default start phase I just use "idle". From there the engine then decides what to do with the x, y values it receives. If it's an entity, then set it as the selected entity in the engine's state and switch the phase to "unit command". The next pointer event action that now comes through is likely going to be a movement if it's an empty tile or perhaps an attack or re-selection if it's another entity.
From there the phases will delegate out to various systems such as the "Movement System" or "Combat System", etc. However I won't get into the fine grained details here.
The scenes are the core of Phaser.js and where the game generally boots from. Whether it's the main menu scene or the actual game scene the logic remains consistent. This is where all your event listeners get registered, which aside from something like a key event listener will pretty much all come from the views. Then in the case of the engine they delegate for processing and listen for some return effects to update the view.
How the returning effects get dispatched to views may vary based on the game, but the basic idea is the same. For instance in most 4X games the effects may be displayed in sequence, unit moves, attacks, next unit moves, etc. However with Button Breach being a single screen game with multiple entities on screen the approach was to bundle things like moves and attacks into a single sequence to make for better visual and play flow.
Piping everything through scenes keeps things clean and centralized. It forces you to think about data and views separately which is a good habit to be in. I had played around with the idea of an event bus, but gave it up since it seemed a bit out of scope for the current needs of the framework. I may revisit the idea as complexity grows, but for now scenes as the central glue works well.
The views are pure display logic and really only have two jobs, the first is to render the UI itself, and the second is to update the UI. The views can be split up into as many separate parts as needed, but you'll generally want to keep things in sensible groups. For instance a basic game like Button Breach has the main board view for handling the actual hex tiles but then also a control view for menus and score and a command view for placing towers, displaying entity info and ending a turn. The various overlays for main menu and game end are also each distinct views.
Here we'll also want to run any visual effects and sounds. For instance if a unit dies we want to display some explosion effects at a particular hex tile where the unit dies. Here is where that would get triggered along with any sound effect for the visual as well.
I've open sourced the framework on GitHub since I've always enjoyed contributing to open source when I can. It's still early and intentionally light, but it's been a solid foundation to build from so far. If you're working on a hex strategy game in Phaser, hopefully it saves you some of the boilerplate I had to work through.