LogoPixi’VN
Make your first

Make your game engine

Guide for advanced users on customizing and replacing Pixi’VN engine modules to build a custom game engine.

This guide is designed only for developers with good experience in JavaScript. If you are not a developer, you can skip this page and use the default engine.

One of the main goals of Pixi’VN is to provide basic utilities for developing video games. The engine is designed to be modular and flexible, allowing developers to use only the parts they need for their specific projects. This modularity also makes it easier to maintain and update the engine over time.

This is exactly what enables you to create your own game engine using the pixi-vn package.

Il primo step è creare un nuovo progetto. You can find more information on how to create a new project starting from a template here. The "Game Engine" template is a simple project that contains the basic structure of a game engine. By default, it exports the basic functionality of pixi-vn while removing anything unused from the package.

A special thread was created to share your idea or ask for help during the development of your game engine: Creating a game engine from pixi-vn

The second step is to understand that Pixi’VN is divided into independent "modules". This means you can replace one "module" without having to worry about modifying the others. To do this, you just need to modify GameUnifier, which connects the modules together, as explained below.

It is also important to understand that by default the engine saves the current state of the game at each step, to give the player the possibility to go back. To do this, the following functions are defined:

  • GameUnifier.getCurrentGameStepState: This function returns the current state of the game step.
  • GameUnifier.restoreGameStepState: This function restores the state of the game step.
  • Game.exportGameState: This function returns the current state of the game. (Optional)
  • Game.restoreGameState: This function restores the state of the game. (Optional)

Storage

Replacing the base storage of Pixi’VN is straightforward.

For example, here is how you could replace it with cacheable:

import { CacheableMemory } from "cacheable"; 

const storage = new CacheableMemory(); 
const flagStorage = new CacheableMemory(); 

export namespace Game {
    export async function initialize(
        element: HTMLElement,
        options: Partial<ApplicationOptions> & { width: number; height: number },
        devtoolsOptions?: Devtools
    ) {
        GameUnifier.init({
            getCurrentGameStepState: () => {
                return {
                    // ...
                    storage: storage.export(), 
                    storage: { 
                        main: createExportableElement([...storage.items]), 
                        flags: createExportableElement([...flagStorage.items]), 
                    }, 
                };
            },
            restoreGameStepState: async (state, navigate) => {
                // ...
                storage.restore(state.storage); 
                storage.setMany(state.storage.main); 
                flagStorage.setMany(state.storage.flags); 
            },
            // storage
            getVariable: (key) => storage.getVariable(key), 
            setVariable: (key, value) => storage.setVariable(key, value), 
            removeVariable: (key) => storage.removeVariable(key), 
            getFlag: (key) => storage.getFlag(key), 
            setFlag: (name, value) => storage.setFlag(name, value), 
            // onLabelClosing is called when the label is closed, it can use for example to clear the temporary variables in the storage
            onLabelClosing: (openedLabelsNumber) => StorageManagerStatic.clearOldTempVariables(openedLabelsNumber),  
            getVariable: (key) => storage.get(key), 
            setVariable: (key, value) => storage.set(key, value), 
            removeVariable: (key) => storage.delete(key), 
            getFlag: (key) => flagStorage.get(key) ?? false, 
            setFlag: (name, value) => flagStorage.set(name, value), 
        });
        // ...
    }

    export function clear() {
        // ...
        storage.clear(); 
        storage.clear(); // new class
        flagStorage.clear(); 
    }

    export function exportGameState(): GameState {
        return {
            // ...
            storageData: storage.export(), 
            storageData: { 
                main: createExportableElement([...storage.items]), 
                flags: createExportableElement([...flagStorage.items]), 
            }, 
        };
    }

    export async function restoreGameState(data: GameState, navigate: (path: string) => void) {
        // ...
        storage.restore(data.storageData); 
        storage.clear(); 
        storage.setMany(data.storageData.main); 
        flagStorage.clear(); 
        flagStorage.setMany(data.storageData.flags); 
    }
}

Renderer

The canvas is also a module that can be replaced. The default canvas uses the pixi.js library, but you can use any other canvas library you want.

You are not forced to use a WebGL-based 2D renderer. You can use any renderer you want, including 3D or React-based renderers. The only requirement is that the renderer state must be saved and restored at each step.

Example:

const canvas = new Canvas(); 

export namespace Game {
    export async function initialize(
        options: CanvasOptions, 
    ) {
        GameUnifier.init({
            getCurrentGameStepState: () => {
                return {
                    // ...
                    canvas: canvas.export(), 
                };
            },
            restoreGameStepState: async (state, navigate) => {
                // ...
                await canvas.restore(state.canvas); 
            },
            // This function is called when the step is completed, it can be used to force the completion of the animations
            onGoNextEnd: async () => { 
                canvas.forceCompletionOfAnimations(); 
            }, 
        });
        return await canvas.init(options); 
    }

    export function clear() {
        // ...
        canvas.clear(); 
    }

    export function exportGameState(): GameState {
        return {
            // ...
            canvasData: canvas.export(), 
        };
    }

    export async function restoreGameState(data: GameState, navigate: (path: string) => void) {
        // ...
        await canvas.restore(data.canvasData); 
    }
}

Sound

The sound module can also be replaced. The default uses PixiJS Sound, but you can use any sound library.

For example, to use howler.js:

import { Howl, Howler } from "howler"; 

export function exportHowlerState() { 
    const state = Howler._howls.map((howl) => ({ 
        src: howl._src, 
        volume: howl.volume(), 
        rate: howl.rate(), 
        loop: howl.loop(), 
        playing: howl.playing(), 
        seek: howl.seek(), 
    })); 
    return state; 
} 

export async function restoreHowlerState(state: Array<any>) { 
    state.forEach((soundData) => { 
        const sound = new Howl({ 
            src: soundData.src, 
            volume: soundData.volume, 
            rate: soundData.rate, 
            loop: soundData.loop, 
        }); 

        if (soundData.playing) { 
            sound.seek(soundData.seek); 
            sound.play(); 
        } 
    }); 
} 

export namespace Game {
    export async function initialize(
        element: HTMLElement,
        options: Partial<ApplicationOptions> & { width: number; height: number },
        devtoolsOptions?: Devtools
    ) {
        GameUnifier.init({
            getCurrentGameStepState: () => {
                return {
                    // ...
                    sound: sound.export(), 
                    sound: exportHowlerState(), 
                };
            },
            restoreGameStepState: async (state, navigate) => {
                // ...
                sound.restore(state.sound); 
                await restoreHowlerState(state.soundData); 
            },
        });
        // ...
    }

    export function clear() {
        sound.clear(); 
        Howler._howls.forEach((howl) => howl.unload()); 
        // ...
    }

    export function exportGameState(): GameState {
        return {
            // ...
            soundData: sound.export(), 
            soundData: exportHowlerState(), 
        };
    }

    export async function restoreGameState(data: GameState, navigate: (path: string) => void) {
        // ...
        sound.restore(data.soundData); 
        await restoreHowlerState(data.soundData); 
    }
}

Narrazione

Replacing the entire narration module is possible but not recommended, as it is a strong point of Pixi’VN.

Since it is handled generically, you can implement your own Label (the key element of narration) by extending LabelAbstract.

Example:

import { LabelAbstract, LabelProps, StepLabelType } from "@drincs/pixi-vn";
import sha1 from "crypto-js/sha1";

export default class Label<T extends {} = {}> extends LabelAbstract<Label<T>, T> {
    public get stepCount(): number {
        return this.steps.length;
    }
    public getStepById(stepId: number): StepLabelType<T> | undefined {
        return this.steps[stepId];
    }
    /**
     * @param id is the id of the label
     * @param steps is the list of steps that the label will perform
     * @param props is the properties of the label
     */
    constructor(id: string, steps: StepLabelType<T>[] | (() => StepLabelType<T>[]), props?: LabelProps<Label<T>>) {
        super(id, props);
        this._steps = steps;
    }

    private _steps: StepLabelType<T>[] | (() => StepLabelType<T>[]);
    /**
     * Get the steps of the label.
     */
    public get steps(): StepLabelType<T>[] {
        if (typeof this._steps === "function") {
            return this._steps();
        }
        return this._steps;
    }

    public getStepSha(index: number): string {
        if (index < 0 || index >= this.steps.length) {
            console.warn("stepSha not found, setting to ERROR");
            return "error";
        }
        try {
            let step = this.steps[index];
            let sha1String = sha1(step.toString().toLocaleLowerCase());
            return sha1String.toString();
        } catch (e) {
            console.warn("stepSha not found, setting to ERROR", e);
            return "error";
        }
    }
}

History

The history module can be replaced. The default uses the deep-diff library, but you can use any history library.

Example:

import HistoryManager, { HistoryManagerStatic } from "./classes/HistoryManager"; 

const stepHistory = new HistoryManager(); 

export namespace Game {
    export async function initialize(
        element: HTMLElement,
        options: Partial<ApplicationOptions> & { width: number; height: number },
        devtoolsOptions?: Devtools
    ) {
        GameUnifier.init({
            restoreGameStepState: async (state, navigate) => {
                HistoryManagerStatic.originalStepData = state; 
                // ...
            },
            addHistoryItem: (historyInfo, opstions) => {
                return stepHistory.add(historyInfo, opstions); 
            },
            // ...
        });
        // ...
    }

    export function clear() {
        // ...
        stepHistory.clear(); 
    }

    export function exportGameState(): GameState {
        return {
            // ...
            historyData: stepHistory.export(), 
        };
    }

    export async function restoreGameState(data: GameState, navigate: (path: string) => void) {
        stepHistory.restore(data.historyData); 
        // ...
    }
}