Criando uma caixa de diálogo com React para o meu jogo em Phaser

Escrito em 6 de outubro de 2021 - 🕒 4 min. de leitura

O Phaser funciona renderizando pixels dentro de um elemento canvas no DOM, e isso é ótimo, mas um dos grandes recursos do desenvolvimento da web é o poder do DOM e CSS para criar excelentes elementos de UI, e você não pode fazer isso com Phaser sozinho.

Dialog Box
Dialog Box

Recentemente, precisei de uma caixa de diálogo para o meu jogo e, pesquisando “rpg dialog box react”, encontrei um ótimo tutorial de Cameron Tatz sobre como criar uma caixa de diálogo usando React e react-spring, e isso é exatamente o que eu estava procurando.

O tutorial original usa uma versão antiga do react-spring, então eu atualizei o package e adicionei alguns outros recursos ao componente, que mostrarei neste tutorial.

O código

Esta caixa de diálogo usará react-spring para mostrar a mensagem letra por letra. Se você pressionar ENTER, irá terminar a animação da mensagem atual e mostrá-la por completo e se você pressionar ENTER novamente irá mover para a próxima mensagem se houver uma.

As duas principais mudanças no código para o componente Message.jsx são que a API useTransition mudou para receber apenas os itens e o objeto de configuração, ao invés dos itens, uma função de mapeamento e o objeto de configuração.

Também adicionei a propriedade onRest ao objeto de configuração useTransition para verificar se a animação da mensagem acabou. onRest é chamado após cada animação de letras, então preciso verificar o index da letra atual.

Se o prop forceShowFullMessage for enviado, simplesmente mostrarei a mensagem completa, sem nenhuma animação.

import React, { useMemo } from 'react';
import { animated, useTransition } from 'react-spring';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles((theme) => ({
    dialogMessage: () => ({
        fontSize: '12px',
        textTransform: 'uppercase',
    }),
}));

const Message = ({
    message = [],
    trail = 35,
    onMessageEnded = () => {},
    forceShowFullMessage = false,
}) => {
    const classes = useStyles();
    const items = useMemo(
        () => message.trim().split('').map((letter, index) => ({
            item: letter,
            key: index,
        })),
        [message]
    );

    const transitions = useTransition(items, {
        trail,
        from: { display: 'none' },
        enter: { display: '' },
        onRest: (status, controller, item) => {
            if (item.key === items.length - 1) {
                onMessageEnded();
            }
        },
    });

    return (
        <div className={classes.dialogMessage}>
            {forceShowFullMessage && (
                <span>{message}</span>
            )}
            {!forceShowFullMessage && transitions((styles, { item, key }) => (
                <animated.span key={key} style={styles}>
                    {item}
                </animated.span>
            ))}
        </div>
    );
};

export default Message;

Para o componente DialogBox.jsx, estou usando uma imagem de fundo muito semelhante à do tutorial original.

Dialog Box background
Dialog Box background

Este arquivo lida com as interações do teclado para a caixa de diálogo, portanto, se o jogador pressionar ENTER e a animação da mensagem ainda estiver sendo reproduzida, ele termina a animação atual definindo forceShowFullMessage como true. Se o jogador pressionar ENTER e a animação da mensagem já estiver concluída, então ele vai para a próxima mensagem.

Além de forceShowFullMessage, também recebo screenWidth e screenHeight como adereços, então posso aumentar ou diminuir o tamanho da caixa dependendo do tamanho do jogo Phaser.

import React, { useCallback, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';

// Images
import dialogBorderBox from '../images/dialog_borderbox.png';

// Components
import Message from './Message';

const useStyles = makeStyles((theme) => ({
    dialogWindow: ({ screenWidth, screenHeight }) => {
        const messageBoxHeight = Math.ceil(screenHeight / 3.5);

        return {
            imageRendering: 'pixelated',
            textTransform: 'uppercase',
            backgroundColor: '#e2b27e',
            border: 'solid',
            borderImage: `url("${dialogBorderBox}") 6 / 12px 12px 12px 12px stretch`,
            padding: '16px',
            position: 'absolute',
            top: `${Math.ceil(screenHeight - (messageBoxHeight + (messageBoxHeight * 0.1)))}px`,
            width: `${Math.ceil(screenWidth * 0.8)}px`,
            left: '50%',
            transform: 'translate(-50%, 0%)',
            minHeight: `${messageBoxHeight}px`,
        };
    },
    dialogTitle: () => ({
        fontSize: '16px',
        marginBottom: '12px',
        fontWeight: 'bold',
    }),
    dialogFooter: () => ({
        fontSize: '16px',
        cursor: 'pointer',
        textAlign: 'end',
        position: 'absolute',
        right: '12px',
        bottom: '12px',
    }),
}));

const DialogBox = ({
    messages,
    characterName,
    onDialogEnded,
    screenWidth,
    screenHeight,
}) => {
    const [currentMessage, setCurrentMessage] = useState(0);
    const [messageEnded, setMessageEnded] = useState(false);
    const [forceShowFullMessage, setForceShowFullMessage] = useState(false);
    const classes = useStyles({
        screenWidth,
        screenHeight,
    });

    const handleClick = useCallback(() => {
        if (messageEnded) {
            setMessageEnded(false);
            setForceShowFullMessage(false);
            if (currentMessage < messages.length - 1) {
                setCurrentMessage(currentMessage + 1);
            } else {
                setCurrentMessage(0);
                onDialogEnded();
            }
        } else {
            setMessageEnded(true);
            setForceShowFullMessage(true);
        }
    }, [currentMessage, messageEnded, messages.length, onDialogEnded]);

    useEffect(() => {
        const handleKeyPressed = (e) => {
            if (['Enter', 'Space', 'Escape'].includes(e.code)) {
                handleClick();
            }
        };
        window.addEventListener('keydown', handleKeyPressed);

        return () => window.removeEventListener('keydown', handleKeyPressed);
    }, [handleClick]);

    return (
        <div className={classes.dialogWindow}>
            <div className={classes.dialogTitle}>
                {characterName}
            </div>
            <Message
                action={messages[currentMessage].action}
                message={messages[currentMessage].message}
                key={currentMessage}
                forceShowFullMessage={forceShowFullMessage}
                onMessageEnded={() => {
                    setMessageEnded(true);
                }}
            />
            <div
                onClick={handleClick}
                className={classes.dialogFooter}
            >
                {(currentMessage === messages.length - 1 && messageEnded) ? 'Ok' : 'Next'}
            </div>
        </div>
    );
};

export default DialogBox;

E este será o resultado final

Dialog Box animation

Se você quiser mais detalhes sobre como integrar React com Phaser, leia o meu post sobre o assunto (em breve).

Tags:


Publicar um comentário

Comentários

Nenhum comentário.