LogoPixi’VN
Crea il tuo primo

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.

Questa guida è pensata solo per sviluppatori con una buona esperienza 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. Puoi trovare maggiori informazioni su come creare un nuovo progetto partendo da un template qui. Il modello "Game Engine" è un progetto semplice che contiene la struttura di base di un engine di gioco. Per impostazione predefinita, esporta le funzionalità di base di pixi-vn, rimuovendo dal pacchetto tutto ciò che non è utilizzato.

È stato creato un thread speciale per condividere la tua idea o chiedere aiuto durante lo sviluppo del tuo motore di gioco: 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

Sostituire la memoria di base di Pixi'VN è semplice.

Ad esempio, ecco come potresti sostituirlo con 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.get(key), 
            setVariable: (key, value) => storage.set(key, value), 
            removeVariable: (key) => storage.remove(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.

Ad esempio:

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
            onPreContinue: 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

Sostituire l'intero modulo narrativo è possibile ma non consigliato, in quanto rappresenta un punto di forza di Pixi'VN.

Poiché viene gestito in modo generico, puoi implementare la tua Label (l'elemento chiave della narrazione) estendendo LabelAbstract.

Ad esempio:

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";
        }
    }
}

Cronologia

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

Ad esempio:

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); 
        // ...
    }
}