Client Camera Follow System

How the Phaser camera follows the player character in the Reldens client: the camera configuration, the initialization order, the interpolation (lerp) values and the responsive resize behavior.

Overview

The camera follow system manages how the Phaser camera tracks the player character during gameplay. It involves multiple components across the client architecture: PlayerEngine, GameEngine, and the scene management.

Key Components

1. PlayerEngine (lib/users/client/player-engine.js)

Purpose: manages the player character on the client side, including the camera initialization and configuration.

Camera configuration properties (PlayerEngine constructor):

this.cameraRoundPixels = Boolean(
    this.config.getWithoutLogs('client/general/engine/cameraRoundPixels', false)
);
this.cameraInterpolationX = Number(
    this.config.getWithoutLogs('client/general/engine/cameraInterpolationX', 0.02)
);
this.cameraInterpolationY = Number(
    this.config.getWithoutLogs('client/general/engine/cameraInterpolationY', 0.02)
);

Configuration source: these paths are not seeded by the migrations, so unless the rows are created manually the hardcoded fallbacks above are used. When the rows exist they are read from the config table with scope client and loaded during the game initialization.

2. Camera initialization flow (PlayerEngine.create())

Execution order, in this order:

  1. Player sprite creation: this.addPlayer(this.playerId, addPlayerData) creates the player sprite in the physics world.
  2. Scene visibility: this.scene.scene.setVisible(true, this.roomName) makes the scene visible.
  3. Camera fade-in effect: this.scene.cameras.main.fadeFrom(this.fadeDuration) starts the fade-in animation (default 1000ms duration).
  4. Physics world configuration: fixedStep = false enables the variable physics timestep, and the physics and camera bounds are set to match the map dimensions.
  5. Scene camera flag: this.scene.cameras.main.setIsSceneCamera(true).
  6. Camera follow: this.scene.cameras.main.startFollow(...) receives the player sprite plus the roundPixels and both interpolation values in a single call:
this.scene.cameras.main.startFollow(
    this.players[this.playerId],
    this.cameraRoundPixels,
    this.cameraInterpolationX,
    this.cameraInterpolationY
);

3. Phaser camera follow API

startFollow() method signature:

camera.startFollow(target, roundPixels, lerpX, lerpY, offsetX, offsetY)

Parameters:

  • target: the game object (player sprite) to follow.
  • roundPixels (optional): boolean - force pixel-perfect rendering.
  • lerpX (optional): number - horizontal interpolation (0 to 1, default 1).
  • lerpY (optional): number - vertical interpolation (0 to 1, default 1).
  • offsetX (optional): number - horizontal offset from the target center.
  • offsetY (optional): number - vertical offset from the target center.

Lerp behavior:

  • Value of 1: the camera instantly snaps to the target position (no interpolation).
  • Value < 1: the camera smoothly interpolates to the target position.
  • Lower values (e.g. 0.04) = slower, smoother camera movement.
  • Higher values (e.g. 0.8) = faster, more responsive camera movement.

4. GameEngine.updateGameSize() integration

Purpose (GameEngine.updateGameSize() in lib/game/client/game-engine.js): handles the responsive behavior when the window resizes or the fullscreen toggles.

Camera lerp adjustment, at the start of updateGameSize() and again at the end of its setTimeout() callback:

if(player){
    // automatically fix the camera position to the player:
    activeScene.cameras.main.setLerp(player.cameraInterpolationX, player.cameraInterpolationY);
}

Execution flow:

  1. Before the resize operations: sets the lerp values.
  2. Timeout delay: the resize operations run in a setTimeout() that waits for client/general/gameEngine/updateGameSizeTimeOut (code fallback 0, seeded value 500).
  3. After the resize operations (after reldens.updateGameSizeAfter is emitted): restores the lerp values.

Why twice? The first call prepares the camera for the UI elements repositioning, and the second call ensures the camera tracking is restored after all the resize operations complete.

5. Event-driven architecture

Scene creation event (GameManager.activateResponsiveBehavior() in lib/game/client/game-manager.js):

this.events.on('reldens.afterSceneDynamicCreate', async () => {
    if(!this.config.getWithoutLogs('client/ui/screen/responsive', true)){
        return;
    }
    this.gameEngine.updateGameSize(this);
    this.gameDom.getWindow().addEventListener('resize', () => {
        this.gameEngine.updateGameSize(this);
    });
});

Timing sequence:

  1. Scene created.
  2. PlayerEngine.create() called - the camera fade starts (1000ms), then the camera follow is initialized with the lerp and roundPixels values.
  3. The reldens.afterSceneDynamicCreate event fires.
  4. updateGameSize() called - adjusts the camera lerp.

6. Configuration values

Database config paths:

  • client/general/engine/cameraRoundPixels: boolean (not seeded, code fallback: false).
  • client/general/engine/cameraInterpolationX: float (not seeded, code fallback: 0.02).
  • client/general/engine/cameraInterpolationY: float (not seeded, code fallback: 0.02).
  • client/players/animations/fadeDuration: integer milliseconds (seeded: 1000).
  • client/general/gameEngine/updateGameSizeTimeOut: integer milliseconds (seeded: 500, code fallback: 0).

Config loading: the values are loaded from the database during the server initialization and sent to the client in the START_GAME message as part of gameConfig.

Example, to enable round pixels and a faster camera:

INSERT INTO `config` (`scope`, `path`, `value`, `type`) VALUES ('client', 'general/engine/cameraRoundPixels', '1', 3);
INSERT INTO `config` (`scope`, `path`, `value`, `type`) VALUES ('client', 'general/engine/cameraInterpolationX', '0.04', 2);
INSERT INTO `config` (`scope`, `path`, `value`, `type`) VALUES ('client', 'general/engine/cameraInterpolationY', '0.04', 2);

7. Physics world integration

Fixed step setting (PlayerEngine.create()):

this.scene.physics.world.fixedStep = false;

Impact:

  • false: variable timestep - the physics updates based on the actual frame time.
  • true: fixed timestep - the physics updates at consistent intervals regardless of the frame rate.

Camera bounds (PlayerEngine.create()):

this.scene.physics.world.setBounds(0, 0, this.scene.map.widthInPixels, this.scene.map.heightInPixels);
this.scene.cameras.main.setBounds(0, 0, this.scene.map.widthInPixels, this.scene.map.heightInPixels);

Both the physics world and the camera are constrained to the map dimensions to prevent the camera from showing areas outside the game world.

8. Responsive behavior

Window resize listener (GameManager.activateResponsiveBehavior()):

this.gameDom.getWindow().addEventListener('resize', () => {
    this.gameEngine.updateGameSize(this);
});

Fullscreen handlers (lib/game/client/handlers/full-screen-handler.js):

  • Entering fullscreen: FullScreenHandler.goFullScreen() calls updateGameSize().
  • Exiting fullscreen: FullScreenHandler.exitFullScreen() calls updateGameSize().

Purpose: ensures the camera interpolation remains consistent across different viewport sizes and display modes.

Data Flow Summary

  1. Database config.
  2. The server loads the config.
  3. The client receives the config in the START_GAME message.
  4. The PlayerEngine constructor reads the config values.
  5. PlayerEngine.create() initializes the camera.
  6. The fade animation starts.
  7. startFollow() begins tracking the player with the roundPixels and lerp values.
  8. Window resize events - updateGameSize() maintains the lerp.

Key Technical Points

  1. The camera follow is configured in a single startFollow() call, after the fade is started.
  2. Lerp values must be passed to startFollow() or set via setLerp() for the interpolation to work.
  3. Round pixels and lerp work together: round pixels prevents the sub-pixel jitter, lerp provides the smooth motion.
  4. The physics timestep affects the camera smoothness: a variable timestep can cause frame-to-frame variations.
  5. The responsive system maintains the camera settings: updateGameSize() ensures the lerp persists through viewport changes.

File Locations

  • PlayerEngine: lib/users/client/player-engine.js
  • GameEngine: lib/game/client/game-engine.js
  • GameManager: lib/game/client/game-manager.js
  • FullScreenHandler: lib/game/client/handlers/full-screen-handler.js
  • Config database: config table with scope='client'

Related Documentation

Go Up