My Game State Controller at this moment

Hi,

I need assistance with implementing a save system for my game in PlayCanvas. Specifically, I want to save the game’s state for features like achievements and other similar systems. Could you provide some guidance or examples on how to achieve this?

I’m considering using JSON to save player key interactions, but this approach seems quite verbose. Is there an easier way to implement this?

Thank you!

I’m using a little wrapper around localStorage like this.

const SaveTool = {
    savePrefix: 'YourGameName', //must be unique prefix for each project
    /**@param {string} key*/
    getItem(key, defaultValue = null) {
        if (typeof localStorage === 'undefined') {
            console.error("[saveTool] [getItem] localStorage is not supported.");
            return defaultValue;
        }
        if(this.savePrefix.length === 0){
            console.error("[saveTool] [getItem] savePrefix is empty. (defaultValues in use)");
            return defaultValue;
        }
        try {
            const result = localStorage.getItem(this.savePrefix + key);
            if (result === null) return defaultValue;
            return JSON.parse(result);
        } catch (error) {
            console.error("Error getting item from localStorage", error);
            return defaultValue;
        }
    },
    /**@param {string} key
     * @param {Object} value*/
    setItem(key, value) {
        if (typeof localStorage === 'undefined') {
            console.error("[saveTool] [setItem] localStorage is not supported.");
            return;
        }
        if(savePrefix.length === 0){
            console.error("[saveTool] [getItem] savePrefix is empty.");
            return;
        }
        try {
            localStorage.setItem(this.savePrefix + key, JSON.stringify(value));
        } catch (error) {
            console.error("Error setting item to localStorage", error);
        }
    }
};

I usually additionally cache getItem (in initialize or smth) so as not to make requests to localStorage constantly. And when changing this cache field, I additionally call setItem. But it should work fine without it, not sure if caching makes a difference in performance. For example, you can store all data in your own playerSaveData object with a set of its properties (playerSaveData.score, playerSaveData.money, etc) and save and read it accordingly, and you can store different objects (even one value) separately.

Really interesting, u already tried to call an own api to save the game state too ? Is my game requirement

[PROGRESS]

To assist others and offer inspiration, I will share how I approached this project. I would greatly appreciate any feedback or suggestions for improving my code.

Game State Management System Overview

Introduction

This post aims to illustrate and explain the architecture and mechanisms involved in the game state management of our self-hosted game. By detailing our approach, we hope to inspire others and welcome feedback to enhance our implementation.

Architecture

Our system architecture is organized as follows:

  • APIFrontendGame

The game is self-hosted, and the interaction between these components is crucial for maintaining a smooth user experience.

Game State Configuration

The game state is dynamically constructed based on NPC dialogues and the initiation and conclusion of mini-games. Here’s how we manage the state across different phases of gameplay:

Constraints

  1. User Authentication: Users must log in to play the game.
  2. State Persistence: The backend must save the game state persistently.
  3. Achievement Trigger: Key player actions should trigger achievements.
  4. Achievement Recording: Achievements must be recorded in the backend.
  5. Social Display: Achievements should be visible in the social components of the game.

While the first constraint is straightforward, managing the game state (Constraint 2) is more involved and critical for the gameplay experience.

Initialization of Game State

Here is how we initialize and manage the game state:

GameStateController.prototype.initialize = function() {
    if (!window.gameStateLoaded) window.gameStateLoaded = false;
    this.app.on("game:save", this.saveGameState.bind(this));
    
    if (!window.gameStateLoaded)
        this.loadGameState();
    
    if (!window.bobState)
        window.bobState = {
            predio1: "predio1",
            predio2: "predio2",
            predio3: "predio3",
            predio6: "predio6",
            predio7: "predio7",
            predio9: "predio9",
        };
    
    if (!window.playerState)
        window.playerState = {
            predio1: 0,
            predio2: 0, 
            predio3: 0,
            predio6: 0,
            predio7: 0,
            predio9: 0,
            entryName: undefined,
            currentScene: "Campus",
            scenePosition: undefined,
            miniGames: {},
            seenConversations: {},
            completedMiniGames: {
                predio1: [],
                predio2: [],
                predio3: [],
                predio6: [],
                predio7: [],
                predio9: [],
            }
        };
    
    const building = window.bobState[this.entity.getPlayerBuilding()];
    if (window.playerState.currentScene.toLowerCase() === (building ? building.toLowerCase() : "")) {
        const bob = this.app.root.findByName("Bob");
        if (bob) bob.enabled = true;
    }
};

Loading and Saving Game State

The game state is loaded from local storage at the start and saved back both periodically and at the end of game sessions. This ensures that the player’s progress is not lost and that the game can be resumed seamlessly.

GameStateController.prototype.loadGameState = function() {
    window.gameStateLoaded = true;
    const gameStateString = localStorage.getItem('gameState');
    if (!gameStateString) return;

    const gameState = JSON.parse(gameStateString);
    window.bobState = gameState.bobState || window.bobState;
    window.playerState = gameState.playerState || window.playerState;

    this.app.scenes.changeScene(window.playerState.currentScene, (err, entity) => {
        if (err) {
            console.error(err);
            return;
        }
        this.useGameState(window.playerState.scenePosition, entity);
    });
};

GameStateController.prototype.useGameState = function(position, root) {
    const player = root.findByName("player");
    if (!window.playerState || !position) return;
    player.rigidbody.teleport(new pc.Vec3(position.x, position.y, position.z));
};

GameStateController.prototype.saveGameState = function() {
    const gameState = {
        bobState: { ...window.bobState },
        playerState: { ...window.playerState }
    };
    delete gameState.playerState.entryName;

    const gameStateString = JSON.stringify(gameState);
    localStorage.setItem('gameState', gameStateString);
    console.log("Game state saved!");
};

Interaction Between Components

  • Backend to Frontend: The backend communicates with the frontend via API.
  • Frontend to Game: The frontend passes data to the game using local storage, ensuring that game state is consistent and up-to-date.

Future Considerations

Further developments will focus on integrating achievements more deeply within the game’s ecosystem, including their triggers, recording, and display within the social components of the game.

Conclusion

This system has proven effective for our needs, providing a robust framework for game state management. We are open to suggestions and hope this documentation assists others in their projects.

1 Like