Electron Integration
Relevant source files
The following files were used as context for generating this wiki page:The Souless project uses the Electron framework to package the Phaser 3 web game into a standalone desktop application. This integration is responsible for creating the native window, managing its lifecycle, and providing a bridge between the game's web-based renderer process and the host operating system's native capabilities, most notably for file system access to handle game saves.
The architecture separates concerns between the Electron Main process, which manages the application and native interactions, and the Renderer process, which runs the Phaser game itself. Communication between these two processes is handled securely via a preload script and Electron's Inter-Process Communication (IPC) mechanism.
Architecture Overview
The application is divided into two primary processes, bridged by a preload script, which is a standard and secure Electron pattern.
- Main Process (
main.js): The entry point of the Electron application. It runs in a Node.js environment and is responsible for creating and managing the application window (BrowserWindow) and handling native OS interactions like file I/O. - Renderer Process (
renderer/directory): This is the web page environment where the Phaser game logic runs. It is loaded into theBrowserWindowand does not have direct access to Node.js or OS-level APIs for security reasons. The entry point for this process isrenderer/index.html. - Preload Script (
preload.js): A script that runs in a privileged context with access to both the Renderer'swindowobject and a limited set of Node.js APIs. It acts as a secure bridge, exposing specific functions from the Main process to the Renderer process viacontextBridge.
This diagram shows the relationship between the core components of the Electron integration.
Main Process
The Main process is configured in main.js. Its primary responsibility is to initialize the application window and handle system-level events and IPC calls.
Window Creation
The createWindow function initializes a BrowserWindow instance. The window is configured to be fullscreen and non-resizable, providing a dedicated game experience.
Key configurations include:
- Dimensions: Set to the primary display's work area size.
webPreferences:preload: Specifiespreload.jsto be loaded before other scripts in the renderer.contextIsolation: Enabled (true) for security, ensuring the preload script and the renderer's JavaScript run in different contexts.sandbox: Disabled (false) for the preload script to use Node.js APIs likeipcRenderer.
In a development environment (isDev), the window loads its content from the Vite dev server (http://localhost:5173) and opens DevTools. In production, it loads the compiled dist/index.html file.
Sources: main.js:18-43
Application Lifecycle
The application's lifecycle is managed by listening to app module events:
app.whenReady(): Creates the main window once Electron has finished initialization.app.on('window-all-closed'): Quits the application when all windows are closed (except on macOS).app.on('activate'): Re-creates the window if the app is activated and no windows are open (macOS dock behavior).
Sources: main.js:52-66
Inter-Process Communication (IPC)
IPC is the cornerstone of the Electron integration, allowing the sandboxed Renderer process to request actions from the privileged Main process. This is primarily used for the save/load system and other native functions.
Preload Script (preload.js)
The preload.js script uses contextBridge.exposeInMainWorld to attach a custom electron object to the renderer's global window object. This object contains functions that the renderer can call. These functions, in turn, use ipcRenderer to send messages to the main process.
This approach is secure because it only exposes specific, pre-defined functionalities and does not leak the entire ipcRenderer or other Node.js modules to the game code.
// preload.js
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electron', {
toggleFullscreen: () => ipcRenderer.send('toggle-fullscreen'),
getAppPath: () => ipcRenderer.invoke('get-app-path'),
loadSave: (slotIndex) => ipcRenderer.invoke('load-save', slotIndex),
saveSave: (data, slotIndex) => ipcRenderer.invoke('save-save', data, slotIndex),
clearSave: (slotIndex) => ipcRenderer.invoke('clear-save', slotIndex),
getSavePath: (slotIndex) => ipcRenderer.invoke('get-save-path', slotIndex),
});
Sources: preload.js:3-11
IPC Handlers (main.js)
The Main process listens for IPC messages using ipcMain.handle for asynchronous, promise-based communication. Each handler corresponds to a function exposed in the preload script.
The following table summarizes the defined IPC channels and their purpose.
| Channel | Handler | Description | Source File |
|---|---|---|---|
toggle-fullscreen | ipcMain.on | Toggles the main window's fullscreen state. | main.js:45-48 |
get-app-path | ipcMain.handle | Returns the application's path. | main.js:68-70 |
load-save | ipcMain.handle | Reads and returns data from a specified save slot file. | main.js:89-102 |
save-save | ipcMain.handle | Writes data to a specified save slot file. | main.js:108-117 |
clear-save | ipcMain.handle | Deletes a specified save slot file. | main.js:123-135 |
get-save-path | ipcMain.handle | Returns the absolute path to a save slot file for debugging. | main.js:141-143 |
Sources: main.js:45-143, preload.js:3-19
Save/Load System Integration
The save/load functionality is a prime example of Electron integration. The SaveSlotManager (and the older SaveManager) in the renderer process checks if it's running within Electron and routes file operations accordingly.
Environment Detection
Both SaveManager and SaveSlotManager use an isElectron() helper method to determine the runtime environment.
// src/utils/SaveSlotManager.js
static isElectron() {
return typeof window !== 'undefined' && window.electron;
}
Sources: src/utils/SaveSlotManager.js:17-19, src/utils/SaveManager.js:11-13
Conditional Logic
Based on the environment, the save and load methods choose their backend:
- Electron: Calls the functions exposed on
window.electron(e.g.,window.electron.saveSave(...)), which trigger IPC calls to the Main process. - Browser: Falls back to using
localStorage(e.g.,localStorage.setItem(...)).
This allows the same game code to function both in a standard web browser for development/testing and within the final desktop application.
The following sequence diagram illustrates the "Save Game" flow in the Electron environment.
This diagram shows the sequence of calls for saving game data, starting from the game logic and ending with a file write operation handled by the Main process.
Sources: src/utils/SaveSlotManager.js:100-114, preload.js:15, main.js:108-117
Build and Packaging
The package.json file defines the scripts and configuration for building and packaging the Electron application using electron-builder.
Scripts
The following scripts are central to the development and distribution workflow:
| Script | Description |
|---|---|
dev | Concurrently runs the Vite dev server (dev:vite) and Electron (dev:electron) for live-reloading development. |
build | Uses Vite to build the renderer process assets into the /dist directory. |
start | Runs the application in production mode, loading from the /dist directory. |
pack | Packages the application into a directory for testing without creating an installer. |
dist | Builds distributable installers and packages for Windows, macOS, and Linux. |
Sources: package.json:6-13
electron-builder Configuration
The build section in package.json contains the configuration for electron-builder.
// package.json
"build": {
"appId": "com.mycompany.mygame",
"productName": "MyElectronGame",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"main.js",
"preload.js"
],
"win": {
"target": [ "nsis", "zip" ],
"icon": "assets/icon.ico"
},
"mac": {
"target": [ "dmg", "zip" ],
"icon": "assets/icon.icns"
},
"linux": {
"target": [ "AppImage", "deb" ],
"icon": "assets/icon.png"
}
}
Key properties include:
appId: A unique identifier for the application.productName: The name of the application.directories.output: Specifies that build artifacts will be placed in thereleasefolder.files: An array of files and directories to include in the final package. This correctly includes the compiled renderer code (dist), the main process entry point (main.js), and the preload script.win,mac,linux: Platform-specific configurations, defining the target package types (e.g.,nsisinstaller for Windows,dmgfor macOS) and application icons.
Sources: package.json:22-49