Movimento e ataque de personagem no Cocos: de nós a animações

Seu personagem está no centro da tela. Você pressiona a tecla direcional e nada acontece. Ao abrir o código, encontra a lógica de movimento, a reprodução de animações e as verificações de estado todas empilhadas dentro de um único update. Você muda a animação e esquece de mudar o estado; muda o estado e esquece a velocidade. No fim, nem você sabe mais o que o personagem deveria estar fazendo naquele momento.
Separar nós parece um pouco abstrato, mas a ideia é simples: movimento e animação precisam ser controlados separadamente. O nó Player fica responsável pelo deslocamento horizontal; o nó Body cuida do salto vertical e da animação de ataque. Os dois rodam de forma independente, sem se atrapalhar. Este artigo começa pela arquitetura de nós, passa pela configuração do componente de animação, pela implementação da máquina de estados e pela escolha do esquema de entrada, e termina com um conjunto completo de scripts para um personagem de luta lateral.
Arquitetura de nós do personagem — movimento horizontal + animação vertical sobreposta
O tutorial oficial do Cocos Creator 3.7 tem um detalhe de projeto fácil de ignorar: separar Player e Body em dois nós. Muita gente, eu incluído no começo, acha desnecessário. Afinal, não parece mais simples usar um único nó para movimento, animação e detecção de colisão?
O problema aparece quando animação e movimento têm ritmos diferentes. Imagine o personagem saltando: no eixo horizontal ele avança em velocidade constante; no eixo vertical ele desenha uma parábola. Com um único nó, o update precisa calcular o deslocamento horizontal e controlar a altura do salto ao mesmo tempo. O código começa organizado e logo perde o rumo. Quando entram animação de ataque, efeito de dano e até rotação ou escala na morte, a lógica de um nó só praticamente sai do controle.
A vantagem da separação é direta: movimento fica com movimento, animação fica com animação. O nó Player cuida apenas do aumento ou redução da coordenada x; o nó Body cuida apenas da coordenada y nos saltos e da troca de quadros de animação. Um não disputa responsabilidade com o outro.
Estrutura dos nós:
Player (nó raiz)
├── PlayerControl.ts (lógica de movimento)
└── Body (nó filho)
├── Sprite (imagem do personagem)
└── componente Animation
Exemplo de código — controle de movimento no nó Player:
// PlayerControl.ts - script do nó Player
@property(CCFloat)
speed: number = 200; // Velocidade de movimento
private moveDir: Vec2 = new Vec2(0, 0);
update(dt: number) {
if (this.moveDir.mag() > 0.5) {
const vx = this.moveDir.x * this.speed;
this.node.x += vx * dt; // Altera apenas a posição horizontal
}
}
// Chamado externamente para definir a direção
setMoveDir(dir: Vec2) {
this.moveDir = dir;
}
Exemplo de código — controle de animação no nó Body:
// BodyControl.ts - script do nó Body
@property(Animation)
animation: Animation = null;
jump(height: number = 100) {
// Animação de salto: deslocamento vertical + quadros da animação jump
this.node.runAction(cc.jumpBy(1.0, 0, 0, height, 1));
this.animation.play('jump');
}
attack() {
this.animation.play('attack');
// Depois do ataque, volta automaticamente para idle
this.scheduleOnce(() => {
this.animation.play('idle');
}, 0.5);
}
Com isso, a lógica de movimento fica no nó Player e a lógica de animação fica no nó Body. Alterar a altura do salto não exige tocar no código de movimento; mudar a velocidade também não exige procurar dentro do script de animação. A manutenção fica bem menos cansativa.
Configuração do componente de animação — vínculo entre Animation e AnimationClip
O componente de animação é um módulo nativo do Cocos Creator, mas alguns atributos são especialmente importantes: defaultClip, o array clips e o playOnLoad, que muita gente passa batido.
Principais atributos do componente Animation:
defaultClip: a animação tocada por padrão, normalmente a animação de idleclips: array de recursos de animação, como idle, caminhada e ataquecurrentClip: animação em reprodução no momento, usada em tempo de execução
O passo a passo é simples:
- Adicione um componente Animation ao nó Body
- Arraste os recursos AnimationClip para o array clips
- Defina defaultClip como a animação de idle
- Marque playOnLoad para o personagem já começar parado
Exemplo de código — configuração do componente de animação:
// PlayerAnimation.ts - script de controle de animação
@property(Animation)
animation: Animation = null;
@property([AnimationClip])
clips: AnimationClip[] = []; // Idle, caminhada, ataque, salto
onLoad() {
this.animation.clips = this.clips;
this.animation.defaultClip = this.clips[0]; // Idle padrão
this.animation.play();
}
playIdle() { this.animation.play('idle'); }
playMove() { this.animation.play('move'); }
playAttack() {
this.animation.play('attack');
this.scheduleOnce(() => { this.playIdle(); }, 0.5);
}
Um detalhe importante: ao trocar de animação, é melhor chamar stop() antes de play(), principalmente em clipes de execução única, como ataques. Sem isso, se o personagem estiver andando e você pressionar o ataque, a animação de ataque pode disputar quadros com a animação de caminhada. Na tela, parece que o personagem está golpeando enquanto desliza, o que fica bem estranho.
Forma correta de trocar animações:
switchAnimation(name: string) {
if (this.animation.currentClip?.name !== name) {
this.animation.stop();
this.animation.play(name);
}
}
Com o componente de animação configurado, o próximo passo é a máquina de estados. Ela faz a troca de animações seguir uma regra clara, em vez de espalhar verificações como if (velocidade > 0) play('move') por vários scripts.
Máquina de estados de animação — idle → movimento → ataque → volta para idle
A ideia central de uma máquina de estados é esta: em qualquer instante, o personagem só pode estar em um estado, como idle, move ou attack, e as transições entre estados precisam ter condições explícitas. Não é troca aleatória; existe uma regra.
No começo, também achei que máquina de estados fosse projeto demais para algo simples. Afinal, é só tocar uma animação; chamar play('attack') não resolve? Resolve até você adicionar salto, depois dano, e então descobrir que as verificações de transição ficaram espalhadas por todo lado: no script de movimento, no script de animação e no tratamento de entrada. Você muda uma condição em um lugar, esquece de sincronizar nos outros dois, e o comportamento do personagem começa a quebrar.
Fluxo da máquina de estados:
[Entry] → [idle]
↓ speed>0
[move]
↓ attackKey
[attack] → [Exit] → [idle]
São três estados: idle, move e attack. As condições de transição são:
- idle → move: velocidade maior que 0
- move → idle: velocidade igual a 0
- any → attack: tecla de ataque pressionada
- attack → idle: fim da animação de ataque
Exemplo de código — implementação da máquina de estados:
// PlayerStateMachine.ts
enum State {
Idle = 'idle',
Move = 'move',
Attack = 'attack'
}
@property(Animation)
animation: Animation = null;
private currentState: State = State.Idle;
private isMoving: boolean = false;
private attackKeyPressed: boolean = false;
switchState(newState: State) {
if (this.currentState === newState) return; // Evita repetição
this.animation.stop();
this.animation.play(newState);
this.currentState = newState;
}
update(dt: number) {
// Verificação do estado de movimento
if (this.isMoving && this.currentState !== State.Move) {
this.switchState(State.Move);
} else if (!this.isMoving && this.currentState !== State.Idle) {
this.switchState(State.Idle);
}
// Estado de ataque, com prioridade maior que movimento
if (this.attackKeyPressed && this.currentState !== State.Attack) {
this.switchState(State.Attack);
this.scheduleOnce(() => { this.switchState(State.Idle); }, 0.5);
}
}
setMoving(value: boolean) { this.isMoving = value; }
triggerAttack() {
this.attackKeyPressed = true;
this.scheduleOnce(() => { this.attackKeyPressed = false; }, 0.1);
}
Os pontos mais importantes da máquina de estados:
- Prioridade de estado: ataque tem prioridade sobre movimento, então o personagem não se move durante o ataque
- Proteção contra troca repetida:
if (currentState === newState) returnevita tocar o mesmo estado várias vezes - Callback de timer: depois que a animação de ataque termina, o personagem volta automaticamente para idle, sem depender da tecla ser solta
A máquina de estados concentra a lógica de troca de animações em um único lugar. Se depois você quiser adicionar salto, dano ou morte, basta incluir novos valores no enum State e acrescentar as condições de transição. É muito mais fácil de expandir do que um conjunto solto de if.
Comparação de controles de entrada — teclado vs toque vs joystick virtual
Os três esquemas funcionam, mas cada um se encaixa em um cenário. Escolher errado afeta diretamente a sensação de controle.
| Esquema | Cenário ideal | Vantagens | Desvantagens | Complexidade |
|---|---|---|---|---|
| Teclado | PC e fase de depuração | Simples, direto e responsivo | Não funciona no celular | Baixa |
| Toque | Jogos com movimento em tela cheia, como voo e corrida infinita | Natural e sem UI cobrindo a tela | Menos precisão de direção e mais chance de toque acidental | Média |
| Joystick virtual | ARPG e jogos de luta | Direção precisa e controle em 360 graus | Ocupa espaço na tela | Alta |
Entrada por teclado, a melhor opção no PC
Se o minigame roda principalmente no PC, ou se você está em fase de depuração e quer validar a lógica rápido, o teclado é a solução mais simples.
Exemplo de código — entrada por teclado:
// KeyboardInput.ts
private moveDir: Vec2 = new Vec2(0, 0);
onLoad() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onKeyDown(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_A:
case KeyCode.ARROW_LEFT:
this.moveDir.x = -1; break;
case KeyCode.KEY_D:
case KeyCode.ARROW_RIGHT:
this.moveDir.x = 1; break;
case KeyCode.KEY_J:
this.playerStateMachine.triggerAttack(); break;
}
}
onKeyUp(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_A:
case KeyCode.ARROW_LEFT:
case KeyCode.KEY_D:
case KeyCode.ARROW_RIGHT:
this.moveDir.x = 0; break;
}
}
update(dt: number) {
this.playerControl.setMoveDir(this.moveDir);
this.playerStateMachine.setMoving(this.moveDir.mag() > 0.5);
}
Detalhes da entrada por teclado:
- Mapeamento de teclas: A/D e setas esquerda/direita são aceitas ao mesmo tempo, cobrindo hábitos diferentes de jogadores
- Tecla de ataque: J, comum em jogos de luta
- Sincronização de estado: a direção de movimento e a máquina de estados são atualizadas a cada frame
Joystick virtual, a melhor opção no celular
Se o minigame roda no celular e é um jogo de luta lateral ou ARPG, onde direção precisa importa, o joystick virtual quase sempre é a escolha certa.
Pontos essenciais na implementação de um joystick virtual:
- Limite de área: o centro do joystick não pode ultrapassar o círculo de fundo
- Normalização da direção: o retorno deve ser um vetor unitário
- Soltar o toque: o joystick volta automaticamente para o centro
Exemplo de código — implementação de joystick virtual:
// Joystick.ts
@property(Node)
joystickBg: Node = null;
@property(Node)
joystickHandle: Node = null;
@property(CCFloat)
radius: number = 100;
private moveDir: Vec2 = new Vec2(0, 0);
onLoad() {
input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this);
input.on(Input.EventType.TOUCH_END, this.onTouchEnd, this);
}
onTouchMove(event: EventTouch) {
const touchPos = event.getUILocation();
const localPos = this.joystickBg.inverseTransformPoint(
v3(), v3(touchPos.x, touchPos.y, 0)
);
const distance = localPos.length();
if (distance > this.radius) {
localPos.x = this.radius * localPos.x / distance;
localPos.y = this.radius * localPos.y / distance;
}
this.joystickHandle.setPosition(localPos);
this.moveDir = v2(localPos.x, localPos.y).normalize();
}
onTouchEnd() {
this.joystickHandle.setPosition(Vec3.ZERO);
this.moveDir = v2(0, 0);
}
getMoveDir(): Vec2 { return this.moveDir; }
Detalhes importantes do joystick virtual:
- Conversão de coordenadas:
inverseTransformPointtransforma coordenadas de tela em coordenadas locais do nó - Limite de raio:
if (distance > radius)garante que o joystick não saia do círculo de fundo - Normalização:
normalize()retorna um vetor unitário, pronto para o script de movimento multiplicar pela velocidade
Se você está fazendo um jogo de voo ou corrida infinita, em que a direção não precisa ser tão precisa, o toque em tela cheia pode funcionar melhor: o jogador desliza o dedo em qualquer ponto da tela para controlar o personagem. Mas essa lógica de entrada é diferente da lógica de um joystick virtual e precisa ser ajustada ao tipo de jogo.
Caso prático — implementação completa de controle para personagem de luta lateral
Agora vamos juntar a arquitetura de nós, a configuração de animação, a máquina de estados e o controle de entrada em uma implementação completa para um personagem de luta lateral.
Estrutura completa do código:
Player (nó raiz)
├── PlayerControl.ts (controle de movimento)
├── PlayerStateMachine.ts (máquina de estados)
└── KeyboardInput.ts (entrada por teclado)
Body (nó filho)
├── Sprite (imagem do personagem)
├── componente Animation
└ AttackEffect (nó opcional para efeito de ataque)
Exemplo completo — PlayerControl.ts:
import { _decorator, Component, Node, Vec2, CCFloat } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerControl')
export class PlayerControl extends Component {
@property(CCFloat)
speed: number = 200;
private moveDir: Vec2 = new Vec2(0, 0);
update(dt: number) {
if (this.moveDir.mag() > 0.5) {
const vx = this.moveDir.x * this.speed;
this.node.x += vx * dt;
}
}
setMoveDir(dir: Vec2) { this.moveDir = dir; }
}
Exemplo completo — PlayerStateMachine.ts:
import { _decorator, Component, Animation } from 'cc';
const { ccclass, property } = _decorator;
enum State {
Idle = 'idle',
Move = 'move',
Attack = 'attack'
}
@ccclass('PlayerStateMachine')
export class PlayerStateMachine extends Component {
@property(Animation)
animation: Animation = null;
private currentState: State = State.Idle;
private isMoving: boolean = false;
private attackKeyPressed: boolean = false;
onLoad() { this.animation.play('idle'); }
switchState(newState: State) {
if (this.currentState === newState) return;
this.animation.stop();
this.animation.play(newState);
this.currentState = newState;
}
update(dt: number) {
if (this.isMoving && this.currentState !== State.Move) {
this.switchState(State.Move);
} else if (!this.isMoving && this.currentState !== State.Idle) {
this.switchState(State.Idle);
}
if (this.attackKeyPressed && this.currentState !== State.Attack) {
this.switchState(State.Attack);
this.scheduleOnce(() => { this.switchState(State.Idle); }, 0.5);
}
}
setMoving(value: boolean) { this.isMoving = value; }
triggerAttack() {
this.attackKeyPressed = true;
this.scheduleOnce(() => { this.attackKeyPressed = false; }, 0.1);
}
}
Exemplo completo — KeyboardInput.ts:
import { _decorator, Component, input, Input, EventKeyboard, KeyCode, Vec2 } from 'cc';
import { PlayerControl } from './PlayerControl';
import { PlayerStateMachine } from './PlayerStateMachine';
const { ccclass, property } = _decorator;
@ccclass('KeyboardInput')
export class KeyboardInput extends Component {
@property(PlayerControl)
playerControl: PlayerControl = null;
@property(PlayerStateMachine)
stateMachine: PlayerStateMachine = null;
private moveDir: Vec2 = new Vec2(0, 0);
onLoad() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
}
onKeyDown(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_A:
case KeyCode.ARROW_LEFT:
this.moveDir.x = -1; break;
case KeyCode.KEY_D:
case KeyCode.ARROW_RIGHT:
this.moveDir.x = 1; break;
case KeyCode.KEY_J:
this.stateMachine.triggerAttack(); break;
}
}
onKeyUp(event: EventKeyboard) {
switch (event.keyCode) {
case KeyCode.KEY_A:
case KeyCode.ARROW_LEFT:
case KeyCode.KEY_D:
case KeyCode.ARROW_RIGHT:
this.moveDir.x = 0; break;
}
}
update(dt: number) {
this.playerControl.setMoveDir(this.moveDir);
this.stateMachine.setMoving(this.moveDir.mag() > 0.5);
}
}
Resultado ao rodar:
- Iniciar o jogo → a animação de idle do personagem toca
- Pressionar A/D ou as setas esquerda/direita → o personagem se move e troca para a animação de caminhada
- Pressionar J → o personagem ataca, toca a animação de ataque e volta para idle depois de 0,5 segundo
- Soltar a tecla direcional → o personagem para de se mover e volta para a animação de idle
Problemas comuns e soluções:
- Travamento na troca de animação: confira se a taxa de quadros do AnimationClip está baixa demais. Uma faixa de 12 a 24 fps costuma funcionar bem.
- Estado residual: se o personagem não volta para idle depois do ataque, verifique se o callback do timer está sendo executado corretamente.
- Conflito de entrada: se o personagem ainda se move durante o ataque, confira se a prioridade do estado de ataque foi colocada antes da lógica de movimento na máquina de estados.
Depois de integrar tudo, a lógica de controle fica clara: movimento no PlayerControl, animação na StateMachine e entrada no KeyboardInput. Os três scripts conversam por interfaces simples e não se atropelam.
Resumo
A arquitetura principal tem três camadas:
- Separação de nós: Player cuida do movimento, Body cuida da animação, sem disputa de responsabilidade
- Máquina de estados: três estados, idle, move e attack, com condições de transição concentradas
- Controle de entrada: teclado no PC, joystick virtual no celular, escolhido de acordo com o tipo de jogo
A vantagem dessa arquitetura é que uma mudança não derruba a outra. Aumentar a velocidade de movimento não exige mexer na animação; adicionar efeito ao ataque não exige tocar no script de movimento; trocar a entrada por joystick virtual não muda a lógica da máquina de estados.
Como próximo passo, você pode combinar esta implementação com a arquitetura em três camadas do quarto artigo da série, sobre design de máquina de estados para minigames, e adicionar um gerenciamento de estados mais complexo ao personagem: salto, dano, morte, uso de habilidades e assim por diante. A máquina de estados do personagem é apenas um submódulo da máquina de estados do jogo. A forma de expandir é a mesma: definir estados, configurar condições de transição e centralizar a lógica de troca.
Fluxo para implementar movimento e ataque de personagem no Cocos Creator
Da arquitetura de nós ao controle de entrada, implemente um controle completo para personagem de luta lateral
⏱️ Estimated time: 45 min
- 1
Step 1: Projetar a arquitetura de nós
Crie um nó raiz Player para receber o script de controle de movimento e um nó filho Body para receber o componente Animation e o Sprite. O Player controla apenas o aumento e a redução da coordenada x; o Body cuida dos saltos no eixo y e da troca de quadros de animação. - 2
Step 2: Configurar o componente de animação
Adicione um componente Animation ao nó Body e arraste os recursos AnimationClip de idle, caminhada, ataque e outros para ele. Defina defaultClip como a animação de idle e marque playOnLoad. No código, use animation.play('clipName') para alternar animações. - 3
Step 3: Implementar a máquina de estados
Defina o enum State, com Idle, Move e Attack, e implemente o método switchState. No update, use isMoving e attackKeyPressed para decidir as transições. No estado de ataque, use um timer scheduleOnce para voltar a idle depois de 0,5 segundo. - 4
Step 4: Conectar o controle de entrada
Crie o script KeyboardInput para escutar os eventos KEY_DOWN e KEY_UP. A/D e as setas esquerda/direita controlam a direção de movimento; a tecla J dispara o ataque. No update, sincronize moveDir a cada frame com PlayerControl e StateMachine. - 5
Step 5: Testar e ajustar
Rode o jogo e valide: a animação de idle toca corretamente, as teclas direcionais movem o personagem e alternam para a animação de caminhada, a tecla de ataque dispara a animação de ataque e volta automaticamente para idle, sem travamentos na troca.
FAQ
Por que separar Player e Body em dois nós?
Como resolver quando o personagem ataca e desliza ao mesmo tempo?
Depois da animação de ataque, o personagem continua no quadro de ataque e não volta para idle. O que verificar?
No PC e no celular, qual esquema de entrada devo escolher?
Como implementar limite e normalização em um joystick virtual?
Por que o estado de ataque usa um timer para voltar a idle em vez de esperar a tecla ser solta?
13 min de leitura · Publicado em: 21 mai 2026 · Atualizado em: 14 jul 2026
Desenvolvimento de mini games Cocos com IA
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Convenções de nomenclatura para recursos de UI em minigames: imagens transparentes, botões, ícones e personagens
Aprenda convenções de nomenclatura para recursos de UI em minigames: estados de botões, categorias de ícones e fórmulas para frames de animação de personagens, com 7 regras práticas validadas por equipes de jogos da Tencent.
Parte 9 de 21
Próximo
De onde vem a sensação de jogo: flash, vibração, texto, som e partículas
Guia prático de game feel: do framework de 19 características aos parâmetros concretos, com flash de 50-100ms, vibração de 0,08s a 40Hz, sincronização de texto e som em 12ms, além de exemplos em Cocos Creator e Unity
Parte 11 de 21



Comentários
Entre com GitHub para comentar