IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Zpět a Vpřed

V minulém díle, Stav hry, jsem ukazoval, co to je stav aplikace a jak ho držet.

V tomto díle ukáži, jaké výhody nám přináší zachování stavu aplikace do URL.

Webová hra - WebGL a BabylonJS - 3D webová hra

Logování

Ve chvíli, kdy držíme stav aplikace na jednom místě, můžeme všechny jeho změny velmi elegantně logovat a tím vidět, co se v aplikaci děje. Vytvoříme si funkci wrapReducer():

function wrapReducer(reducer){
    // Kontrola pro případ, že bych byl v prostředí node.js
    if('groupCollapsed' in console){
        return function (oldState,action) {
            console.groupCollapsed(`==[${action.type}]==>`);
            console.log(oldState);
            console.log('||');
            console.log('||');

            console.log(`[${action.type}]`, action);
            const newState = reducer(oldState, action);

            console.log('||');
            console.log('||');
            console.log('\\/');
            console.log(newState);

            console.groupEnd();
            return newState;
        }
    }else{

        return reducer;
    }
}

Potom vytváříme store tak, že reducer nejdříve "obalíme" touto logovací funkcí:

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

Nyní se po každé akci do konzole prohlížeče vypíše, co se stalo + stav předtím a potom.

URL

Ve webové aplikaci by bylo dobré, aby se každá změna stavu zaznamenávala do URL adresy v prohlížeči. Do URL však nemůžeme uložit příliš mnoho informací a tak můžeme stav ukládat do LocalStorage pod unikátním IDčkem a dané ID dávat do URL adresy.

function loadState(key){
    try {
        const serializedState = localStorage.getItem(key);
        if (serializedState === null) {
            return null;
        }
        return JSON.parse(serializedState);
    } catch (err) {
        return null;
    }
}
saveState(state){
    const key = uuid.v4();
    const serializedState = JSON.stringify(state);
    localStorage.setItem(key,serializedState);
    return key;
}
function createStateFromUri(uri){
    const key = uri.split('#',2)[1];
    const state = loadState(key);
    if(state){
        return state;
    }else{
        return defaultState;
    }
}
function createUriFromState(state){
    var key = saveState(state);
    return `#${key}`;
}

Nyní budeme poslouchat změny na store nejen kvůli vykreslování, ale i proto, abychom si nový stav uložili do LocalStorage a URL:

const store = Redux.createStore(stateReducer, createStateFromUri(document.location.toString()));

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

function render() {
    updateScene(scene, store.getState());
}

store.subscribe(render);
render();


store.subscribe(()=> {
    const state = store.getState();
    const uri = createUriFromState(state);
    const title = createTitleFromState(state);
    document.title = title;
    history.pushState({}, title, uri);
});

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


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

Zpět a vpřed

Nyní ukládáme stav do URL adresy a při načtení hry ho z něj načítáme. Pokud chceme plně využít potenciál webové aplikace, musíme umožnit uživateli klikat na tlačítka zpět a vpřed v prohlížeči.

Ve stavu aplikace budeme ukládat poslední akci a přidáme akci CHANGE_STATE, která změní stav podle toho, co ji přijde:

function stateReducer(state, action) {
    switch (action.type) {
        case 'CHANGE_STATE':
            return  Object.assign({},action.state, {lastAction:action});

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

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

        default:
            return state;
    }
}

Do URL budeme ukládat změny stavu pouze pokud poslední akce není CHANGE_STATE:

const store = Redux.createStore(wrapReducer(stateReducer), createStateFromUri(document.location.toString()));
const root = document.getElementById("root");
const canvas = document.getElementById("scene");
const engine = new BABYLON.Engine(canvas, true);
const scene = createScene(canvas, engine, store);
engine.runRenderLoop(function () {
    scene.render();
});
window.addEventListener("resize", function () {
    engine.resize();
});

function render() {
    updateScene(scene, store.getState());
}
store.subscribe(()=>{
    const state = store.getState();
    if(state.lastAction.type!=='CHANGE_STATE'){
        const uri = createUriFromState(state);
        history.pushState({}, '', uri);
    }
});
store.subscribe(render);
render();

// Vždy když uživatel klikne na zpět nebo vpřed, odešle se akce CHANGE_STATE.
window.onpopstate = function () {
    const state = createStateFromUri(document.location.toString());
    store.dispatch(createAction.CHANGE_STATE(state));
};

Titulek

Na to, aby naše hra naplno využívala historii prohlížeče, už stačí měnit titulek podle aktuálního stavu. Vytvoříme proto funkci, která vyrábí ze stavu titulek stránky:

const WEB_NAME = 'Simple web game';
const TITLE_SEPARATOR = ' | ';

function createTitleFromState(state) {
    let titleParts = [];

    if (state.blocks.length> 1) {
        titleParts.push(state.blocks.length + ' blocks world');
    }
    titleParts.push(WEB_NAME);

    return titleParts.join(TITLE_SEPARATOR);
}

A tuto funkci při každé změně stavu využijeme:

store.subscribe(()=>
    const state = store.getState();
    if(state.lastAction.type!=='CHANGE_STATE'){
        const uri = createUriFromState(state);
        const title = createTitleFromState(state);
        document.title = title;
        history.pushState({}, title, uri);
    }
});

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 webappgames.git­hub.io/web-game.

V dalším díle, TypeScript, ukáži, jak jednotlivé Javascriptové soubory spojit pomocí nástroje WebPack.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

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

 

Předchozí článek
Stav hry
Všechny články v sekci
WebGL a BabylonJS - 3D webová hra
Přeskočit článek
(nedoporučujeme)
TypeScript
Článek pro vás napsal Pavol Hejný
Avatar
Uživatelské hodnocení:
2 hlasů
/^(web )?(app )?developer$/
Aktivity