Cocos Mini-Spiel: Charakterbewegung und Angriff – von Nodes bis Animation

Ihr Charakter steht in der Bildschirmmitte, Sie drücken die Richtungstaste – nichts passiert. Im Code liegen Bewegungslogik, Animationswiedergabe und Zustandsprüfung alle in einer update-Funktion. Sie ändern die Animation und vergessen den Zustand, ändern den Zustand und vergessen die Geschwindigkeit – am Ende wissen Sie selbst nicht mehr, was der Charakter gerade tun soll.
Node-Trennung klingt abstrakt, bedeutet aber schlicht: Bewegung und Animation getrennt steuern – der Player-Node übernimmt horizontale Verschiebung, der Body-Node vertikale Sprünge und Angriffs-Animationen, beide laufen unabhängig voneinander. Dieser Artikel führt Sie von der Node-Architektur über Animations-Konfiguration und State-Machine bis zur Eingabesteuerung – mit vollständigem Code für einen Side-Scrolling-Kampfcharakter.
Charakter-Node-Architektur – horizontale Bewegung + vertikale Animation
Im offiziellen Cocos Creator 3.7-Tutorial gibt es ein leicht übersehenes Design: Player- und Body-Nodes sind getrennt. Viele (mich eingeschlossen) halten das zunächst für unnötig – ein Node für Bewegung, Animation und Kollisionserkennung, geht das nicht auch?
Das Problem: Animation und Bewegung haben unterschiedliche Rhythmen. Stellen Sie sich einen Sprung vor – horizontal gleichmäßig vorwärts, vertikal eine Parabel. In einem einzigen Node müsste update horizontalen Versatz und Sprunghöhe gleichzeitig berechnen – der Code wird schnell chaotisch. Mit Angriffs-Animation, Treffer-Effekten und Tod-Rotation/Skalierung gerät die Logik eines einzelnen Nodes aus dem Ruder.
Vorteil der Trennung: Bewegung ist Bewegung, Animation ist Animation. Der Player-Node verwaltet nur die x-Koordinate, der Body-Node nur y-Sprung und Animations-Frames – beide stören sich nicht gegenseitig.
Node-Struktur:
Player (Root-Node)
├── PlayerControl.ts (Bewegungslogik)
└── Body (Child-Node)
├── Sprite (Charakterbild)
└── Animation-Komponente
Code-Beispiel – Player-Node-Bewegung:
// PlayerControl.ts - Player-Node-Skript
@property(CCFloat)
speed: number = 200; // Bewegungsgeschwindigkeit
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; // nur horizontale Position
}
}
// Richtung von außen setzen
setMoveDir(dir: Vec2) {
this.moveDir = dir;
}
Code-Beispiel – Body-Node-Animation:
// BodyControl.ts - Body-Node-Skript
@property(Animation)
animation: Animation = null;
jump(height: number = 100) {
// Sprung: vertikale Verschiebung + jump-Animation
this.node.runAction(cc.jumpBy(1.0, 0, 0, height, 1));
this.animation.play('jump');
}
attack() {
this.animation.play('attack');
// nach Angriff automatisch zu idle
this.scheduleOnce(() => {
this.animation.play('idle');
}, 0.5);
}
Bewegungslogik liegt am Player-Node, Animationslogik am Body-Node. Sprunghöhe ändern, ohne Bewegungscode anzufassen; Geschwindigkeit anpassen, ohne im Animations-Skript zu suchen – deutlich einfacher zu warten.
Animations-Komponente konfigurieren – Animation + AnimationClip
Die Animation-Komponente ist in Cocos Creator eingebaut, doch einige Eigenschaften werden leicht übersehen: defaultClip, das clips-Array und playOnLoad.
Kern-Eigenschaften der Animation-Komponente:
defaultClip: Standard-Animation (meist idle)clips: Animations-Ressourcen (idle, walk, attack usw.)currentClip: aktuell laufende Animation (Laufzeit)
Konfiguration in fünf Schritten:
- Animation-Komponente am Body-Node hinzufügen
- AnimationClip-Ressourcen ins clips-Array ziehen
- defaultClip auf idle setzen
- playOnLoad aktivieren (Charakter startet im Stand)
- Im Code per
animation.play('clipName')wechseln
Code-Beispiel – Animations-Konfiguration:
// PlayerAnimation.ts - Animations-Steuerung
@property(Animation)
animation: Animation = null;
@property([AnimationClip])
clips: AnimationClip[] = []; // idle, walk, attack, jump
onLoad() {
this.animation.clips = this.clips;
this.animation.defaultClip = this.clips[0]; // Standard: idle
this.animation.play();
}
playIdle() { this.animation.play('idle'); }
playMove() { this.animation.play('move'); }
playAttack() {
this.animation.play('attack');
this.scheduleOnce(() => { this.playIdle(); }, 0.5);
}
Wichtiges Detail: Beim Animationswechsel zuerst stop(), dann play() – besonders bei einmaligen Clips wie attack. Sonst kann beim Angriff während des Gehens die walk-Animation mit attack konkurrieren – der Charakter wirkt, als würde er gleichzeitig schwingen und rutschen.
Korrekter Animationswechsel:
switchAnimation(name: string) {
if (this.animation.currentClip?.name !== name) {
this.animation.stop();
this.animation.play(name);
}
}
Als Nächstes die State-Machine – damit Animationswechsel regelbasiert statt als verstreute if (speed>0) play('move')-Prüfungen laufen.
Animations-State-Machine – idle → move → attack → idle
Kernidee: Der Charakter ist immer genau in einem Zustand (idle, move, attack), Übergänge folgen klaren Bedingungen – nicht willkürlich.
Anfangs wirkte mir das überdesignt – play('attack') reicht doch? Mit Sprung-, Verletzungs- und Tod-Zuständen landen die Prüfungen überall: im Bewegungs-, Animations- und Eingabe-Skript. Eine Änderung an einer Stelle, zwei andere vergessen – und das Verhalten bricht.
State-Machine-Ablauf:
[Entry] → [idle]
↓ speed>0
[move]
↓ attackKey
[attack] → [Exit] → [idle]
Drei Zustände: idle (Stand), move (Bewegung), attack (Angriff). Übergänge:
- idle → move: Geschwindigkeit > 0
- move → idle: Geschwindigkeit = 0
- beliebig → attack: Angriffstaste
- attack → idle: Angriffs-Animation beendet
Code-Beispiel – State-Machine:
// 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; // Doppelwechsel vermeiden
this.animation.stop();
this.animation.play(newState);
this.currentState = newState;
}
update(dt: number) {
// Bewegungszustand
if (this.isMoving && this.currentState !== State.Move) {
this.switchState(State.Move);
} else if (!this.isMoving && this.currentState !== State.Idle) {
this.switchState(State.Idle);
}
// Angriff (Vorrang vor Bewegung)
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);
}
Wichtige Details:
- Zustands-Priorität: Angriff vor Bewegung (keine Bewegung während Angriff)
- Doppelwechsel vermeiden:
if (currentState === newState) return - Timer-Callback: Nach Angriff automatisch zu idle, nicht auf Tastenloslassen warten
Die State-Machine bündelt Animationslogik an einer Stelle. Neue Zustände wie Sprung, Verletzung oder Tod: Enum erweitern, Übergänge ergänzen – deutlich skalierbarer als verstreute if-Abfragen.
Eingabesteuerung im Vergleich – Tastatur vs. Touch vs. Virtual Joystick
Drei Ansätze für unterschiedliche Szenarien – die falsche Wahl verschlechtert das Spielgefühl spürbar.
| Lösung | Einsatz | Vorteile | Nachteile | Komplexität |
|---|---|---|---|---|
| Tastatur | PC, Debug | Einfach, schnell | Nicht auf dem Handy | Niedrig |
| Touch | Vollbild-Spiele (Flug, Parkour) | Natürlich, kein UI-Overlay | Ungenau, Fehlberührungen | Mittel |
| Virtual Joystick | ARPG, Kampf | Präzise, 360° | Belegt Bildschirmfläche | Hoch |
Tastatur-Eingabe (PC-Standard)
Für PC-Spiele oder schnelles Debuggen ist die Tastatur die einfachste Lösung.
Code-Beispiel – Tastatur-Eingabe:
// 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);
}
Details:
- Tastenbelegung: A/D und Pfeiltasten parallel
- Angriffstaste: J (üblich in Kampfspielen)
- Zustandssync: moveDir und State-Machine pro Frame aktualisieren
Virtual Joystick (Mobile-Standard)
Für Handy-Side-Scroller und ARPG mit präziser Richtung ist der Virtual Joystick fast Pflicht.
Implementierungs-Punkte:
- Grenzenbegrenzung: Griff darf den Hintergrundkreis nicht verlassen
- Normalisierung: Einheitsvektor zurückgeben
- Touch-Ende: Griff kehrt zur Mitte zurück
Code-Beispiel – Virtual Joystick:
// 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; }
Details:
- Koordinaten:
inverseTransformPointfür lokale Node-Koordinaten - Radius:
if (distance > radius)hält den Griff im Kreis - Normalisierung:
normalize()liefert Einheitsvektor für Geschwindigkeit × Richtung
Für Flug- oder Parkour-Spiele ohne präzise Richtung reicht Vollbild-Touch – die Logik unterscheidet sich vom Joystick und muss zum Spieltyp passen.
Praxis – vollständige Side-Scrolling-Kampfcharakter-Steuerung
Node-Architektur, Animation, State-Machine und Eingabe zu einer lauffähigen Lösung zusammenführen.
Code-Struktur:
Player (Root-Node)
├── PlayerControl.ts (Bewegung)
├── PlayerStateMachine.ts (State-Machine)
└── KeyboardInput.ts (Tastatur)
Body (Child-Node)
├── Sprite (Charakterbild)
├── Animation-Komponente
└ AttackEffect (Angriffs-Effekt, optional)
Vollständiger Code – 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; }
}
Vollständiger Code – 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);
}
}
Vollständiger Code – 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);
}
}
Erwartetes Verhalten:
- Spielstart → idle-Animation
- A/D oder Pfeiltasten → Bewegung + walk-Animation
- J → Angriff + attack-Animation → nach 0,5 s idle
- Richtungstaste loslassen → Stopp + idle
Typische Probleme und Lösungen:
- Animations-Ruckeln: AnimationClip-Framerate prüfen (empfohlen 12–24 fps)
- Hängender Zustand: Nach Angriff kein idle – Timer-Callback prüfen
- Eingabekonflikt: Bewegung während Angriff – Angriffs-Priorität vor Bewegungsprüfung setzen
Nach der Integration: Bewegung in PlayerControl, Animation in StateMachine, Eingabe in KeyboardInput – drei Skripte, klare Schnittstellen, keine gegenseitige Störung.
Zusammenfassung
Drei Schichten bilden das Kern-Design:
- Node-Trennung: Player für Bewegung, Body für Animation
- State-Machine: idle, move, attack mit zentralen Übergangsbedingungen
- Eingabe: Tastatur am PC, Virtual Joystick auf Mobile – passend zum Spieltyp
Vorteil: Änderungen bleiben lokal. Geschwindigkeit anpassen, ohne Animationscode zu berühren; Angriffs-Effekt hinzufügen, ohne Bewegungs-Skript zu ändern; Joystick statt Tastatur – State-Machine bleibt gleich.
Als Nächstes können Sie Teil 4 der Serie „Mini-Spiel-State-Machine-Design“ nutzen und komplexere Zustände wie Sprung, Verletzung, Tod oder Skills ergänzen. Die Charakter-State-Machine ist ein Submodul der Spiel-State-Machine – das Prinzip bleibt: Zustände definieren, Übergänge festlegen, Wechsel zentral steuern.
Cocos Creator: Charakterbewegung und Angriff implementieren
Von der Node-Architektur bis zur Eingabesteuerung – vollständige Side-Scrolling-Kampfcharakter-Steuerung
⏱️ Estimated time: 45 min
- 1
Step 1: Node-Architektur entwerfen
Player-Root-Node mit Bewegungs-Skript erstellen, Body-Child-Node mit Animation-Komponente und Sprite. Player steuert nur x-Koordinate, Body übernimmt y-Sprung und Animations-Frames. - 2
Step 2: Animation-Komponente konfigurieren
Am Body-Node Animation-Komponente hinzufügen, AnimationClip-Ressourcen für idle, walk, attack einbinden. defaultClip auf idle setzen, playOnLoad aktivieren. Im Code per animation.play('clipName') wechseln. - 3
Step 3: State-Machine implementieren
State-Enum (Idle, Move, Attack) definieren, switchState-Methode implementieren. In update anhand von isMoving und attackKeyPressed Zustände wechseln; Angriff per scheduleOnce nach 0,5 s zurück zu idle. - 4
Step 4: Eingabesteuerung anbinden
KeyboardInput-Skript für KEY_DOWN und KEY_UP erstellen. A/D und Pfeiltasten für Richtung, J für Angriff. moveDir in update pro Frame an PlayerControl und StateMachine übergeben. - 5
Step 5: Testen und optimieren
Spiel starten und prüfen: idle-Animation läuft, Richtungstasten bewegen Charakter mit walk-Animation, Angriffstaste löst attack aus und kehrt nach 0,5 s zu idle zurück – flüssige Übergänge ohne Ruckeln.
FAQ
Warum Player und Body als zwei Nodes trennen?
Charakter schwingt das Schwert und rutscht gleichzeitig – wie beheben?
Nach dem Angriff bleibt der Charakter im attack-Frame, kehrt nicht zu idle zurück?
Welche Eingabe für PC und Mobile?
Wie Grenzenbegrenzung und Normalisierung beim Virtual Joystick?
Warum Timer statt Tastenloslassen für Rückkehr zu idle?
9 Min. Lesezeit · Veröffentlicht am: 21. Mai 2026 · Aktualisiert am: 14. Juli 2026
AI-gestützte Cocos Mini-Game-Entwicklung
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Mini-Spiel-UI: Benennungskonventionen für transparente Bilder, Buttons, Icons und Charakter-Sprites
Benennungskonventionen für Mini-Spiel-UI-Assets: Button-Zustände, Icon-Kategorien und Charakter-Animationsframes – sieben Goldregeln aus dem Tencent-Game-Team für übersichtliche Projekte und bis zu 80 % schnelleres Auffinden von Dateien.
Teil 9 von 21
Nächster
Woher kommt Game Feel bei Mini-Spielen: Blitzweiß, Vibration, Floating Text, Sound, Partikel
Game-Feel-Design in der Praxis: vom 19-Merkmale-Framework zu konkreten Parametern – Blitzweiß 50–100 ms, Vibration 0,08 s bei 40 Hz, Floating Text mit 12 ms Sound-Sync; vollständige Lösung mit Cocos Creator und Unity-Codebeispielen
Teil 11 von 21
Ähnliche Beiträge
Mini-Spiel-Produktexperiment: Günstiger Validierungspfad für Gameplay und Monetarisierung

Mini-Spiel-Produktexperiment: Günstiger Validierungspfad für Gameplay und Monetarisierung
Indie-Spieleentwicklung: Erst Gameplay validieren, dann Systeme ausbauen (MVP-Praxisleitfaden)


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen