Architecture Overview
Relevant source files
The following files were used as context for generating this wiki page:
The Souless project is a 2D roguelike game built using a hybrid architecture that combines the Electron framework for desktop application capabilities with the Phaser 3 game engine for rendering and gameplay logic. This structure allows for both browser-based development and testing via a Vite dev server, and packaging as a standalone desktop application.
The architecture emphasizes a clear separation of concerns, notably between the application's shell (window management, file system access) and the game's core logic. It also employs a data-driven design, where game content such as characters, enemies, and maps are defined in configuration files, decoupling them from the core engine code. This makes the project scalable and easier to maintain.
Core Technologies
The project is built on a foundation of modern web technologies, packaged for the desktop.
| Technology | Role | Source |
|---|---|---|
| Electron | Desktop application framework. Manages the main window and provides access to native capabilities like the file system for saving game data. | package.json:18 |
| Phaser 3 | Core game engine. Handles rendering, physics, animations, input, and scene management. | package.json:30 |
| Vite | Build tool and development server. Bundles the game's JavaScript code for the renderer process and provides a fast development environment. | package.json:20 |
| JavaScript (ES6 Modules) | The primary programming language used throughout the project. | package.json:6 |
Sources: package.json:6,18,20,30
Application Structure
The application follows Electron's standard main/renderer process model.
This diagram shows the separation between the Electron main process, which controls the application window, and the renderer process, which runs the Phaser game.
Sources: main.js, renderer/index.html
Main Process
The main process, defined in main.js, is the application's entry point. Its responsibilities include:
- Creating and managing the
BrowserWindow, which is set to fullscreen. - Loading the game content into the window, either from the Vite dev server (
http://localhost:5173) in development or from the bundleddist/index.htmlfile in production. - Handling application lifecycle events, such as
app.whenReady()andwindow-all-closed. - Exposing Inter-Process Communication (IPC) handlers for native file system operations, which are used by the save/load system.
Sources: main.js:14-38, 51-54, 70-125
Renderer Process
The renderer process is where the Phaser game lives.
- Entry Point:
renderer/index.htmlis the HTML file loaded into theBrowserWindow. It includes the main script that starts the game. - Game Initialization: The script initializes a new
Phaser.Gameinstance, configured with a list of all game scenes (MainMenuScene,GameScene, etc.). - Build Configuration:
vite.config.jsconfigures Vite to userenderer/index.htmlas the input and build the output to thedistdirectory, which is then loaded by Electron in production.
Sources: renderer/index.html, vite.config.js:25-31
Game Engine and Scene Management
The game's flow is managed through a series of Phaser scenes. Each scene is a self-contained part of the game with its own preload, create, and update loops.
Scene Flow
The primary flow between scenes is as follows:
This diagram illustrates the transitions between the main menu, intro, core gameplay, and ending scenes.
- MainMenuScene: The initial scene. It handles loading save slot information and provides options to start a new game or load an existing one.
- IntroScene: Plays a narrative introduction for a new game before transitioning to the
GameScene. - GameScene: The core of the game. It manages the player, enemies, map rendering, physics, combat, and all other gameplay systems. When moving between maps via portals, the
GameSceneis restarted with new map data. - EndingScene: Plays the final cutscene and dialogue after the player defeats the final boss.
Sources: src/scenes/MainMenuScene.js:278-291, src/scenes/IntroScene.js:283-291, src/scenes/GameScene.js:1330-1340, src/scenes/EndingScene.js:283-287
Data Persistence (Save/Load System)
The save system is architected to support both desktop (Electron) and browser environments, with a clear separation between the game logic and file system operations.
Save/Load Flow
The following sequence diagram shows how game data is saved in the Electron environment.
This diagram shows the GameScene initiating a save, which is handled by SaveSlotManager. The manager then uses an IPC call to the main process, which performs the actual file write operation.
Sources: src/utils/SaveSlotManager.js:100-111, main.js:98-106
Components
main.js(IPC Handlers): The Electron main process listens for IPC events (load-save,save-save,clear-save). It uses Node.js'sfsmodule to read, write, and delete.jsonsave files in the user's data directory. This isolates all direct file system access to the main process for security and stability.SaveSlotManager.js: A static class that acts as the single source of truth for all save/load operations within the renderer process.- It detects if the game is running in Electron and uses
window.electronIPC calls accordingly. If not, it falls back tolocalStoragefor browser-based play. - It manages up to 3 save slots (
MAX_SLOTS = 3). - It provides a high-level API to save and load specific parts of the game state, such as
savePosition,saveCharacterState, andsaveLevelSystem. - It uses a queue (
_saveQueue) to prevent race conditions from frequent save calls.
- It detects if the game is running in Electron and uses
Sources: main.js:70-125, src/utils/SaveSlotManager.js
Saved Data Structure
The SaveSlotManager defines a default data structure for a save slot, which is populated as the game progresses.
| Key | Type | Description |
|---|---|---|
levelSystem | Object | Contains global level, total experience, and experience to next level. |
characterExp | Object | Stores experience points for each individual character. |
characters | Object | A map of character-specific states, including HP, MP, and stats. |
currentCharacter | string | The key of the character currently being played. |
lastPosition | Object | The player's last map key, x/y coordinates, and character type. |
skillCooldowns | Object | Stores remaining cooldowns for each character's skills. |
killTracker | string | A serialized JSON string of enemies killed. |
portalConditions | string | A serialized JSON string of unlocked portals and defeated bosses. |
clearedBosses | Array | An array of boss IDs that have been defeated. |
timestamp | number | The Unix timestamp of the last save. |
Sources: src/utils/SaveSlotManager.js:23-41, 151-161
Data-Driven Design
A key architectural feature is the use of configuration objects to define game content. This separates data from logic, allowing for easier updates, balancing, and content creation without altering core game code.
This diagram shows how GameScene and its associated managers/factories consume data from configuration files to build the game world.
Key Configuration Files
src/config/characterData.js: A large object defining every playable character. For each character, it specifies sprite information, physics properties (speed, jump power), animations, and a detailed breakdown of every skill, including damage, hitbox sequences, cooldowns, and visual effects.src/config/mapData.js: Defines all maps in the game. Each map configuration includes its parallax background layers, gravity, default player spawn location, enemy types and spawn rules, and boss information.src/config/enemiesData.js: Similar tocharacterData.js, this file defines all enemy types, including their stats, sprites, animations, and AI behavior (patrol range, attack patterns, skills).src/config/portalData.js: Contains a mapping of all portal connections, defining how maps link to one another.
Sources: src/config/characterData.js, src/config/mapData.js, src/config/enemiesData.js, src/config/portalData.js
Entity State Management
Character and enemy states (e.g., idle, attacking, jumping) are managed by a StateMachine class. This prevents conflicting actions and ensures animations play correctly.
- State Locking: When a character performs an action like an attack, the state machine enters a "locked" state (
isLocked = true). This prevents the player from initiating another action (like jumping or moving) until the current action's animation is complete. - State Transitions: The state machine transitions back to a default state (
idleorjump) once an animation completes or a timeout is reached. This is handled via aanimationcompleteevent listener or asetTimeout. - Decoupling: This system decouples the state logic from the character's main update loop, simplifying the character class and centralizing state transition rules.
Sources: src/entities/characters/systems/StateMachine.js:38-89
Conclusion
The architecture of Souless is well-suited for a 2D action game. It leverages the strengths of Electron for desktop integration and Phaser 3 for powerful game development features. The clear separation between the main and renderer processes, combined with a robust data persistence layer, provides a stable foundation. Furthermore, its data-driven design and component-like systems (such as the StateMachine) make the codebase modular, scalable, and easy for developers to extend with new content.