coding

Criando um build para celular para o meu jogo com o Phaser JS - Game Devlog #23

Escrito em 9 de setembro de 2021 - 🕒 5 min. de leitura

Fala galera, aqui estamos nós novamente com outro devlog para o jogo Super Ollie, e como o jogo está sendo feito com JavaScript, eu posso exportar facilmente para praticamente qualquer plataforma, e hoje vou criar uma versão para celular do jogo.

Controlando com o celular

O personagem no meu jogo é controlado com um gamepad ou um teclado, então a maneira mais fácil de fazer uma versão para celular é adicionar algum tipo de controle virtual, tipo uns botões na tela do celular.

Botões no celular
Botões no celular

Para fazer isso eu vou precisar de 3 sprites para botões, um para o botão A, um para o botão B e outro para os botões direcionais, veja um exemplo do meu sprite abaixo:

Sprites dos botões
Sprites dos botões

Para o código, farei praticamente o mesmo que fiz para o HUD, que é criar uma classe e usar setScrollFactor(0) para as imagens ficarem estáticas na tela.

class TouchScreenControls extends GameGroup {
    constructor({
        scene,
        x = 0,
        y = 0,
        name,
        dPadAsset = 'd_pad',
        buttonAAsset = 'button_a',
        buttonBAsset = 'button_b',
        controlKeys = {},
    }) {
        const {
            LEFT,
            RIGHT,
            UP,
            DOWN,
            SPACE,
            NUMPAD_ZERO,
        } = Input.Keyboard.KeyCodes;

        const baseKeys = {
            d_pad_left: LEFT,
            d_pad_right: RIGHT,
            d_pad_up: UP,
            d_pad_down: DOWN,
            button_a: SPACE,
            button_b: NUMPAD_ZERO,
        };

        const keys = {
            ...baseKeys,
            ...controlKeys,
        };

        const children = [];
        [
            dPadAsset,
            buttonAAsset,
            buttonBAsset,
        ].forEach(this.assignButtonActions);

        super({
            scene,
            children,
            name,
        });

        this.setOrigin(0, 0);
        this.setDepth(TOUCH_CONTROLS_DEPTH);
        this.setControlsPositions();
    }
}

export default TouchScreenControls;

Para a função assignButtonActions, preciso escrever um código que será executado quando o jogador pressionar um dos botões na tela do celular, e a maneira mais fácil de fazer isso é simular as teclas do teclado que já estão mapeadas.

Por exemplo, eu já codifiquei que quando a barra de espaço é pressionada, o herói precisa pular, então tudo que eu preciso fazer agora é criar um código para quando o jogador pressiona o botão A na tela do celular, que irá simular que a barra de espaço foi pressionada. Depois de pesquisar um pouco, eu encontrei um Gist no GitHub com uma função que faz exatamente isso.

/**
 * source https://gist.github.com/GlauberF/d8278ce3aa592389e6e3d4e758e6a0c2
 * Simulate a key event.
 * @param {Number} keyCode The keyCode of the key to simulate
 * @param {String} type (optional) The type of event : down, up or press. The default is down
 */
export const simulateKeyEvent = (keyCode, type) => {
    const evtName = (typeof type === 'string') ? `key${type}` : 'keydown';
    const event = document.createEvent('HTMLEvents');
    event.initEvent(evtName, true, false);
    event.keyCode = keyCode;

    document.dispatchEvent(event);
};

Agora, para o código da função assignButtonActions, o segredo aqui é disparar um evento para pointerdown, pointerup e pointerout, caso contrário, não funcionará corretamente na touchscreen do celular.

assignButtonActions(asset) {
    if (asset === buttonPrefix + dPadAsset) {
        let angle = 0;
        ['left', 'up', 'right', 'down'].forEach((dPadDirection) => {
            const assetName = `${asset}_${dPadDirection}`;
            const child = new GameObjects.Image(
                scene,
                x,
                y,
                asset
            );
            child.setOrigin(0, 0);
            child.setName(assetName);
            child.setAngle(angle);
            child.setInteractive();

            child.on('pointerdown', () => {
                child.setTint(0x90989e);
                simulateKeyEvent(keys[assetName], 'down');
            });

            child.on('pointerup', () => {
                child.clearTint();
                simulateKeyEvent(keys[assetName], 'up');
            });

            child.on('pointerout', () => {
                child.clearTint();
                simulateKeyEvent(keys[assetName], 'up');
            });
            angle += 90;
            children.push(child);
        });
    } else {
        const child = new GameObjects.Image(
            scene,
            x,
            y,
            asset
        );
        child.setOrigin(0, 0);
        child.setName(asset);
        child.setInteractive();

        child.on('pointerdown', () => {
            child.setTint(0x90989e);
            simulateKeyEvent(keys[asset], 'down');
        });

        child.on('pointerup', () => {
            child.clearTint();
            simulateKeyEvent(keys[asset], 'up');
        });

        child.on('pointerout', () => {
            child.clearTint();
            simulateKeyEvent(keys[asset], 'up');
        });
        children.push(child);
    }
}

A função setControlsPositions é meio sem graça, ela simplesmente define as posições de cada botão na tela:

setControlsPositions() {
    const { width, height } = this.scene.cameras.main;
    const paddingX = 20;
    const paddingY = 60;
    this.forEach((child) => {
        const { name } = child;
        child.setX(paddingX);
        child.setY(height - paddingY);
        child.setScrollFactor(0);

        switch (name) {
            case 'big_d_pad_left': {
                child.setX(child.x);
                child.setY(child.y - 20);
                break;
            }

            case 'big_d_pad_up': {
                child.setX(child.x + 58);
                child.setY(child.y - 49);
                break;
            }

            case 'big_d_pad_right': {
                child.setX(child.x + 88);
                child.setY(child.y + 8);
                break;
            }

            case 'big_d_pad_down': {
                child.setY(child.y + 37);
                child.setX(child.x + 30);
                break;
            }

            case 'big_button_a': {
                child.setY(child.y - 25);
                child.setX(width - paddingX - 35);
                break;
            }

            case 'big_button_b': {
                child.setY(child.y - 5);
                child.setX(width - paddingX - 85);
                break;
            }

            case 'd_pad_left': {
                child.setX(child.x);
                child.setY(child.y - 20);
                break;
            }

            case 'd_pad_up': {
                child.setX(child.x + 40);
                child.setY(child.y - 40);
                break;
            }

            case 'd_pad_right': {
                child.setX(child.x + 60);
                break;
            }

            case 'd_pad_down': {
                child.setY(child.y + 20);
                child.setX(child.x + 20);
                break;
            }

            case 'button_a': {
                child.setY(child.y - 35);
                child.setX(width - paddingX - 35);
                break;
            }

            case 'button_b':
            default: {
                child.setY(child.y - 20);
                child.setX(width - paddingX - 75);
                break;
            }
        }
    });
}

Build para celular

Eu não quero ter que carregar assets que não sejam usados, então vou criar um build separado para celulares e verificar se devo ou não carregar os assets dos botões:

preload() {
    if (getGlobal('IS_MOBILE_BUILD')) {
        this.load.image('big_d_pad', bigDPad);
        this.load.image('big_button_a', bigButtonA);
        this.load.image('big_button_b', bigButtonB);
        this.load.image('big_button_Y', bigButtonY);
        this.load.image('d_pad', dPad);
        this.load.image('button_a', buttonA);
        this.load.image('button_b', buttonB);
        this.load.image('button_Y', buttonY);
    }
}

create() {
    if (getGlobal('IS_MOBILE_BUILD')) {
        this.touchScreenControls = new TouchScreenControls({
            scene: this,
            name: 'touchScreenControls',
            dPadAsset: 'd_pad',
            buttonAAsset: 'button_a',
            buttonBAsset: 'button_b',
            useBigButtons: true,
        });

        this.touchScreenControls.forEach((child) => {
            this.add.existing(child);
        });
    }
}

Agora no meu package.json, irei criar um script de build com "build-mobile": "webpack --mode production --config webpack.config.prod.js --env = mobile", e na minha configuração do Webpack eu vou exportar uma função ao em vez de um objeto, para eu poder acessar o parâmetro env:

// webpack.config.js
module.exports = (env = {}) => {
    const isMobileBuild = env === 'mobile';
    if (isMobileBuild) {
        BUILD_PATH = path.resolve(__dirname, 'www/build');
        DIST_PATH = path.resolve(__dirname, 'www');
    }
}

Além disso, vou criar a variável IS_MOBILE_BUILD com o Webpack Define Plugin:

new webpack.DefinePlugin({
    IS_MOBILE_BUILD: JSON.stringify(isMobileBuild),
})

Buildando o APK

Quero que meu jogo rode no Android por enquanto e, para isso, usarei o Cordova. Primeiro, preciso instalar o Cordova com npm install -g cordova e, em seguida, instalar o Android Studio e yada yada e, finalmente, criar meu projeto com cordova create skate.

Cordova espera que os arquivos de build estejam na pasta www/, e é por isso que eu tive que mudar o meu script do Webpack antes. Agora posso simplesmente executar cordova build --release android e um arquivo APK não assinado será gerado para mim.

E é isso! Mole mole, fácil fácil!

Nos vemos no próximo devlog, pessoal!

Tags:


Publicar um comentário

Comentários

Nenhum comentário.