Déplacement et attaque du personnage Cocos : des nœuds à l'animation

Votre personnage est au centre de l’écran ; vous appuyez sur une flèche directionnelle — il ne bouge pas. Vous ouvrez le code : déplacement, animations et états sont entassés dans un seul update. Vous modifiez l’animation et oubliez l’état, ou l’inverse, et vous ne savez plus ce que le personnage devrait faire.
La séparation des nœuds peut sembler abstraite : en pratique, déplacement et animation sont gérés à part — le nœud Player pour le déplacement horizontal, Body pour le saut vertical et les animations d’attaque, chacun indépendamment. Cet article part de l’architecture des nœuds, configure le composant Animation, implémente la machine à états, compare les schémas d’entrée, et fournit un contrôle complet pour un personnage de combat en vue latérale.
Architecture des nœuds — déplacement horizontal + animation verticale
Le tutoriel officiel Cocos Creator 3.7 propose une conception souvent négligée : séparer Player et Body. Beaucoup (moi y compris au début) trouvent cela inutile — un seul nœud pour déplacement, animation et collisions, non ?
Le problème vient du rythme différent. Imaginez un saut : horizontal uniforme, vertical en parabole. Avec un seul nœud, update calcule à la fois le déplacement horizontal et la hauteur — le code devient vite ingérable. Ajoutez attaque, effet de dégâts, rotation à la mort : la logique d’un seul nœud dérape.
L’intérêt de la séparation : déplacement d’un côté, animation de l’autre. Player ne modifie que x ; Body gère y, saut et frames d’animation — sans se marcher dessus.
Structure des nœuds :
Player(nœud racine)
├── PlayerControl.ts(logique de déplacement)
└── Body(nœud enfant)
├── Sprite(image du personnage)
└── composant Animation
Exemple — contrôle du déplacement sur Player :
// PlayerControl.ts - script du nœud Player
@property(CCFloat)
speed: number = 200; // vitesse de déplacement
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; // position horizontale uniquement
}
}
// appel externe pour définir la direction
setMoveDir(dir: Vec2) {
this.moveDir = dir;
}
Exemple — contrôle d’animation sur Body :
// BodyControl.ts - script du nœud Body
@property(Animation)
animation: Animation = null;
jump(height: number = 100) {
// animation de saut : déplacement vertical + frames jump
this.node.runAction(cc.jumpBy(1.0, 0, 0, height, 1));
this.animation.play('jump');
}
attack() {
this.animation.play('attack');
// retour automatique à idle après l'attaque
this.scheduleOnce(() => {
this.animation.play('idle');
}, 0.5);
}
Déplacement sur Player, animation sur Body : modifier la hauteur de saut sans toucher au déplacement, ou la vitesse sans fouiller les scripts d’animation — maintenance nettement plus simple.
Configuration du composant Animation — Animation + AnimationClip
Le composant Animation est intégré à Cocos Creator ; quelques propriétés comptent : defaultClip, le tableau clips, et playOnLoad souvent oublié.
Propriétés clés du composant Animation :
defaultClip: animation par défaut (souvent idle)clips: tableau de ressources (idle, marche, attaque, etc.)currentClip: clip en cours (état runtime)
Étapes de configuration :
- Ajoutez Animation sur le nœud Body
- Glissez les AnimationClip dans le tableau clips
- Définissez defaultClip sur l’animation idle
- Cochez playOnLoad (le personnage démarre en idle)
Exemple — configuration du composant Animation :
// PlayerAnimation.ts - script de contrôle d'animation
@property(Animation)
animation: Animation = null;
@property([AnimationClip])
clips: AnimationClip[] = []; // idle, marche, attaque, saut
onLoad() {
this.animation.clips = this.clips;
this.animation.defaultClip = this.clips[0]; // idle par défaut
this.animation.play();
}
playIdle() { this.animation.play('idle'); }
playMove() { this.animation.play('move'); }
playAttack() {
this.animation.play('attack');
this.scheduleOnce(() => { this.playIdle(); }, 0.5);
}
Détail utile : avant un nouveau play(), appelez souvent stop(), surtout pour les clips « one-shot » comme l’attaque. Sinon, en marchant puis en appuyant sur attaque, les animations se chevauchent — le personnage semble frapper en glissant.
Bonne pratique de commutation :
switchAnimation(name: string) {
if (this.animation.currentClip?.name !== name) {
this.animation.stop();
this.animation.play(name);
}
}
Une fois le composant configuré, place pour la machine à états — des transitions régulières plutôt que des if (vitesse>0) play('move') éparpillés.
Machine à états — idle → move → attack → retour idle
Principe : le personnage n’est qu’à un état à la fois (idle, move, attack), avec des conditions de transition explicites — pas de changements au hasard.
Au début, la machine à états semblait excessive — play('attack') suffit, non ? Puis viennent saut, blessure, mort : les tests sont partout (déplacement, animation, entrées). On modifie un endroit, on oublie les autres — bugs de comportement.
Diagramme de flux :
[Entry] → [idle]
↓ speed>0
[move]
↓ attackKey
[attack] → [Exit] → [idle]
Trois états : idle, move, attack. Transitions :
- idle → move : vitesse > 0
- move → idle : vitesse = 0
- any → attack : touche d’attaque
- attack → idle : fin de l’animation d’attaque
Exemple — implémentation de la machine à états :
// 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; // éviter les doublons
this.animation.stop();
this.animation.play(newState);
this.currentState = newState;
}
update(dt: number) {
// état de déplacement
if (this.isMoving && this.currentState !== State.Move) {
this.switchState(State.Move);
} else if (!this.isMoving && this.currentState !== State.Idle) {
this.switchState(State.Idle);
}
// état d'attaque (priorité sur le déplacement)
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);
}
Points clés :
- Priorité : attaque avant déplacement (pas de déplacement pendant l’attaque)
- Anti-doublon :
if (currentState === newState) return - Timer : retour à idle après l’attaque, pas à la relâche de touche
La logique de commutation est centralisée ; ajouter saut, blessure ou mort = nouvelles valeurs dans State et nouvelles transitions — bien plus extensible que des if dispersés.
Comparaison des entrées — clavier vs tactile vs joystick virtuel
Chaque schéma a son usage ; un mauvais choix dégrade la prise en main.
| Schéma | Usage | Avantages | Inconvénients | Complexité |
|---|---|---|---|---|
| Clavier | PC, debug | Simple, réactif | Indisponible sur mobile | Faible |
| Tactile | Vol, runner plein écran | Naturel, sans UI | Direction imprécise, faux touchers | Moyenne |
| Joystick virtuel | ARPG, combat | Direction précise, 360° | Occupe l’écran | Élevée |
Clavier (PC en priorité)
Pour un mini-jeu surtout sur PC, ou pour valider la logique en debug, le clavier est le plus simple.
Exemple — entrée clavier :
// 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);
}
Détails clavier :
- Mapping : A/D et flèches gauche/droite
- Attaque : touche J (courante en combat)
- Sync : direction et machine à états mis à jour chaque frame
Joystick virtuel (mobile en priorité)
Sur mobile, pour combat latéral ou ARPG avec direction précise, le joystick virtuel est quasi indispensable.
Points d’implémentation :
- Limite de zone : le curseur ne dépasse pas le cercle de fond
- Normalisation : vecteur unitaire en sortie
- Relâchement : retour au centre
Exemple — joystick virtuel :
// 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; }
Détails joystick :
- Conversion :
inverseTransformPointpour les coordonnées locales - Rayon :
if (distance > radius)garde le curseur dans le cercle - Normalisation :
normalize()pour multiplier par la vitesse
Pour vol ou runner sans direction fine, le tactile plein écran convient — logique différente du joystick, à adapter au jeu.
Cas pratique — contrôle complet d’un personnage de combat latéral
Intégration de l’architecture, de l’animation, de la machine à états et des entrées.
Structure du code :
Player(nœud racine)
├── PlayerControl.ts(déplacement)
├── PlayerStateMachine.ts(machine à états)
└── KeyboardInput.ts(clavier)
Body(nœud enfant)
├── Sprite(image)
├── composant Animation
└ AttackEffect(effet d'attaque, optionnel)
Exemple complet — 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; }
}
Exemple complet — 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);
}
}
Exemple complet — 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);
}
}
Comportement attendu :
- Démarrage → animation idle
- A/D ou flèches → déplacement + marche
- J → attaque → retour idle après 0,5 s
- Relâchement des directions → arrêt + idle
Problèmes courants :
- Saccades : vérifier le framerate des AnimationClip (12–24 fps recommandé)
- État résiduel : pas de retour à idle après attaque → vérifier le callback du timer
- Conflit d’entrée : déplacement pendant l’attaque → prioriser attack avant move dans update
Logique claire : déplacement dans PlayerControl, animation dans StateMachine, entrées dans KeyboardInput — communication par interfaces.
Résumé
Trois couches :
- Nœuds séparés : Player pour le déplacement, Body pour l’animation
- Machine à états : idle, move, attack avec transitions centralisées
- Entrées : clavier sur PC, joystick virtuel sur mobile selon le jeu
Avantage : modifier une couche n’impacte pas les autres. Vitesse sans toucher aux animations ; effets d’attaque sans toucher au déplacement ; joystick sans refondre la machine à états.
Ensuite, combinez avec l’article 4 de la série sur la machine à états à trois niveaux pour ajouter saut, blessure, mort, compétences — le personnage n’est qu’un sous-module, même logique : états, transitions, gestion centralisée.
Flux d'implémentation du déplacement et de l'attaque dans Cocos Creator
De la conception de l'architecture des nœuds au contrôle des entrées, implémentation complète d'un personnage de combat en vue latérale
⏱️ Estimated time: 45 min
- 1
Step 1: Concevoir l'architecture des nœuds
Créez le nœud racine Player avec le script de contrôle du déplacement, et le nœud enfant Body avec le composant Animation et le Sprite. Player ne modifie que la coordonnée x ; Body gère le saut sur l'axe y et le changement de frames d'animation. - 2
Step 2: Configurer le composant Animation
Ajoutez le composant Animation sur Body, assignez les AnimationClip (idle, marche, attaque, etc.). Définissez defaultClip sur l'animation idle, cochez playOnLoad. Dans le code, basculez avec animation.play('clipName'). - 3
Step 3: Implémenter la machine à états
Définissez l'énumération State (Idle, Move, Attack) et la méthode switchState. Dans update, basculez selon isMoving et attackKeyPressed ; utilisez scheduleOnce pour revenir à idle 0,5 s après l'attaque. - 4
Step 4: Brancher les entrées
Créez le script KeyboardInput pour écouter KEY_DOWN et KEY_UP. A/D et flèches gauche/droite pour la direction, J pour l'attaque. Dans update, synchronisez moveDir vers PlayerControl et StateMachine à chaque frame. - 5
Step 5: Tester et ajuster
Lancez le jeu : animation idle OK, touches directionnelles déplacent le personnage et activent la marche, J déclenche l'attaque puis retour à idle après 0,5 s, transitions fluides sans saccades.
FAQ
Pourquoi séparer Player et Body en deux nœuds ?
Le personnage glisse en frappant : comment corriger ?
Après l'attaque, la frame d'attaque reste affichée sans retour à idle ?
Quel schéma d'entrée pour PC et mobile ?
Comment limiter la zone du joystick et normaliser la direction ?
Pourquoi un timer pour revenir à idle après l'attaque, plutôt que la relâche de touche ?
9 min de lecture · Publié le: 21 mai 2026 · Mis à jour le: 27 juil. 2026
Développement de mini-jeux Cocos assisté par IA
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Conventions de nommage des ressources UI pour mini-jeux : PNG transparents, boutons, icônes, sprites
Maîtrisez les conventions de nommage des ressources UI pour mini-jeux : états des boutons, catégories d'icônes, formule complète pour les frames d'animation — 7 règles d'or de l'équipe Tencent Games pour des fichiers lisibles et +80 % d'efficacité en collaboration.
Partie 9 sur 21
Suivant
D'où vient le game feel : flash blanc, vibration, texte flottant, son et particules
Guide pratique du game feel : du cadre des 19 caractéristiques aux paramètres concrets — flash blanc 50-100 ms, vibration 0,08 s à 40 Hz, synchronisation son/texte à 12 ms — avec exemples de code Cocos Creator et Unity.
Partie 11 sur 21



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire