LogoPixi’VN
Make your first

Crea il tuo game engine

Guida per utenti avanzati sulla personalizzazione e la sostituzione dei moduli del motore Pixi'VN per creare un motore di gioco personalizzato.

This guide is designed only for developers with good experience in JavaScript. Se non sei uno sviluppatore, puoi saltare questa pagina e utilizzare l'engine predefinito.

Uno degli obiettivi principali di Pixi’VN è fornire utilità di base per lo sviluppo di videogiochi. Il motore è progettato per essere modulare e flessibile, consentendo agli sviluppatori di utilizzare solo le parti di cui hanno bisogno per i loro progetti specifici. Questa modularità semplifica anche la manutenzione e l'aggiornamento dell'engine nel tempo.

Questo è esattamente ciò che ti permette di creare il tuo engine di gioco utilizzando il pacchetto pixi-vn.

Il primo step è creare un nuovo progetto. You can find more information on how to create a new project starting from a template here. Il modello "Game Engine" è un progetto semplice che contiene la struttura di base di un engine di gioco. 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: discussions#389

The second step is to understand that Pixi’VN is divided into independent "modules". Ciò significa che è possibile sostituire un "modulo" senza doversi preoccupare di modificare gli altri. 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); 
        // ...
    }
}