3. díl - Stav hry

JavaScript 3D webová hra Stav hry

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulém díle jsem ukazoval, jak se dají mazat krychle V tomto díle ukážu, jak můžeme držet stav celé aplikace na jednom místě.

Před čtením tohoto článku je dobré znát základy MVC architektury a něco o frameworku Redux.

Webová hra

Stav aplikace

Stav aplikace jsou data, která představují podstatu toho, co uživatel vidí na obrazovce.

V běžné webové aplikaci - např. Wikipedii, je stav aplikace to, na jaké stránce + odstavci jsem. V Eshopu je stav aplikace obvykle jaký produkt si prohlížím + co mám v košíku. V naší hře bude stav aplikace jaké krychle jsou na scéně.

Redux je Framework pro držení stavu aplikace a manipulaci s ním. V single page aplikacích využívajících Redux se stav aplikace obvykle ukládá do čistých javascriptových objektů nebo Immutable objektů. Já využiji čisté JS objekty. Takto bude vypadat defaultní stav hry:

const defaultState = {
    blocks: [{id:'My first block!!!',position:{x:0,y:0,z:0}}]
};

Poznámka: Natočení a vzdálenost kamery by také měla být součástí stavu aplikace. To je však trochu obtížnější na implementaci a tím pádem tomu věnuji celý článek někdy v budoucnu.

Na to aby nám dnešní ukázka fungovala, musíme přidat knihovny Redux a knihovnu pro generování uuid. V dnešním díle to udělám v hlavičce html. V některém z dalších dílů použiji balíčkovací systém npm.

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.6.0/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/node-uuid/1.4.8/uuid.min.js"></script>

Stavem aplikace se manipuluje pomocí funkce zvané reducer. Do ní předáme původní stav + akci a ven vypadne stav nový. Je velmi důležité vyrobit stav nový a neupravovat stav původní, abychom nad aplikací neztratili kontrolu. Akce je prostý JS objekt, který má klíč type. Ten pojmenovává podstatu toho, jak se stav změní. Pro naši hru budeme mít zatím 2 typy akcí: přidávání nové krychle a smazání krychle.

function stateReducer(state, action) {
    switch (action.type) {

        case 'BLOCK_ADD':
            return {
                blocks: state.blocks.concat([action.newBlock])
            };

        case 'BLOCK_DELETE':
            return {
                blocks: state.blocks.filter((block)=>block.id!==action.blockId)
            };

        default:
            return state;
    }
}

Nyní můžeme z reduceru a defaultního stavu vyrobit Redux store. To je "centrální uložište" stavu naší hry.

const store = Redux.createStore(stateReducer,defaultState);

Tam, kde jsem původně zasahoval přímo do Babylon scény, budu odesílat akce do Redux uložiště.

function onPointerUp(event) {
    const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
    if (pickInfo.hit) {
        const currentMesh = pickInfo.pickedMesh;
        switch (event.button) {
            case 0:
                const diff = currentMesh.position.subtract(pickInfo.pickedPoint);
                const position = currentMesh.position.clone();

                ['x', 'y', 'z'].forEach((dimension) => {
                    if (diff[dimension] >= 0.5 - 0.001) {
                        position[dimension]--;
                    } else if (diff[dimension] <= -0.5 + 0.001) {
                        position[dimension]++;
                    }
                });

                store.dispatch({
                    type: 'BLOCK_ADD', newBlock: {
                        id: uuid.v4(),//Pro nový blok generuji nové id pomocí knihovny "uuid":https://www.npmjs.com/package/uuid
                        position: {
                            x: Math.floor(position.x),
                            y: Math.floor(position.y),
                            z: Math.floor(position.z)
                        }
                    }
                });
                break;

            case 2:
                store.dispatch({type: 'BLOCK_DELETE', blockId: currentMesh.name});
                break;
        }
    }
}

Musím však zaručit, aby se krychle vytvořila či smazala i ve scéně, ne pouze v datovém modelu. Proto si funkci createScene rozdělím na dvě:

  • createScene - Připraví mi prázdnou Babylon scénu. To docílím tak, že z createScene odstarním část, kdy vytvářím první krychli. Tu spustím při startu hry.
  • updateScene - Vytvoří ve scéně krychle přesně podle aktuálního stavu aplikace.
function updateScene(scene, state) {
    //Nejdříve vyčistím scénu.
    scene.meshes.forEach((mesh) => {
        mesh.dispose();
    });
    scene.meshes = [];

    //Vyhledám základní materiál.
    const materialNormal = scene.materials.find(material=>material.name==='material-normal');

    //Potom ji naplním podle aktuálního stavu.
    state.blocks.forEach(block=>{
        const newBox = BABYLON.Mesh.CreateBox(block.id, 1, scene);
        newBox.material = materialNormal;
        newBox.position = new BABYLON.Vector3(block.position.x, block.position.y, block.position.z);
    });
}

Tu spustím po vytvoření store a vždy, když se mi změní stav aplikace.

const store = Redux.createStore(stateReducer, defaultState);

var canvas = document.getElementById("scene");
var engine = new BABYLON.Engine(canvas, true);
var scene = createScene(canvas, engine);

//Vytvořím pomocnou funkci render.
function render() {
    updateScene(scene, store.getState());
}

store.subscribe(render);
render();

engine.runRenderLoop(function () {
    scene.render();
});

// Resize
window.addEventListener("resize", function () {
    engine.resize();
});

Poznámka: Není uplně efektivní po každé změně stavu celou scénu přemazávat. V tomhle případě jsem tento postup zvolil proto, že za mírné plýtvání pamětí, získám mnohem lepší kontrolu nad tím, co se v aplikace děje. Později můžeme funkci updateScene optimalizovat.

Soubory

Vzhledem k tomu, že se nám začíná projekt rozrůstat, rozdělil jsem ho do několika souborů:

src
    script
        scene
            create-scene.ts
            update-scene.ts
        state
            default-state.js
        state-reducers
            state-reducer.js
        browser.tsx
    style
        index.css
index.html

Rozdělanou hru si můžeš stáhnout pod článkem, nebo jít do Git repozitáře, kde najdeš nejnovější verzi zdrojových kódů. Nebo si ji rovnou můžeš vyzkoušet na hejny.github.io/web-game. V dalším díle ukáži, jaké featury díky tomu mohu docílit.


 

Stáhnout

Staženo 20x (3.59 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

 

Článek pro vás napsal Pavol Hejný
Avatar
Jak se ti líbí článek?
1 hlasů
Autor se věnuje vývoji mnoha www aplikací - https://www.pavolhejny.com/
Miniatura
Předchozí článek
Vylepšování základní scény
Miniatura
Všechny články v sekci
Vytvoř si vlastní 3D webovou hru
Miniatura
Následující článek
Zpět a Vpřed
Aktivity (12)

 

 

Komentáře

Avatar
Michal Rosival:11. července 11:20

Super séria :)
kedy sa môžem tešiť na ďalší diel?

 
Odpovědět 11. července 11:20
Avatar
Pavol Hejný
Autoredaktor
Avatar
Odpovídá na Michal Rosival
Pavol Hejný:12. července 1:59

Dík za pochvalu,
právě jsem vydal další díl https://www.itnetwork.cz/…zpet-a-vpred .
Popravdě jsem se na tom trochu zasekl, protože jsem viděl hrozně moc věcí, jak je udělat lépe a tak aktualizuji i články, které jsem už vydal.
Ale do 8.8 by to mělo dospět k něčemu opravdu hratelnému, protože budu mít livestream na Péhápkářích o 3D hrách na webu a chtěl bych to tam využít.

 
Odpovědět 12. července 1:59
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 2 zpráv z 2.