- Blog ➔
- Programação ↴
Criando uma caixa de diálogo com React para o meu jogo em Phaser
Escrito em 6 de outubro de 2021 - 🕒 4 min. de leituraO Phaser funciona renderizando píxeis 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.
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.
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
Se você quiser mais detalhes sobre como integrar React com Phaser, leia o meu post sobre o assunto.
Tags:
Posts relacionados
Publicar um comentário
Comentários
Nenhum comentário.