(Feat) Corps 3e personne anime

Le pawn n'avait aucun mesh 3e personne : en coop, les autres joueurs
voyaient un pseudo flotter au-dessus du vide. C'est aussi le prerequis de
la customisation de personnage, qu'on ne peut pas empiler sur un corps
inexistant.

UPlayerBodyAnimInstance publie l'etat du pawn pour ABP_PlayerBody_A, sur
le meme partage des roles que UFpsArmsAnimInstance : le C++ lit et
republie, le Blueprint choisit l'animation. Rien n'est replique -- tout
derive d'un etat que le moteur ou le projet synchronisent deja (Velocity,
bIsCrouched, bIsSprinting, RemoteViewPitch).

VerticalSpeed distingue le SAUT de la CHUTE : bIsFalling vaut vrai dans
les deux cas, le moteur appelant "falling" tout ce qui n'est pas au sol.
Sans son signe, sortir d'une falaise jouerait l'animation de detente.

Le Cast du pawn est relance dans NativeUpdateAnimation : chez un client
le mesh arrive par replication et peut s'animer avant que le pawn soit
la, ce qui figerait le corps de l'ami en pose de reference pour toute la
partie -- invisible en solo comme chez l'hote.

Dev_Scene rejoint MapsToCook : GameLevel peut pointer sur elle le temps
d'un test, et une reference soft n'est pas suivie par le cooker.

Limites connues : pas d'animation accroupie (absente du pack) et le
BlendSpace s'arrete a 500 alors que le sprint est a 650.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-07 13:53:54 +02:00
parent c69e39e3c1
commit 0ffa79efc3
23 changed files with 308 additions and 5 deletions
@@ -0,0 +1,93 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "PlayerBodyAnimInstance.h"
#include "FpsPlayer.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "KismetAnimationLibrary.h"
void UPlayerBodyAnimInstance::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
}
void UPlayerBodyAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
// Chez un CLIENT, le mesh arrive par replication et peut commencer a
// s'animer avant que le pawn soit completement initialise : le Cast du
// NativeInitialize rend alors nullptr, une fois pour toutes, et le corps de
// l'ami reste fige en pose de reference pour le reste de la partie. On
// reessaie donc tant qu'on n'a rien -- le cout est un Cast par frame
// uniquement pendant les quelques frames ou le pawn manque encore.
if (!OwningPlayer.IsValid())
{
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
if (!OwningPlayer.IsValid())
{
return;
}
}
const AFpsPlayer* BodyOwner = OwningPlayer.Get();
const UCharacterMovementComponent* Movement = BodyOwner->GetCharacterMovement();
if (!Movement)
{
return;
}
// Composante horizontale seule : en pleine chute la vitesse verticale
// depasse largement la vitesse de course, et un BlendSpace indexe dessus
// jouerait un sprint en l'air.
const FVector Velocity = BodyOwner->GetVelocity();
const FVector HorizontalVelocity(Velocity.X, Velocity.Y, 0.f);
GroundSpeed = HorizontalVelocity.Size();
// L'acceleration, et pas seulement la vitesse : elle tombe a zero des que la
// touche est relachee, alors que la vitesse decroit encore pendant tout le
// freinage. Sans elle le cycle de marche continuerait de tourner sur un
// personnage qui glisse jusqu'a l'arret.
bIsMoving = GroundSpeed > MovingSpeedThreshold
&& !Movement->GetCurrentAcceleration().IsNearlyZero();
bIsFalling = Movement->IsFalling();
// Prise telle quelle, sans seuil : c'est le Blueprint qui decide ou placer la
// bascule saut / chute. Une valeur brute reste une valeur brute -- lui donner
// un seuil ici, ce serait decider a la place de la machine a etats, et il
// faudrait recompiler pour le regler.
VerticalSpeed = Velocity.Z;
// Les deux etats du pawn, repliques chacun de leur cote : bIsCrouched par le
// moteur, bIsSprinting par AFpsPlayer. On les lit ici plutot que de deviner
// l'accroupissement a la hauteur de capsule, qui est en pleine interpolation
// pendant toute la transition.
bIsCrouching = BodyOwner->bIsCrouched;
bIsSprinting = BodyOwner->IsSprinting();
// Direction relative au CORPS et non a la vitesse du monde : c'est ce que
// demande un BlendSpace directionnel. GetActorRotation() suffit comme
// reference parce que bUseControllerRotationYaw garde le corps aligne sur le
// regard -- sur un jeu a la 3e personne libre, il faudrait la rotation de
// controle a la place.
//
// UKismetAnimationLibrary et pas UAnimInstance::CalculateDirection, depreciee
// en 5.8 : le calcul est le meme, mais l'ancienne cassera la compilation a la
// prochaine version du moteur.
LocomotionDirection = UKismetAnimationLibrary::CalculateDirection(HorizontalVelocity, BodyOwner->GetActorRotation());
// GetBaseAimRotation et pas GetControlRotation : le second n'existe que sur
// la machine qui possede le pawn, le premier retombe sur RemoteViewPitch
// pour les pawns des autres joueurs. NormalizeAxis ramene les 350 degres
// d'un regard vers le bas a -10, seule forme exploitable par un AimOffset.
AimPitch = FRotator::NormalizeAxis(BodyOwner->GetBaseAimRotation().Pitch);
LocomotionPlayRate = FMath::Clamp(
GroundSpeed / ReferenceLocomotionSpeed,
MinLocomotionPlayRate,
MaxLocomotionPlayRate);
}
@@ -0,0 +1,150 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "PlayerBodyAnimInstance.generated.h"
class AFpsPlayer;
/**
* AnimInstance du corps vu a la TROISIEME personne -- celui que les autres
* joueurs voient, et dont on ne percoit soi-meme que l'ombre.
*
* Meme partage des roles que UFpsArmsAnimInstance : elle ne DECIDE de rien, elle
* lit l'etat du pawn une fois par frame et le republie en variables. C'est la
* machine a etats du Blueprint enfant qui choisit l'animation, parce qu'une
* transition se regle a l'oeil avec des durees de fondu et n'a rien a faire dans
* du C++ qu'il faut recompiler.
*
* La difference avec les bras est reseau, et elle est essentielle : les bras ne
* sont vus que par leur proprietaire, ce corps-ci est vu par TOUT LE MONDE.
* Chaque machine evalue donc les quatre corps de la partie. Et pourtant rien
* n'est replique ici -- tout est DERIVE d'un etat que le moteur ou le projet
* synchronisent deja :
*
* Velocity -> replique par le mouvement du Character
* bIsCrouched -> UPROPERTY repliquee d'ACharacter
* bIsSprinting -> repliquee par AFpsPlayer (c'est a ca qu'elle sert)
* AimPitch -> RemoteViewPitch, replique par APawn sans rien demander
*
* Repliquer une seule de ces variables serait envoyer deux fois la meme
* information -- exactement le reproche fait a UPlayerNameplateComponent.
*/
UCLASS()
class SURVIVAL_PROJET_API UPlayerBodyAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
protected:
// ------------------------------------------------------------------
// Etat publie vers le Blueprint d'animation
// ------------------------------------------------------------------
/** Vitesse horizontale en cm/s : l'axe du BlendSpace de deplacement. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float GroundSpeed = 0.f;
/**
* Direction du deplacement par rapport a l'orientation du corps, en degres :
* 0 = on avance, 90 = on va vers la droite, 180 = on recule.
*
* Elle est non nulle en permanence dans ce jeu et ce n'est pas un detail :
* bUseControllerRotationYaw garde le corps aligne sur la camera, donc un pas
* de cote est un VRAI pas de cote, jamais un demi-tour. Le pack ne livre que
* des cycles vers l'avant, donc personne ne la lit encore ; elle est publiee
* parce que le jour ou les cycles lateraux arriveront, il n'y aura qu'un
* BlendSpace a brancher et pas une classe a rouvrir.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float LocomotionDirection = 0.f;
/** Vrai quand le joueur se deplace REELLEMENT, freinage exclu. */
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsMoving = false;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsSprinting = false;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsFalling = false;
/**
* Vitesse verticale en cm/s : positif = on monte, negatif = on tombe.
*
* C'est elle, et elle seule, qui distingue un SAUT d'une CHUTE. bIsFalling
* vaut vrai dans les deux cas : le moteur appelle "falling" tout ce qui n'est
* pas au sol, detente comprise. Sans ce signe, sortir d'une falaise jouerait
* l'animation de detente -- un personnage qui prend son elan pour tomber.
*
* Gratuite sur le reseau comme le reste : le CharacterMovement replique deja
* la velocite des pawns distants, on ne fait que lire sa composante Z.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float VerticalSpeed = 0.f;
UPROPERTY(BlueprintReadOnly, Category = "Movement")
bool bIsCrouching = false;
/**
* Tangage du regard, en degres, borne a -90..90. Positif = le joueur regarde
* vers le haut.
*
* Gratuit sur le reseau : APawn compresse et replique deja RemoteViewPitch
* pour ses pawns distants, et GetBaseAimRotation() rend la bonne valeur des
* deux cotes sans qu'on ait une seule propriete a declarer. Personne ne la
* lit tant qu'il n'y a pas d'AimOffset -- mais le jour ou un ami doit voir
* qu'on regarde le ciel, c'est cette variable qu'on branchera.
*/
UPROPERTY(BlueprintReadOnly, Category = "Aim")
float AimPitch = 0.f;
/**
* Vitesse de lecture du cycle de deplacement.
*
* A NE BRANCHER QUE si l'on joue une animation unique. Avec le BlendSpace du
* pack, qui interpole deja marche et course sur l'axe de la vitesse, la
* brancher etirerait une seconde fois un cycle deja etire et donnerait une
* cadence qui accelere deux fois trop vite. Elle reste ici parce que le
* BlendSpace est calibre sur les vitesses du pack et pas sur les notres
* (200 / 400 / 650) : si la foulee patine, c'est le reglage a sortir.
*/
UPROPERTY(BlueprintReadOnly, Category = "Movement")
float LocomotionPlayRate = 1.f;
/**
* En dessous de cette vitesse (cm/s) on considere le joueur immobile.
*
* Un seuil strictement a zero ferait clignoter la transition course/repos
* pendant le freinage, ou la vitesse decroit en passant par des valeurs
* minuscules mais non nulles.
*/
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0"))
float MovingSpeedThreshold = 10.f;
/** Vitesse (cm/s) a laquelle le cycle tourne a sa cadence d'origine. */
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "1"))
float ReferenceLocomotionSpeed = 400.f;
/** Bornes du Play Rate, memes garde-fous que sur les bras. */
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MinLocomotionPlayRate = 0.6f;
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
float MaxLocomotionPlayRate = 1.8f;
private:
/**
* Cache du pawn. TryGetPawnOwner() fait un Cast a chaque appel, et cette
* fonction tourne a chaque frame sur QUATRE corps en partie a quatre.
*
* TWeakObjectPtr et pas un pointeur nu : apres un respawn le pawn est
* detruit, et cet objet peut lui survivre d'une frame.
*/
TWeakObjectPtr<AFpsPlayer> OwningPlayer;
};
@@ -23,7 +23,10 @@ public class Survival_projet : ModuleRules
// EngineSettings : UGameMapsSettings::GetGameDefaultMap(), qui evite de
// redeclarer le chemin de la map de menu dans le C++ alors qu'il est
// deja dans DefaultEngine.ini.
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "EngineSettings" });
// AnimGraphRuntime : UKismetAnimationLibrary::CalculateDirection, qui remplace
// la methode homonyme d'UAnimInstance depreciee en 5.8. Prive et pas public --
// seul le .cpp de UPlayerBodyAnimInstance l'appelle, son header n'en montre rien.
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "EngineSettings", "AnimGraphRuntime" });
// Coop Steam : on ne depend QUE de l'API generique (OnlineSubsystem) et de
// ses helpers (OnlineSubsystemUtils, qui porte le resolveur d'adresses).