Summary: GameScene.js is a nearly 2000-line monolithic scene that currently handles input controls, terrain generation, physics, and UI rendering all in one place
github.com
github.com
. This issue proposes refactoring GameScene into a more modular architecture by extracting major game systems into separate ES6 modules. By isolating input handling, terrain generation, and UI/HUD management (and other subsystems) into their own classes or services, we can greatly improve code clarity and maintainability while following modern Phaser best practices (e.g. system-based design or an ECS-like separation of concerns). Why It Matters: A modular structure will make the code easier to understand, test, and extend. Right now, one class is doing too many jobs – for example, it directly reads input and applies physics responses, generates terrain segments, updates on-screen text, and manages game-over logic all together. This increases the risk of bugs and makes future changes (like tweaking terrain or adding new UI elements) error-prone. Separating concerns will let developers work on one system without unintentionally affecting others. This refactor is an important step toward a maintainable codebase for a Steam release, where stability and extendibility are crucial. Proposal: Break GameScene into a set of cooperating modules, each responsible for a single aspect of the game, and integrate them into the scene. Possible subsystem breakdown:
Input Controller Module: Encapsulate keyboard/gamepad input logic. For instance, unify the existing keyboard controls (this.cursors and Phaser input events) with the Manette gamepad/keyboard mapper so that GameScene no longer directly checks keys
github.com
. An InputController class (perhaps building on the Manette utility) would handle reading input each frame and translating it into high-level actions or events (e.g. “jump” or “move left”). GameScene would simply query the Input module’s state or subscribe to its events, decoupling raw input from game physics logic.
Terrain Generator Module: Move all terrain generation and management code out of GameScene. This includes the functions that create new ground segments, manage the terrainSegments array, and draw or remove terrain
github.com
. A new TerrainManager (or similar) class can maintain terrain state and expose methods like initTerrain() (for the initial platform), updateTerrain(playerX) (to extend terrain when the player approaches the edge
github.com
), and cleanupTerrain() (remove off-screen segments). This module would use Phaser’s Matter physics via the scene, but contain the algorithm for terrain shape and placement. GameScene would call these methods in its update loop rather than containing the logic inline.
UI/HUD Module: Extract the on-screen UI elements (speed text, altitude text, score, lives display, toast messages) into a dedicated UI manager. For example, a HudDisplay class can be responsible for creating text objects and graphics in GameScene.create() and providing update methods (e.g. updateSpeed(speedValue)) that GameScene calls when game state changes. This separates rendering logic from gameplay logic. We may also consider making the HUD a separate Phaser Scene running in parallel (a common pattern for Phaser 3), but initially a module within the same scene is fine. The key is that GameScene shouldn’t directly manipulate text and graphics; instead, call a method like this.hud.updateLives(currentLives) or this.hud.showToast(message) to keep UI logic in one place.
(Optional) Other Systems: Identify any remaining responsibilities in GameScene and consider modularizing them. For example, collision handling and physics tweaks might stay in GameScene, but if there are distinct systems (like a Collectible/Life System for extra life pickups or a GameOver controller), those could be modules too. The goal is that GameScene.js mainly orchestrates these components (initializing them and delegating calls), following a more service-based architecture rather than performing all logic itself.
Plan & Steps:
Define Module Boundaries: In the code, mark sections that pertain to input, terrain, UI, etc. (e.g., input handling code around movement and jumping, terrain generation functions, UI text creation and updates). Sketch out interfaces for new modules (e.g., what methods TerrainManager or HudDisplay should have).
Create Module Classes: Implement new ES6 classes/files for each subsystem. For example, create src/game/InputController.js, src/game/TerrainManager.js, and src/game/HudDisplay.js (file paths can be adjusted to the project structure). Copy relevant logic from GameScene into these classes:
In InputController: initialize within a scene (subscribe to Phaser input events or poll this.input.keyboard), map keys to actions (could integrate the existing Manette system fully), and expose methods like update() or getters like isJumpPressed(). It can internally use Manette or replace parts of it, ensuring all input (keyboard arrows, WASD, gamepad) funnels through one interface.
In TerrainManager: include properties like the terrain segments array, segment width, last terrain Y, etc. Bring over generateNextTerrainSegment() and related helper code
github.com
, and the drawing logic for the terrain (using a Phaser Graphics object). This class might require a reference to the Phaser scene or Matter physics world to add bodies for collision. It can emit an event or callback when new terrain is added (if needed for other systems).
In HudDisplay: include methods to create and update text and graphics. On creation, use the scene to add text objects for speed, altitude, etc., and perhaps a container for toast messages. Provide functions like updateSpeed(value), updateAltitudeDrop(value), updateLives(count) and showToast(message) that handle the necessary text/graphic changes. This module can also listen for global game events (e.g., a Phaser event for when lives change or score updates) instead of being manually updated, depending on what fits best.
Integrate Modules into GameScene: In GameScene.js, remove or simplify the sections that were moved. Import the new modules at the top. In the scene’s constructor or create(), instantiate the modules, e.g. this.inputController = new InputController(this); this.terrain = new TerrainManager(this); this.hud = new HudDisplay(this); (passing the scene or relevant context). Replace direct calls with module calls:
Instead of checking this.cursors.left.isDown or this.manette.isActionActive(...) in update, call something like this.inputController.update() each frame, then query its state (e.g., if (inputController.jump) or better, have the InputController itself trigger the jump by calling a provided callback on the player object).
In the main update loop, after moving the player or updating physics, call this.terrain.update(player.x) to generate new terrain if needed and this.hud.updateHud(player, gameState) or specific update methods for each HUD element as values change.
Move any UI updates (like setting text or calling showToast) out of GameScene.update into the HUD module. For instance, when the player performs a trick or loses a life, call this.hud.showToast("...") and this.hud.updateLives(lives). The HUD module internally updates the toastContainer and lives graphics.
Follow Phaser Best Practices: Ensure the refactoring aligns with Phaser architecture. For example, keep heavy game logic out of the core update as much as possible – the new modules can use Phaser events (like hooking into scene.events.on('update', ...) if that pattern is cleaner). This is akin to an ECS approach where each system handles its own update tick. Document these new modules so future developers recognize the structure (e.g., comment that TerrainManager is responsible for ground generation and must be updated every frame).
Test and Adjust: After moving code, run the game and all tests. It’s likely some behaviors might initially break (because of context changes). Fix any issues (e.g., ensuring the modules correctly reference the scene’s Matter physics world, or that input events still register). The gameplay should remain identical after the refactor. Use the automated tests (and add new ones per Issue 2) to verify everything still works.
Acceptance Criteria:
Game logic is cleanly separated: No single file (or class) should be doing “everything” anymore. GameScene.js length should be drastically reduced, primarily coordinating module interactions.
The Input module handles all key and gamepad inputs; game update logic no longer directly calls this.input.keyboard or checks this.cursors inside GameScene
github.com
.
The Terrain module manages terrain data and physics bodies. Terrain generation and removal logic should reside outside GameScene (GameScene simply invokes it or responds to its events). The visual rendering of terrain remains identical after refactor.
The HUD/UI module exclusively manages on-screen text/graphics for score, speed, altitude, lives, and trick notifications. GameScene does not manually create or update text objects after this change.
All existing gameplay features remain functional: player movement, jumping, tricks, collisions, scoring, etc. should behave as before (this ensures the refactor didn’t introduce regressions).
Code respects Phaser lifecycle. For example, if the HUD uses a separate scene or the modules use events, they should properly clean up on scene shutdown.
Developers can now navigate the codebase more easily (each subsystem has its own file), setting the stage for easier future enhancements (like adjusting terrain algorithm or input schemes) and for adding tests for each subsystem in isolation.
Verification Checklist:
Full game playtest: Start the game and play through various scenarios (normal sledding, perform tricks, lose lives, etc.) to confirm nothing is broken by the refactor. The game should transition from the start screen to gameplay and through game over/restart exactly as before.
Automated tests pass: All current test suites run without errors. Unit tests and integration tests that touch GameScene functionality (e.g., input response, terrain continuity, UI updates) should still pass or are updated alongside the code changes.
No new console errors or warnings in the browser during play (especially no Phaser runtime errors related to the refactored parts, such as missing objects or undefined properties).
Code quality checks (linting, if any) report no issues. New modules should follow the project’s code style.
Module interfaces documented: The newly introduced classes have comments or README updates explaining their purpose and how they interact (for future reference as this is a significant structural change).
Summary: GameScene.js is a nearly 2000-line monolithic scene that currently handles input controls, terrain generation, physics, and UI rendering all in one place
github.com
github.com
. This issue proposes refactoring GameScene into a more modular architecture by extracting major game systems into separate ES6 modules. By isolating input handling, terrain generation, and UI/HUD management (and other subsystems) into their own classes or services, we can greatly improve code clarity and maintainability while following modern Phaser best practices (e.g. system-based design or an ECS-like separation of concerns). Why It Matters: A modular structure will make the code easier to understand, test, and extend. Right now, one class is doing too many jobs – for example, it directly reads input and applies physics responses, generates terrain segments, updates on-screen text, and manages game-over logic all together. This increases the risk of bugs and makes future changes (like tweaking terrain or adding new UI elements) error-prone. Separating concerns will let developers work on one system without unintentionally affecting others. This refactor is an important step toward a maintainable codebase for a Steam release, where stability and extendibility are crucial. Proposal: Break GameScene into a set of cooperating modules, each responsible for a single aspect of the game, and integrate them into the scene. Possible subsystem breakdown:
Input Controller Module: Encapsulate keyboard/gamepad input logic. For instance, unify the existing keyboard controls (this.cursors and Phaser input events) with the Manette gamepad/keyboard mapper so that GameScene no longer directly checks keys
github.com
. An InputController class (perhaps building on the Manette utility) would handle reading input each frame and translating it into high-level actions or events (e.g. “jump” or “move left”). GameScene would simply query the Input module’s state or subscribe to its events, decoupling raw input from game physics logic.
Terrain Generator Module: Move all terrain generation and management code out of GameScene. This includes the functions that create new ground segments, manage the terrainSegments array, and draw or remove terrain
github.com
. A new TerrainManager (or similar) class can maintain terrain state and expose methods like initTerrain() (for the initial platform), updateTerrain(playerX) (to extend terrain when the player approaches the edge
github.com
), and cleanupTerrain() (remove off-screen segments). This module would use Phaser’s Matter physics via the scene, but contain the algorithm for terrain shape and placement. GameScene would call these methods in its update loop rather than containing the logic inline.
UI/HUD Module: Extract the on-screen UI elements (speed text, altitude text, score, lives display, toast messages) into a dedicated UI manager. For example, a HudDisplay class can be responsible for creating text objects and graphics in GameScene.create() and providing update methods (e.g. updateSpeed(speedValue)) that GameScene calls when game state changes. This separates rendering logic from gameplay logic. We may also consider making the HUD a separate Phaser Scene running in parallel (a common pattern for Phaser 3), but initially a module within the same scene is fine. The key is that GameScene shouldn’t directly manipulate text and graphics; instead, call a method like this.hud.updateLives(currentLives) or this.hud.showToast(message) to keep UI logic in one place.
(Optional) Other Systems: Identify any remaining responsibilities in GameScene and consider modularizing them. For example, collision handling and physics tweaks might stay in GameScene, but if there are distinct systems (like a Collectible/Life System for extra life pickups or a GameOver controller), those could be modules too. The goal is that GameScene.js mainly orchestrates these components (initializing them and delegating calls), following a more service-based architecture rather than performing all logic itself.
Plan & Steps:
Define Module Boundaries: In the code, mark sections that pertain to input, terrain, UI, etc. (e.g., input handling code around movement and jumping, terrain generation functions, UI text creation and updates). Sketch out interfaces for new modules (e.g., what methods TerrainManager or HudDisplay should have).
Create Module Classes: Implement new ES6 classes/files for each subsystem. For example, create src/game/InputController.js, src/game/TerrainManager.js, and src/game/HudDisplay.js (file paths can be adjusted to the project structure). Copy relevant logic from GameScene into these classes:
In InputController: initialize within a scene (subscribe to Phaser input events or poll this.input.keyboard), map keys to actions (could integrate the existing Manette system fully), and expose methods like update() or getters like isJumpPressed(). It can internally use Manette or replace parts of it, ensuring all input (keyboard arrows, WASD, gamepad) funnels through one interface.
In TerrainManager: include properties like the terrain segments array, segment width, last terrain Y, etc. Bring over generateNextTerrainSegment() and related helper code
github.com
, and the drawing logic for the terrain (using a Phaser Graphics object). This class might require a reference to the Phaser scene or Matter physics world to add bodies for collision. It can emit an event or callback when new terrain is added (if needed for other systems).
In HudDisplay: include methods to create and update text and graphics. On creation, use the scene to add text objects for speed, altitude, etc., and perhaps a container for toast messages. Provide functions like updateSpeed(value), updateAltitudeDrop(value), updateLives(count) and showToast(message) that handle the necessary text/graphic changes. This module can also listen for global game events (e.g., a Phaser event for when lives change or score updates) instead of being manually updated, depending on what fits best.
Integrate Modules into GameScene: In GameScene.js, remove or simplify the sections that were moved. Import the new modules at the top. In the scene’s constructor or create(), instantiate the modules, e.g. this.inputController = new InputController(this); this.terrain = new TerrainManager(this); this.hud = new HudDisplay(this); (passing the scene or relevant context). Replace direct calls with module calls:
Instead of checking this.cursors.left.isDown or this.manette.isActionActive(...) in update, call something like this.inputController.update() each frame, then query its state (e.g., if (inputController.jump) or better, have the InputController itself trigger the jump by calling a provided callback on the player object).
In the main update loop, after moving the player or updating physics, call this.terrain.update(player.x) to generate new terrain if needed and this.hud.updateHud(player, gameState) or specific update methods for each HUD element as values change.
Move any UI updates (like setting text or calling showToast) out of GameScene.update into the HUD module. For instance, when the player performs a trick or loses a life, call this.hud.showToast("...") and this.hud.updateLives(lives). The HUD module internally updates the toastContainer and lives graphics.
Follow Phaser Best Practices: Ensure the refactoring aligns with Phaser architecture. For example, keep heavy game logic out of the core update as much as possible – the new modules can use Phaser events (like hooking into scene.events.on('update', ...) if that pattern is cleaner). This is akin to an ECS approach where each system handles its own update tick. Document these new modules so future developers recognize the structure (e.g., comment that TerrainManager is responsible for ground generation and must be updated every frame).
Test and Adjust: After moving code, run the game and all tests. It’s likely some behaviors might initially break (because of context changes). Fix any issues (e.g., ensuring the modules correctly reference the scene’s Matter physics world, or that input events still register). The gameplay should remain identical after the refactor. Use the automated tests (and add new ones per Issue 2) to verify everything still works.
Acceptance Criteria:
Game logic is cleanly separated: No single file (or class) should be doing “everything” anymore. GameScene.js length should be drastically reduced, primarily coordinating module interactions.
The Input module handles all key and gamepad inputs; game update logic no longer directly calls this.input.keyboard or checks this.cursors inside GameScene
github.com
.
The Terrain module manages terrain data and physics bodies. Terrain generation and removal logic should reside outside GameScene (GameScene simply invokes it or responds to its events). The visual rendering of terrain remains identical after refactor.
The HUD/UI module exclusively manages on-screen text/graphics for score, speed, altitude, lives, and trick notifications. GameScene does not manually create or update text objects after this change.
All existing gameplay features remain functional: player movement, jumping, tricks, collisions, scoring, etc. should behave as before (this ensures the refactor didn’t introduce regressions).
Code respects Phaser lifecycle. For example, if the HUD uses a separate scene or the modules use events, they should properly clean up on scene shutdown.
Developers can now navigate the codebase more easily (each subsystem has its own file), setting the stage for easier future enhancements (like adjusting terrain algorithm or input schemes) and for adding tests for each subsystem in isolation.
Verification Checklist:
Full game playtest: Start the game and play through various scenarios (normal sledding, perform tricks, lose lives, etc.) to confirm nothing is broken by the refactor. The game should transition from the start screen to gameplay and through game over/restart exactly as before.
Automated tests pass: All current test suites run without errors. Unit tests and integration tests that touch GameScene functionality (e.g., input response, terrain continuity, UI updates) should still pass or are updated alongside the code changes.
No new console errors or warnings in the browser during play (especially no Phaser runtime errors related to the refactored parts, such as missing objects or undefined properties).
Code quality checks (linting, if any) report no issues. New modules should follow the project’s code style.
Module interfaces documented: The newly introduced classes have comments or README updates explaining their purpose and how they interact (for future reference as this is a significant structural change).