3. diel - Stav hry
V minulom diele som ukazoval, ako sa dajú mazať kocky V tomto diele ukážem, ako môžeme držať stav celej aplikácie na jednom mieste.
Pred čítaním tohto článku je dobré poznať základy MVC architektúry a niečo o frameworku Redux.
Stav aplikácie
Stav aplikácie sú dáta, ktoré predstavujú podstatu toho, čo používateľ vidí na obrazovke.
V bežnej webovej aplikácii - napr. Wikipédii, je stav aplikácia to, na akej stránke + odseku som. V Eshope je stav aplikácie zvyčajne aký produkt si prezerám + čo mám v košíku. V našej hre bude stav aplikácie aké kocky sú na scéne.
Redux je Framework pre držanie stavu aplikácie a manipuláciu s ním. V single page aplikáciách využívajúcich Redux sa stav aplikácie obvykle ukladá do čistých javascriptových objektov alebo immutable objektov. Ja využijem čisté JS objekty. Takto bude vyzerať defaultný stav hry:
const defaultState = { blocks: [{id:'My first block!!!',position:{x:0,y:0,z:0}}] };
Poznámka: Natočenie a vzdialenosť kamery by tiež mala byť súčasťou stavu aplikácie. To je však trochu ťažšie na implementáciu a tým pádom tomu venujem celý článok niekedy v budúcnosti.
Na to aby nám dnešná ukážka fungovala, musíme pridať knižnice Redux a knižnicu pre generovanie uuid. V dnešnom diele to urobím v hlavičke html. V niektorom z ďalších dielov použijem 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>
Stavom aplikácie sa manipuluje pomocou funkcie zvanej reducer. Do nej odovzdáme pôvodný stav + akciu a von vypadne stav nový. Je veľmi dôležité vyrobiť stav nový a neupravovať stav pôvodnej, aby sme nad aplikáciou nestratili kontrolu. Akcia je prostý JS objekt, ktorý má kľúč type. Ten pomenúva podstatu toho, ako sa stav zmení. Pre našu hru budeme mať zatiaľ 2 typy akcií: pridávanie nové kocky a zmazanie kocky.
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; } }
Teraz môžeme z reducer a defaultného stave vyrobiť Redux store. To je "centrálne úložisko" stave našej hry.
const store = Redux.createStore(stateReducer,defaultState);
Tam, kde som pôvodne zasahoval priamo do Babylon scény, budem odosielať akcie do Redux úložisko.
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čiť, aby sa kocky vytvorila či zmazala aj v scéne, nie iba v dátovom modeli. Preto si funkciu createScene rozdelím na dve:
- createScene - Pripraví mi prázdnu Babylon scénu. To docielim tak, že z createScene odstarním časť, kedy vytváram prvý kocku. Tú spustím pri štarte hry.
- updateScene - Vytvorí v scéne kocky presne podľa aktuálneho stavu aplikácie.
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); }); }
Tú spustím po vytvorení store a vždy, keď sa mi zmení stav aplikácie.
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: Nie je uplně efektívne po každej zmene stavu celú scénu přemazávat. V tomto prípade som tento postup zvolil preto, že za mierne plytvanie pamäťou, získam oveľa lepšiu kontrolu nad tým, čo sa v aplikácie deje. Neskôr môžeme funkciu updateScene optimalizovať.
Súbory
Vzhľadom na to, že sa nám začína projekt rozrastať, rozdelil som ho do niekoľkých súborov:
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
Rozrobenú hru si môžeš stiahnuť pod článkom, alebo ísť do Git repozitára, kde nájdeš najnovšiu verziu zdrojových kódov. Alebo si ju rovno môžeš vyskúšať na webappgames.github.io/web-game. V ďalšom diele ukážem, aké Feature vďaka tomu môžem docieliť.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 72x (3.59 kB)
Aplikácia je vrátane zdrojových kódov v jazyku JavaScript