Keep your place in this quest

Log in or sign up for free to subscribe, follow lesson progress, and access more learning content.

Comprenons maintenant comment fonctionnent les personnages et les animations dans Cave. Un personnage n'est pas simplement un modèle 3D qui se déplace dans le niveau. Dans un jeu réel, il combine généralement la physique, la logique de déplacement, un maillage visible, un armature, des animations et du code ou des Logic Bricks qui déterminent quelle animation doit être jouée.

Pour rendre cela concret, nous utiliserons comme référence principale le modèle par défaut Player des projets de démarrage de Cave. Si vous créez un Third Person Game ou un Top Down Game, Cave génère un joueur qui se déplace déjà, possède un personnage animé visible et joue des animations de locomotion basiques.

image.png

Cette leçon vous montrera comment cette configuration est organisée et comment vous pouvez contrôler les animations depuis Python.

Vous apprendrez :

  • Comment le modèle de départ Player est construit.
  • Pourquoi le maillage visuel est une entité enfant et non directement sur l'entité racine du joueur.
  • Ce que fait le Composant Animation.
  • Comment accéder à l'animateur depuis Python (une logique similaire s'applique aux Logic Bricks).
  • Comment jouer des animations par leur nom.
  • Comment fonctionnent le mélange, les couches et les filtres d'os.
  • Comment les callbacks d'animation et les sockets s'intègrent au système.

La Structure du Modèle Player

Dans le projet de démarrage, le Player est un Entity Template. Cela signifie que la configuration du joueur est réutilisable et peut être placée dans plusieurs scènes.

La structure est globalement la suivante :

image.png

Les enfants exacts peuvent varier selon les options du projet que vous avez sélectionnées, mais l'idée importante est que l'entité racine Player possède la configuration gameplay, tandis que l'entité enfant Mesh possède le personnage animé visible.

L'ennemi de départ utilise une idée très similaire. L'entité racine de l'ennemi a la physique et le comportement du personnage, et son enfant Mesh contient le maillage visible et le Composant Animation.

L'Entité Racine Player

L'entité racine Player représente le personnage de gameplay. Elle contient habituellement :

  • Un Transform Component.
  • Un Character Component.
  • Des Composants Python pour le déplacement du joueur, l'UI, les aides à l'animation et d'autres logiques gameplay.
  • Des propriétés telles que la santé ou des paramètres de comportement optionnels.
  • Des enfants utilisés pour la caméra, l'UI, le maillage visuel et objets auxiliaires.

Le Character Component est particulièrement important car il gère la physique du personnage : marcher, sauter, collisions, pentes et déplacements dans le monde. Cela signifie que la racine Player est principalement responsable du gameplay et de la physique.

L'Entité Enfant Mesh

Dans le modèle du joueur, il y a une entité enfant appelée Mesh. Cette entité contient généralement :

  • Un Transform Component.
  • Un ou plusieurs Mesh Components.
  • Un Animation Component.

Le Composant Mesh fournit au personnage son modèle 3D visible et son matériau. Si le personnage utilise plusieurs matériaux, Cave peut représenter cela avec plusieurs Composants Mesh, suivant la même règle multimaterial expliquée dans la leçon d'importation.

Le Composant Animation est ce qui évalue l'armature et joue les animations.

Donc, simplement :

Entité Responsabilité Principale
Player Physique, déplacement, logique gameplay, propriétés.
Player -> Mesh Modèle visible, matériau, animation d'armature.

Cette séparation est volontaire.

Pourquoi le Mesh Est Une Entité Enfant

Vous vous demandez peut-être pourquoi le mesh n'est pas directement placé sur l'entité racine Player.

La raison est que la physique du personnage et le personnage visible animé ont souvent besoin de transformées différentes.

La transformée racine du Player représente le corps de gameplay. C'est ce qui se déplace dans le monde, entre en collision avec les murs et porte le Character Component. La transformée de l'enfant Mesh représente l'apparence visuelle du personnage.

Cette séparation est utile dans de nombreuses situations :

  • Le mesh peut nécessiter une échelle différente de la capsule physique.
  • Le mesh peut devoir avoir un petit décalage pour s'aligner avec la collision du personnage.
  • Le mesh peut devoir tourner indépendamment du corps de mouvement.
  • Le mesh peut devoir faire face à la direction du mouvement pendant que la racine conserve une orientation de gameplay.
  • Le mesh peut devoir rester orienté vers l'avant pendant que le joueur se déplace latéralement.

Par exemple, imaginez que le joueur se déplace vers la gauche. Si vous avez une animation « marche à gauche » appropriée, vous pouvez vouloir que le mesh du personnage reste face à l'avant pendant que l'animation gère le déplacement latéral. Mais si vous n'avez pas d'animation latérale, vous pouvez vouloir que le mesh tourne vers la gauche pour que le personnage marche visuellement dans cette direction.

Les deux cas sont valides.

Ils nécessitent des transformées séparées :

Transformée Contrôle
Transformée racine Player Position physique, corps de gameplay, déplacement dans le monde.
Transformée enfant Mesh Orientation visuelle, échelle, décalages, présentation d'animation.

C'est pourquoi le joueur et l'ennemi de départ gardent le mesh animé comme entité enfant.

Ce Que Fait le Composant Animation

Le Animation Component est le composant principal utilisé pour jouer des animations squelettiques sur une entité.

Il nécessite :

  • Une Armature.
  • Une Animation par défaut.
  • Un Composant Mesh valide sur la même entité (ou plusieurs).

L'armature définit le squelette. L'animation définit comment ce squelette bouge dans le temps. Le Composant Animation évalue la pose animée finale et l'applique au personnage visible.

Dans le joueur de démarrage par défaut, l'enfant Mesh utilise :

  • Proto Mesh
  • Proto Mat
  • Proto Armature
  • p-idle comme animation par défaut (animation d'attente)

Ces assets de départ sont là pour que vous puissiez immédiatement inspecter un personnage animé fonctionnel.

Armatures, Animations, et Retargeting

  • Une Armature est le squelette du personnage. Elle contient les os que le maillage suit.
  • Une Animation stocke un mouvement au cours du temps pour les os dans une armature.

Le Composant Animation relie ces éléments :

Élément Signification
Mesh Le modèle de personnage visible.
Armature Le squelette à l'intérieur du personnage.
Animation Le mouvement, tel que idle, marche, course ou chute.
Animation Component Le composant qui joue l'animation sur l'entité.

Cave peut aussi effectuer du retargeting d'animations lorsque c'est possible. Si l'animation jouée appartient à une armature différente compatible, le Composant Animation peut utiliser le retargeting pour appliquer ce mouvement à l'armature actuelle.

Le retargeting est utile quand vous voulez réutiliser des animations entre personnages compatibles, mais cela dépendra encore de la qualité et de la compatibilité des rigs importés. Si une animation semble tordue, décalée ou étrange, vérifiez le rig source, l'armature importée et la configuration du retargeting.


Accéder à l'Animateur depuis Python

Voyons maintenant comment animer les personnages via la logique.

Dans les projets Cave, il est courant de garder la logique gameplay du joueur sur l'entité racine Player, puis d'accéder à l'enfant Mesh depuis ce code. Les scripts de démarrage par défaut suivent cette idée.

Voici le modèle de base :

import cave

class PlayerAnimationExample(cave.Component):
    def start(self, scene: cave.Scene):
        self.mesh : cave.Entity = self.entity.getChild("Mesh")
        self.animator : cave.AnimationComponent = self.mesh.get("Animation")

    def update(self):
        # Exemple : jouer une animation idle.
        self.animator.playByName("p-idle", blend=0.2, loop=True)

Dans cet exemple :

  • self.entity est l'entité racine Player.
  • getChild("Mesh") trouve l'entité enfant visuelle.
  • self.mesh.get("Animation") récupère le Composant Animation.
  • La variable s'appelle animator, c'est la pratique courante dans les scripts Cave.
  • playByName(...) joue une animation par son nom.

C'est la connexion de base dont vous avez besoin avant de contrôler les animations via le code.

Jouer des Animations par leur Nom

La méthode la plus courante que vous utiliserez est playByName.

Son usage de base est le suivant :

self.animator.playByName("p-walk", blend=0.2, loop=True)

Les paramètres importants sont :

Paramètre Signification
anim Le nom de l'animation à jouer.
blend La durée, en secondes, pendant laquelle Cave doit effectuer le mélange vers la nouvelle animation.
loop Si l'animation doit être bouclée.
layer Sur quelle couche d'animation jouer l'animation.

Par exemple :

self.animator.playByName("p-idle", blend=0.2, loop=True)
self.animator.playByName("p-walk", blend=0.2, loop=True)
self.animator.playByName("p-run", blend=0.2, loop=True)
self.animator.playByName("p-fall", blend=0.2, loop=True)

Ce sont les mêmes types d'appels que ceux utilisés par les scripts par défaut du joueur et de l'ennemi.

Un Exemple Simple de Locomotion

Une configuration d'animation joueur courante est :

  • Jouer idle quand le personnage est immobile.
  • Jouer marche quand le personnage se déplace.
  • Jouer course quand le personnage se déplace en courant.
  • Jouer chute quand le personnage n'est pas au sol.

Voici un exemple simplifié :

import cave

class SimplePlayerAnimator(cave.Component):
    def start(self, scene: cave.Scene):
        self.character : cave.CharacterComponent = self.entity.get("Character")

        self.mesh : cave.Entity = self.entity.getChild("Mesh")
        self.meshTransform : cave.TransformComponent = self.mesh.getTransform() if self.mesh else None
        self.animator      : cave.AnimationComponent = self.mesh.get("Animation") if self.mesh else None

    def update(self):
        if self.animator is None or self.character is None:
            return

        direction = self.character.getWalkDirection()
        isMoving = direction.length() > 0
        isRunning = False # Remplacez ceci par votre propre entrée ou condition de gameplay.

if self.character.onGround():
            if isMoving:
                if isRunning:
                    self.animator.playByName("p-run", blend=0.2, loop=True)
                else:
                    self.animator.playByName("p-walk", blend=0.2, loop=True)
            else:
                self.animator.playByName("p-idle", blend=0.2, loop=True)
        else:
            self.animator.playByName("p-fall", blend=0.2, loop=True)

Cet exemple est volontairement simple. Le contrôleur de départ réel gère également l'entrée, la direction du mouvement, le saut, un comportement optionnel de point-and-click, et la rotation du maillage, mais l'idée de l'animation est la même.

Rotation du maillage vers le mouvement

Parce que l'entité visuelle Mesh a sa propre transformation, vous pouvez la faire tourner séparément du Player racine.

Le joueur de départ le fait lorsque le personnage bouge :

if direction.length() > 0 and self.meshTransform:
    self.meshTransform.lookAtSmooth(
        self.entity.getTransform().transformDirection(-direction),
        6.0 * cave.getDeltaTime()
    )

L'idée importante n'est pas les mathématiques exactes. L'essentiel, c'est que le corps du gameplay et le maillage visuel peuvent être contrôlés séparément.

Cela vous permet de choisir comment le personnage doit se tourner :

  • Faire face à la direction du mouvement.
  • Garder la face vers l'avant en déplaçant latéralement.
  • Tourner en douceur vers la direction désirée.
  • Utiliser différentes règles d'orientation visuelle pour différents modes de jeu.

C'est une des principales raisons pour lesquelles le personnage de départ a une entité enfant Mesh.

Fusion des animations

La fusion des animations rend les transitions plus fluides.

Sans fusion, passer d'une animation à une autre peut se faire instantanément. Avec la fusion, Cave peut effectuer une transition douce entre les animations sur la même couche.

Par exemple :

# De ceci :
self.animator.playByName("p-idle", blend=0.2, loop=True)

# À cela :
self.animator.playByName("p-walk", blend=0.2, loop=True)

Ici, blend=0.2 signifie que Cave fusionnera la nouvelle animation pendant 0.2 secondes.

C'est particulièrement important pour les personnages. Un joueur peut facilement remarquer des changements d'animation brutaux, même dans un prototype.

Les bonnes valeurs de fusion pour débutants sont généralement petites :

  • 0.1 pour des transitions très rapides.
  • 0.2 pour des transitions normales de locomotion.
  • 0.4 ou plus pour des transitions plus lentes et lourdes.

Vous devriez régler cela à l'impression.

Couches d'animation

Cave supporte plusieurs couches d'animation. Chaque couche peut jouer sa propre animation, et les couches supérieures peuvent se superposer aux couches inférieures. Vous pouvez exécuter des animations dans n'importe quelle couche.

L'animation de locomotion par défaut tourne généralement sur la couche 0.

Par exemple :

self.animator.playByName("p-walk", blend=0.2, layer=0, loop=True)

Si vous avez une animation d'action pour le haut du corps, comme une attaque, une visée ou un rechargement, vous pouvez la jouer sur une autre couche :

self.animator.playByName("Attack", blend=0.1, layer=1, loop=False)

L'idée est :

Couche Usage courant
Couche 0 Locomotion corps complet, comme idle, marche, course, saut, chute.
Couche 1 Actions du haut du corps, telles que attaque, visée, rechargement, tenir une arme.

Cela permet au personnage de continuer à marcher pendant qu'une autre couche ajoute une action par-dessus.

Poids des couches

Chaque couche a un poids, ce qui contrôle son influence.

Vous pouvez le modifier depuis Python :

self.animator.setLayerWeight(1, 1.0)

Vous pouvez aussi le lire :

weight = self.animator.getLayerWeight(1)

Un poids de 1.0 signifie que la couche est entièrement active. Un poids de 0.0 signifie qu'elle n'a aucune influence visible.

Les poids des couches sont utiles quand vous voulez faire apparaître ou disparaître progressivement une couche d'action, comme lever lentement une arme, viser, ou fusionner vers une posture spéciale.

Il faut mentionner que le paramètre blend fusionne uniquement des animations sur la même couche. Et nous ne fusionnons pas entre les animations jouées sur différentes couches. Donc si vous avez une animation jouée sur la couche 0, puis soudain vous jouez une animation sur la couche 1, quel que soit la valeur de blend, il n'y aura pas de fusion entre elles. La fusion ne se fait que s'il y a déjà une animation jouée sur la couche 1. Si vous voulez faire une fusion entre couches, il faut créer une logique personnalisée utilisant le poids des couches pour ça.

Filtres d'os

Les filtres d'os contrôlent quels os sont affectés par une couche et à quel degré chaque os est influencé.

C'est ce qui rend possible l'animation du haut du corps séparément.

Par exemple, vous pouvez vouloir :

  • Que la couche 0 contrôle tout le corps.
  • Que la couche 1 contrôle seulement la colonne vertébrale, les bras, les mains, et les os de l'arme.

En Python, vous pouvez créer un filtre pour une couche et assigner une influence à un os et à ses enfants :

def setupUpperBodyLayer(self):
    armature = self.animator.armature.get()
    spine = armature.getBone("mixamorig:Spine")

    upperBody = self.animator.createLayerFilter(1)
    upperBody.defaultBlend = 0.0
    upperBody.setToBone(spine, 1.0, recursive=True)

Dans cet exemple :

  • defaultBlend = 0.0 signifie que la couche n'affecte aucun os par défaut.
  • setToBone(spine, 1.0, recursive=True) signifie que la colonne vertébrale et ses enfants sont pleinement affectés.
  • La couche 1 peut maintenant être utilisée pour les animations du haut du corps.

Les noms des os dépendent de l'armature que vous avez importée. Inspectez toujours votre armature et utilisez les noms d'os corrects pour votre personnage.

Exemple pratique de couche

Imaginez un personnage en vue tierce pouvant marcher et attaquer en même temps.

Une configuration possible est :

Couche Ce qu'elle gère
Couche 0 Idle, marche, course, chute.
Couche 1 Attaque du haut du corps.

Le code pourrait ressembler à ceci :

import cave

class CombatAnimator(cave.Component):
    def start(self, scene: cave.Scene):
        self.mesh = self.entity.getChild("Mesh")
        self.animator : cave.AnimationComponent = self.mesh.get("Animation") if self.mesh else None

        if self.animator:
            armature = self.animator.armature.get()
            spine = armature.getBone("mixamorig:Spine")

            upperBody = self.animator.createLayerFilter(1)
            upperBody.defaultBlend = 0.0
            upperBody.setToBone(spine, 1.0, recursive=True)
            self.animator.setLayerWeight(1, 1.0)

    def playWalk(self):
        self.animator.playByName("p-walk", blend=0.2, layer=0, loop=True)

    def playAttack(self):
        self.animator.playByName("Attack", blend=0.1, layer=1, loop=False)

Cet exemple suppose que vous avez un asset d'animation nommé Attack. Si votre projet utilise un autre nom d'animation, utilisez ce nom à la place.

L'idée principale est que la locomotion reste sur la couche 0, tandis que l'attaque joue sur la couche 1 et n'affecte que les os filtrés.

Callbacks d'animation

Les animations peuvent exécuter du code Python à des moments spécifiques.

Cave supporte des callbacks tels que :

  • Au début de l'animation.
  • À la fin de l'animation.
  • Sur une image spécifique de l'animation.

Les callbacks sont utiles lorsque l'animation doit déclencher du gameplay ou des effets.

Exemples :

  • Jouer un son de pas.
  • Créer de la poussière lors d'un pas.
  • Appliquer des dégâts à une image d'attaque.
  • Démarrer une traînée d'arme.
  • Déclencher un effet de particules.
  • Notifier la logique que l'animation est terminée.

Les animations connaissent souvent le timing exact mieux que le code. Si une épée doit infliger des dégâts uniquement lorsque le coup atteint la zone cible, un callback d'animation peut placer ce moment de gameplay sur la bonne image.

Exemple de callback de pas

Les projets de démarrage peuvent inclure des callbacks de pas dans les animations de marche et de course. Les inspecter est un bon moyen de voir un callback d'animation en action.

L'idée est simple :

  1. L'animation sait quand le pied touche le sol.
  2. Le callback est placé sur cette image.
  3. Le callback joue un son ou crée un petit effet.

Dans un callback d'animation, Cave fournit des variables utiles telles que :

  • entity, l'entité propriétaire.
  • animator, le Component Animation.
  • handle, la couche/gestionnaire d'animation en cours.

Un callback très simple de pas pourrait ressembler à ceci :

cave.playSound("Footstep Grass", volume=0.5)

Vous pouvez étendre cette idée pour choisir différents sons selon le matériau du sol, la vitesse du personnage ou l'état actuel.

Callbacks d'animation réutilisables

Les callbacks appartiennent à l'asset d'animation.

Cela signifie que si plusieurs entités jouent la même animation, le callback peut s'exécuter pour chaque entité qui l'utilise.

Cela rend les callbacks réutilisables. Par exemple, la même animation de marche peut déclencher des sons de pas pour chaque personnage qui la joue, tant que le code du callback utilise correctement l'entité propriétaire.

L'animation stocke le timing. L'entité fournit le contexte.

Callbacks de pose avancés

Cave supporte également les callbacks post-évaluation via Python. Ceux-ci ont lieu après que le Component Animation a évalué la pose de l'armature.

C'est une fonctionnalité avancée, mais elle peut être utilisée pour :

  • Cinématique inverse (Inverse kinematics).
  • Orientation de la tête.
  • Visée d'arme.
  • Placement des pieds.
  • Ajustements finaux de la pose.

Le Player Toolkit de départ inclut un exemple de Foot IK qui utilise ce concept. Il récupère l'enfant Mesh, récupère le component Animation, puis enregistre une méthode avec :

self.animator.addPostEvaluationCallback(self.postEvaluation)

Vous n'avez pas besoin d'écrire des systèmes d'IK immédiatement, mais il est utile de savoir que Cave permet à Python d'ajuster la pose finale après la lecture normale des animations.

Component Animation Socket

Le Animation Socket Component permet à une entité enfant de suivre un os d'une entité parent animée.

L'entité enfant doit être sous un parent qui possède un Component Animation. Ensuite, le socket peut choisir un os et copier sa position, rotation, et optionnellement son échelle, avec des offsets si besoin.

Les sockets sont utiles pour attacher des objets à des personnages animés :

  • Épée dans une main.
  • Arme à feu dans une main.
  • Bouclier sur un bras.
  • Casque sur la tête.
  • Sac à dos sur un os de la colonne vertébrale.
  • Accessoire attaché à une main.

Par exemple, si un personnage tient une épée, l’épée peut être une entité enfant avec un Animation Socket Component qui suit l’os de la main. Lorsque le personnage attaque, court ou est en position d’attente, l’épée reste attachée à la position animée correcte.

Ce que vous devez retenir

  • Le joueur de départ est un template avec une entité racine Player et une entité enfant Mesh.
  • La racine Player gère habituellement la physique, le déplacement, les propriétés et la logique de jeu.
  • L’entité enfant Mesh gère généralement le modèle visible, le matériau, l’armature et le Animation Component.
  • Cette séparation permet d’avoir des transformées séparées pour le corps physique et le personnage visuel.
  • Le Animation Component joue des animations squelettiques en utilisant une armature et des assets d’animation.
  • En Python, il est courant d’appeler le Animation Component animator.
  • playByName joue une animation en fonction du nom de l’asset et supporte le blending, la boucle et les couches.
  • Les couches permettent de superposer plusieurs animations.
  • Les filtres d’os permettent à une couche d’affecter seulement une partie du squelette.
  • Les callbacks d’animation lient le timing des animations aux événements de jeu.
  • Les Animation sockets attachent des accessoires aux os animés.

Quand vous inspectez le template Player ou Enemy de départ, utilisez cette structure comme carte : entité racine pour la logique de jeu, entité enfant Mesh pour le visuel et l’animation.