(Feat) Add Animation Fps Charater

This commit is contained in:
2026-08-13 22:07:08 +02:00
parent 176beab9fc
commit cca54a5e0d
157 changed files with 850 additions and 148 deletions
+51 -12
View File
@@ -51,10 +51,13 @@ règles de mort), demander avant de coder plutôt que de deviner.
## Compiler
```powershell
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" Survival_projetEditor Win64 Development -Project="d:\Projet\Perso\Unreal\Survival_projet\Survival_projet.uproject" -WaitMutex
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" EmberWildEditor Win64 Development -Project="d:\Projet\Perso\Unreal\Survival_projet\EmberWild.uproject" -WaitMutex
```
- Un DLL nommé `UnrealEditor-Survival_projet-000N.dll` signifie que **l'éditeur était ouvert** :
*(Le module et le `.uproject` s'appellent désormais **EmberWild** ; seul le dossier sur le disque
est resté `Survival_projet`.)*
- Un DLL nommé `UnrealEditor-EmberWild-000N.dll` signifie que **l'éditeur était ouvert** :
c'est un hot reload. Prévenir l'auteur de fermer et relancer l'éditeur.
- `-Rebuild` échoue tant que l'éditeur tourne (DLL verrouillés).
- **Ne jamais réécrire en masse les timestamps** des fichiers source pour forcer un build.
@@ -243,7 +246,7 @@ distribué par le dépôt Steam. C'est la procédure de référence.
```powershell
& "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\RunUAT.bat" BuildCookRun `
-project="D:\Projet\Perso\Unreal\Survival_projet\Survival_projet.uproject" `
-project="D:\Projet\Perso\Unreal\Survival_projet\EmberWild.uproject" `
-noP4 -platform=Win64 -clientconfig=Development `
-build -cook -stage -pak -compressed -prereqs `
-archive -archivedirectory="D:\Projet\Perso\Unreal\Survival_projet\Build\Package" `
@@ -254,19 +257,28 @@ distribué par le dépôt Steam. C'est la procédure de référence.
commande `EmberJoin`. Sans eux, un échec chez le testeur ne dit rien. Premier cook ≈ 18 min,
les suivants sont bien plus courts (DDC chaud).
**2. Copier dans le dépôt** — le contenu de `Build\Package\Windows\` va **à la racine** de
`C:\sdk\tools\ContentBuilder\content\`, en excluant `steam_appid.txt` : Steam fournit lui-même
l'AppID au lancement, et le fichier l'écraserait.
**2. Copier et uploader — c'est le MÊME script, il n'y a rien à copier à la main.**
`C:\sdk\tools\ContentBuilder\upload_emberwild.bat` (App **4282850**, dépôt **4282851**) fait les
trois étapes : `rmdir /s /q content` puis `mkdir`, robocopy depuis `Build\Package\Windows`
en `/XF steam_appid.txt`, puis `steamcmd`. Le `.vdf` n'a **pas** de `SetLive`, donc le build monte
sans devenir live ; `upload_emberwild.bat live` utilise l'autre `.vdf` et publie.
**3. Uploader**`C:\sdk\tools\ContentBuilder\upload_emberwild.bat` (App **4282850**, dépôt
**4282851**). Le `.vdf` n'a **pas** de `SetLive`, donc le build monte sans devenir live.
Deux choses à savoir sur ce script :
**4. Publier sur la branche `develop`**, jamais sur `default`. Et vérifier que *Launch Options*
pointe sur `Survival_projet.exe` — puis **publier les changements**, sinon rien n'est visible
- **Il repart d'un `content\` vide à chaque fois, et c'est indispensable** : le staging d'UAT ne
supprime rien, donc un fichier disparu du jeu — ou renommé, comme au passage
`Survival_projet``EmberWild` — resterait dans le dépôt indéfiniment et les testeurs
traîneraient des assets fantômes.
- **Il garde en dur le nom de l'exe** (`EmberWild.exe`) dans ses trois garde-fous : présence du
build, date de l'exe, vérification d'après-copie. Un renommage de projet le casse avec un
« aucun build trouve » trompeur, alors que le build est bien là.
**3. Publier sur la branche `develop`**, jamais sur `default`. Et vérifier que *Launch Options*
pointe sur `EmberWild.exe` — puis **publier les changements**, sinon rien n'est visible
côté client.
**5. Tester à deux** : les deux lancent le jeu **depuis Steam**, l'hôte clique *Jouer*, puis
Échap → *Inviter des amis*. Les logs à lire sont dans `Survival_projet/Saved/Logs/` de chaque
**4. Tester à deux** : les deux lancent le jeu **depuis Steam**, l'hôte clique *Jouer*, puis
Échap → *Inviter des amis*. Les logs à lire sont dans `EmberWild/Saved/Logs/` de chaque
côté — `ShowInviteUI` chez l'hôte, `invitation acceptee` puis `voyage vers [...]` chez l'invité.
**Deux réglages qui cassent tout si on les oublie** : la *List of Maps to Include* dans les
@@ -565,6 +577,33 @@ du joueur dans lui-même.
accidentelle).
- **Clic gauche** = utiliser l'objet actif de la barre. Bindé sur le **pawn**, donc muet quand
l'inventaire est ouvert — le conflit avec le drag & drop se règle tout seul.
- **L'effet du clic gauche part d'un AnimNotify, jamais de l'appui.** Le clic ne fait que lancer
le montage ; c'est `UAnimNotify_ItemAction`, posé dans la timeline au moment de l'impact, qui
appelle `AFpsPlayer::TriggerHeldItemAction()`. Sinon la hache abat l'arbre *avant* de l'avoir
touché — le résultat précède le geste qui aurait dû le produire. L'instant de l'impact
appartient à l'**animation** : un délai en secondes sur le DataAsset serait à re-régler à
chaque animation livrée, et faux dès qu'on touche au Play Rate. Trois points à tenir :
le montage d'usage ne tourne **que sur les bras du joueur local**, donc le notify tombe sur une
seule machine et le routage réseau ne change pas ; un objet **sans `UseMontage` retombe sur
l'effet immédiat** (aucun marqueur ne viendra jamais, le priver d'effet le casserait) ; et la
case visée est **figée à l'appui** (`PendingUseSlotIndex`), la molette pouvant changer d'objet
pendant les trois dixièmes de seconde du geste.
- **Saut des bras FPS : une machine à états, pas des montages.** `JumpStart``JumpFallLoop`
`JumpLanding` / `HardLanding`. Le milieu de la chaîne est une **boucle**, et une boucle ne se
joue pas en montage — c'est ce qui a fait retirer `LandMontage` / `HardLandMontage`, dont le
geste se serait de toute façon superposé à l'état de chute qui venait de démarrer. La sortie de
l'état de chute est pilotée par `ELandingImpact` (`None` / `Soft` / `Hard`), produit par le pawn.
**`Hard` signifie exactement « cette chute a coûté de la vie »** : son seuil est
`FallDamageMinSpeed` et rien d'autre — il n'existe aucun réglage d'animation à côté, qui
finirait par diverger et ferait encaisser le personnage sans qu'il perde un point de vie. Le
seuil bas, lui, reste celui de la secousse de caméra (`LandPunchMinFallSpeed`). Un enum et pas
deux booléens : les trois valeurs mènent aux trois
sorties et s'excluent par construction, donc aucune combinaison ne rend deux transitions valides
en même temps. Il est **remis à `None` dès la première image de vol** (dans `Tick`), ce qui
dispense chaque transition de vérifier `bIsFalling` par-dessus. Deux états de réception distincts
et pas un seul avec un `Blend Poses by bool` : c'est le piège déjà noté pour l'accroupissement,
« Automatic Rule Based on Sequence Player in State » a besoin d'**un** Sequence Player pour lire
sa durée.
- **Objets à charges** (`MaxUses > 1`) ne s'empilent jamais. Les valeurs de restauration
s'appliquent **par utilisation**. L'usure suit l'objet jusqu'au sol et revient avec lui.
- **Mort** : tout tombe au sol autour du corps, respawn au `PlayerStart`, jauges pleines.
+14 -3
View File
@@ -86,11 +86,22 @@ FontDPIPreset=Standard
FontDPI=72
[/Script/Engine.Engine]
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Survival_projet")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Survival_projet")
; Le nom du MODULE est grave dans chaque .uasset qui reference une classe C++ :
; un Blueprint derive d'AFpsPlayer stocke "/Script/<module>.FpsPlayer". Renommer
; le module sans redirection casserait donc la classe parente de tous les BP,
; widgets et DataAssets d'un coup. Ces lignes sont lues AVANT la resolution des
; references, donc le moteur charge l'ancien nom et trouve le nouveau.
;
; Elles restent valables tant que des assets non re-sauvegardes portent l'ancien
; nom -- c'est-a-dire indefiniment en pratique, un asset n'etant reecrit que
; quand on y touche. Ne pas les supprimer.
+ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="Survival_projet",NewGameName="/Script/EmberWild")
+ActiveGameNameRedirects=(OldGameName="/Script/Survival_projet",NewGameName="/Script/EmberWild")
; Sans cette ligne le moteur instancie UGameUserSettings et nos reglages de jeu
; n'existent tout simplement pas au runtime.
GameUserSettingsClassName=/Script/Survival_projet.SurvivalUserSettings
GameUserSettingsClassName=/Script/EmberWild.SurvivalUserSettings
; ==================================================================
; Coop Steam (4 joueurs, listen server, P2P par le relais Valve)
+10 -1
View File
@@ -9,6 +9,15 @@ CommonUI.FallbackToDesiredOnAutoRestoreFailure=1
[/Script/EngineSettings.GeneralProjectSettings]
ProjectID=4774BE68499C24D0A70C6E87B49DA419
; Le titre affiche est ce que porte la barre de fenetre du jeu packagee. Il est
; independant du nom du module et de l'exe : on peut donc ecrire le nom commercial
; ("Emberwild", comme sur la fiche Steam) sans toucher au code.
ProjectName=Emberwild
ProjectDisplayedTitle=NSLOCTEXT("[/Script/EngineSettings]", "ProjectDisplayedTitle", "Emberwild")
CompanyName=Highland Game Studio
CompanyDistinguishedName=Highland Game Studio
Homepage="https://highlandgamesstudio.com/"
SupportContact="https://highlandgamesstudio.com/"
[/Script/DiscordRichPresence.DiscordRichPresenceSettings]
ClientId=1432045921620725933
@@ -58,7 +67,7 @@ bMoviesAreSkippable=True
+StartupMovies=UnrealLogo
+StartupMovies=StudioLogo
[/Script/Survival_projet.SurvivalAudioSettings]
[/Script/EmberWild.SurvivalAudioSettings]
MasterSoundClass=/Game/Game/Sounds/Mix/SC_Master.SC_Master
MusicSoundClass=/Game/Game/Sounds/Mix/SC_Music.SC_Music
EffectsSoundClass=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@
"Description": "",
"Modules": [
{
"Name": "Survival_projet",
"Name": "EmberWild",
"Type": "Runtime",
"LoadingPhase": "Default",
"AdditionalDependencies": [
@@ -3,13 +3,13 @@
using UnrealBuildTool;
using System.Collections.Generic;
public class Survival_projetTarget : TargetRules
public class EmberWildTarget : TargetRules
{
public Survival_projetTarget(TargetInfo Target) : base(Target)
public EmberWildTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
DefaultBuildSettings = BuildSettingsVersion.V7;
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_8;
ExtraModuleNames.Add("Survival_projet");
ExtraModuleNames.Add("EmberWild");
}
}
@@ -2,9 +2,9 @@
using UnrealBuildTool;
public class Survival_projet : ModuleRules
public class EmberWild : ModuleRules
{
public Survival_projet(ReadOnlyTargetRules Target) : base(Target)
public EmberWild(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
+6
View File
@@ -0,0 +1,6 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "EmberWild.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, EmberWild, "EmberWild" );
@@ -0,0 +1,43 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "AnimNotify_ItemAction.h"
#include "Components/SkeletalMeshComponent.h"
#include "Engine/World.h"
#include "FpsPlayer.h"
void UAnimNotify_ItemAction::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference)
{
Super::Notify(MeshComp, Animation, EventReference);
if (!MeshComp)
{
return;
}
// L'editeur d'animation joue les notifies dans sa fenetre de previsualisation,
// sur un mesh de preview qui n'appartient a aucun pawn. Le Cast ci-dessous
// suffirait a s'en proteger, mais le monde de preview n'est pas un monde de
// jeu et rien de ce qui suit n'y aurait de sens : on s'arrete plus tot.
const UWorld* World = MeshComp->GetWorld();
if (!World || !World->IsGameWorld())
{
return;
}
// Le montage tourne sur les bras ou sur le corps, deux composants du pawn :
// dans les deux cas le proprietaire du mesh EST le personnage.
if (AFpsPlayer* Player = Cast<AFpsPlayer>(MeshComp->GetOwner()))
{
Player->TriggerHeldItemAction();
}
}
FString UAnimNotify_ItemAction::GetNotifyName_Implementation() const
{
// Ce qui s'affiche sur le marqueur dans la timeline du montage. Sans cette
// surcharge le moteur y ecrirait le nom de la classe, prefixe compris.
return TEXT("Item Action");
}
@@ -58,6 +58,7 @@ void UFpsArmsAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
bIsSprinting = Player->IsSprinting();
bIsFalling = Movement->IsFalling();
LandingImpact = Player->GetLandingImpact();
bIsCrouching = Movement->IsCrouching();
VerticalSpeed = Movement->Velocity.Z;
@@ -249,6 +249,11 @@ void AFpsPlayer::Tick(float DeltaTime)
if (GetCharacterMovement()->IsFalling())
{
LastFallSpeed = GetCharacterMovement()->Velocity.Z;
// Et on efface la reception precedente des la premiere image de vol :
// c'est ce qui permet aux transitions de la machine a etats de se
// contenter de comparer l'enum, sans verifier bIsFalling par-dessus.
LandingImpact = ELandingImpact::None;
}
UpdateCameraEffects(DeltaTime);
@@ -499,17 +504,55 @@ void AFpsPlayer::Landed(const FHitResult& Hit)
FallSpeed, LandPunchMinFallSpeed);
}
// Les degats decident de tout : c'est la meme valeur qui fait perdre de la
// vie et qui fait encaisser les bras. Une reception lourde est, par
// definition, une chute qui a fait mal.
const float Damage = ComputeFallDamage(FallSpeed);
// Pas de garde d'autorite ici : USurvivalStatsComponent::ApplyDamage la pose
// deja, et la dupliquer donnerait deux endroits ou la regle peut changer.
// Chez un client l'appel ne fait donc rien -- sa vie descend par OnRep_Stats.
if (Damage > 0.f && SurvivalStats)
{
SurvivalStats->ApplyDamage(Damage);
}
// Le geste des bras suit le meme seuil bas que la camera : une marche
// d'escalier ne doit ni secouer la vue ni faire encaisser le personnage.
if (FallSpeed > LandPunchMinFallSpeed)
// Sous ce seuil l'enum reste a None, et la machine a etats repart droit de
// la chute vers la locomotion sans jouer de reception.
if (FallSpeed <= LandPunchMinFallSpeed)
{
UAnimMontage* Montage = (FallSpeed >= HardLandingFallSpeed) ? HardLandMontage : LandMontage;
PlayArmsMontage(Montage);
LandingImpact = ELandingImpact::None;
}
else
{
LandingImpact = (Damage > 0.f) ? ELandingImpact::Hard : ELandingImpact::Soft;
}
LastFallSpeed = 0.f;
}
float AFpsPlayer::ComputeFallDamage(float FallSpeed) const
{
// Comparaison STRICTE : a vitesse nulle comme a toute vitesse sous le seuil,
// la chute ne coute rien. Avec un >=, un FallDamageMinSpeed mis a zero en
// PIE ferait perdre de la vie a chaque pas.
if (FallSpeed <= FallDamageMinSpeed)
{
return 0.f;
}
// Lineaire en VITESSE et pas en energie : la vitesse est ce que le joueur
// percoit -- le temps qu'il passe a tomber -- alors qu'un modele en v²
// rendrait les dix derniers metres brutalement mortels sans que rien a
// l'ecran ne l'ait annonce.
return FMath::GetMappedRangeValueClamped(
FVector2D(FallDamageMinSpeed, FMath::Max(FallDamageMaxSpeed, FallDamageMinSpeed + 1.f)),
FVector2D(FallDamageMin, FallDamageMax),
FallSpeed);
}
void AFpsPlayer::OnJumped_Implementation()
{
Super::OnJumped_Implementation();
@@ -692,18 +735,67 @@ void AFpsPlayer::HandleSprintReleased()
void AFpsPlayer::UseSelectedItem()
{
const int32 SelectedIndex = InventoryComponent->GetSelectedHotbarIndex();
// La case est figee ICI, a l'appui, et relue a l'impact : voir
// PendingUseSlotIndex.
PendingUseSlotIndex = InventoryComponent->GetSelectedHotbarIndex();
// Le geste part AVANT le routage reseau, et sans rien attendre de lui : il
// n'existe que sur cette machine (bras en OnlyOwnerSee) et doit repondre au
// clic dans la frame. Le faire transiter par le serveur ajouterait un
// aller-retour a la seule partie du systeme que le joueur ressent
// immediatement.
//
// Consequence assumee : le geste se joue meme si le serveur refuse ensuite
// l'action. Sur un coup de pioche c'est invisible ; pour un consommable qui
// n'existe plus, c'est OnRep_Slots qui remet l'affichage d'aplomb.
const EUseMontageResult Result = HeldItemComponent
? HeldItemComponent->PlayUseMontage()
: EUseMontageResult::NoMontage;
switch (Result)
{
case EUseMontageResult::Started:
// L'effet appartient desormais au montage : il partira de son marqueur,
// au moment ou la hache touche. Rien a faire de plus a l'appui.
return;
case EUseMontageResult::AlreadyPlaying:
// Marteler le bouton ne relance pas le bras ; il ne doit pas davantage
// declencher l'effet, sinon un clic maintenu recolterait a chaque image
// pendant que le geste, lui, n'en joue qu'un.
return;
default:
break;
}
// Aucun montage a jouer : personne n'enverra de marqueur, l'effet part tout
// de suite. C'est le comportement d'avant, conserve pour les objets sans
// animation -- la plupart des consommables aujourd'hui.
TriggerHeldItemAction();
}
void AFpsPlayer::TriggerHeldItemAction()
{
// Le montage d'usage ne tourne aujourd'hui que sur les bras du joueur local,
// donc cette garde ne rejette rien. Elle est la pour le jour ou un montage
// sera joue sur le CORPS, visible des quatre machines : sans elle, chaque
// coup de hache partirait en autant d'exemplaires qu'il y a de joueurs.
if (!IsLocallyControlled() || !InventoryComponent)
{
return;
}
// L'hote agit tout de suite, le client demande. On envoie meme si le slot
// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
// peut avoir une frame d'avance sur le notre.
if (!HasAuthority())
{
Server_UseItem(SelectedIndex);
Server_UseItem(PendingUseSlotIndex);
return;
}
PerformUseItem(SelectedIndex);
PerformUseItem(PendingUseSlotIndex);
}
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
@@ -1028,6 +1120,15 @@ void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdj
CrouchTargetAlpha = 1.f;
AddCrouchTilt(1.f);
// Le geste des bras part d'ici et pas d'une transition d'AnimBP : c'est le
// seul endroit qui soit certain que la capsule change VRAIMENT de taille.
// Le CharacterMovement peut refuser un accroupissement, et une machine a
// etats qui suivrait la touche jouerait alors un geste sans rien derriere.
if (HeldItemComponent)
{
HeldItemComponent->PlayCrouchMontage(/*bEntering=*/true);
}
}
void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
@@ -1036,4 +1137,11 @@ void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjus
CrouchTargetAlpha = 0.f;
AddCrouchTilt(-1.f);
// Symetrique exact d'OnStartCrouch : se relever peut etre refuse par un
// plafond bas, et c'est le moteur qui tranche, pas la touche.
if (HeldItemComponent)
{
HeldItemComponent->PlayCrouchMontage(/*bEntering=*/false);
}
}
@@ -123,6 +123,13 @@ void UHeldItemComponent::RefreshFromInventory()
const FInventorySlot Selected = Inventory->GetSelectedSlot();
SetHeldItem(Selected.IsEmpty() ? nullptr : Selected.Item.Get());
// Reposee meme quand SetHeldItem est sortie tot faute de changement : chez un
// client, les premiers rafraichissements tombent avant que le controller soit
// la, donc avant qu'on ait le droit de toucher aux bras. Sans ce rattrapage,
// un joueur qui reapparait avec sa pioche deja selectionnee garderait les
// cycles mains nues jusqu'a ce qu'il change de case.
UpdateArmsAnimLayer();
}
void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem)
@@ -134,6 +141,11 @@ void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem)
HeldItem = NewItem;
ApplyHeldVisuals();
// La couche AVANT le montage : le geste de sortie doit se superposer aux
// cycles du nouvel outil, pas a ceux qu'on vient de quitter. L'inverse se
// voit sur la premiere image, la main partant de la mauvaise pose.
UpdateArmsAnimLayer();
PlayEquipMontage();
// En local d'abord, pour que l'objet apparaisse sous les doigts sans attendre
@@ -145,29 +157,141 @@ void UHeldItemComponent::SetHeldItem(UItemDataAsset* NewItem)
}
}
void UHeldItemComponent::UpdateArmsAnimLayer()
{
AFpsPlayer* Player = GetOwningPlayer();
if (!Player || !Player->IsLocallyControlled())
{
return;
}
USkeletalMeshComponent* Arms = Player->GetFirstPersonArms();
if (!Arms)
{
return;
}
// Le mesh REELLEMENT monte fait foi, pas le seul HeldItem : un objet dont on
// a oublie de cocher bShowInHand ferait sinon jouer les cycles de la pioche
// sur une main vide.
const TSubclassOf<UAnimInstance> DesiredLayer =
(bItemVisibleInHand && HeldItem) ? HeldItem->ArmsAnimLayer : nullptr;
if (DesiredLayer == LinkedArmsLayer)
{
return;
}
// Delier AVANT de lier, toujours. LinkAnimClassLayers ajoute une couche sans
// retirer la precedente : enchainer hache puis pioche laisserait les deux en
// place, et la hache l'emporterait sur toute fonction que la pioche
// n'implemente pas. Aucun message ne le signale.
if (LinkedArmsLayer)
{
Arms->UnlinkAnimClassLayers(LinkedArmsLayer);
}
if (DesiredLayer)
{
Arms->LinkAnimClassLayers(DesiredLayer);
}
LinkedArmsLayer = DesiredLayer;
}
void UHeldItemComponent::PlayEquipMontage()
{
// Ranger un objet ne joue rien : le pack ne livre que des gestes de sortie
// d'outil, et un desequipement sans animation ne se remarque pas -- l'objet
// suivant arrive dans la foulee avec le sien.
if (!HeldItem || !HeldItem->EquipMontage)
if (!HeldItem)
{
return;
}
PlayLocalArmsMontage(HeldItem->EquipMontage, /*bSkipIfAlreadyPlaying=*/false);
}
EUseMontageResult UHeldItemComponent::PlayUseMontage()
{
// Le test porte sur l'OBJET et pas sur le mesh monte : une pomme n'apparait
// pas dans la main (bShowInHand faux) mais elle occupe bien la case active,
// et la manger ne doit pas declencher un coup de poing.
UAnimMontage* const Montage = HeldItem ? HeldItem->UseMontage.Get() : UnarmedUseMontage.Get();
if (!Montage)
{
return EUseMontageResult::NoMontage;
}
// Le geste deja en cours est distingue AVANT l'appel, parce que
// PlayLocalArmsMontage rend false dans les deux cas et que l'appelant, lui,
// doit les separer : l'un enverra son marqueur d'impact, l'autre jamais.
const AFpsPlayer* Player = GetOwningPlayer();
const USkeletalMeshComponent* Arms = Player ? Player->GetFirstPersonArms() : nullptr;
const UAnimInstance* ArmsAnim = Arms ? Arms->GetAnimInstance() : nullptr;
if (ArmsAnim && ArmsAnim->Montage_IsPlaying(Montage))
{
return EUseMontageResult::AlreadyPlaying;
}
// Chez un pawn distant les bras ne jouent rien (voir PlayLocalArmsMontage) :
// aucun marqueur n'en sortira, d'ou NoMontage plutot qu'un etat a part. La
// garde IsLocallyControlled() du pawn reste seule juge de ce qui part sur
// le reseau.
return PlayLocalArmsMontage(Montage, /*bSkipIfAlreadyPlaying=*/true)
? EUseMontageResult::Started
: EUseMontageResult::NoMontage;
}
bool UHeldItemComponent::PlayCrouchMontage(bool bEntering)
{
UAnimMontage* const Held = bEntering ? HeldEnterCrouchMontage.Get() : HeldExitCrouchMontage.Get();
UAnimMontage* const Unarmed = bEntering ? UnarmedEnterCrouchMontage.Get() : UnarmedExitCrouchMontage.Get();
// Le repli sur la version mains vides est ce qui permet de n'en fournir
// qu'une au depart : les gestes « avec objet » arriveront plus tard sans
// qu'on ait a repasser dans le code.
//
// Le test porte sur le MESH monte et pas sur HeldItem, contrairement au clic
// gauche : ce qui change la posture des genoux, c'est un objet qu'on voit
// dans la main, pas une pomme rangee dans la case active.
UAnimMontage* const Montage = (bItemVisibleInHand && Held) ? Held : Unarmed;
// Pas de bSkipIfAlreadyPlaying : marteler la touche d'accroupissement doit
// relancer le geste, puisque la capsule, elle, change bel et bien de taille
// a chaque fois.
return PlayLocalArmsMontage(Montage, /*bSkipIfAlreadyPlaying=*/false);
}
bool UHeldItemComponent::PlayLocalArmsMontage(UAnimMontage* Montage, bool bSkipIfAlreadyPlaying)
{
if (!Montage)
{
return false;
}
AFpsPlayer* Player = GetOwningPlayer();
// Les bras sont en OnlyOwnerSee : chez l'hote, le pawn de chaque ami porte
// AUSSI un squelette de bras que personne ne rend. Y jouer un montage ferait
// tourner quatre machines a etats pour rien. La garde est deja tenue par
// RefreshFromInventory, on la repose ici parce qu'un futur appelant
// l'oublierait.
// tourner quatre machines a etats pour rien.
if (!Player || !Player->IsLocallyControlled())
{
return;
return false;
}
Player->PlayArmsMontage(HeldItem->EquipMontage);
if (bSkipIfAlreadyPlaying)
{
const USkeletalMeshComponent* Arms = Player->GetFirstPersonArms();
const UAnimInstance* ArmsAnim = Arms ? Arms->GetAnimInstance() : nullptr;
if (ArmsAnim && ArmsAnim->Montage_IsPlaying(Montage))
{
return false;
}
}
return Player->PlayArmsMontage(Montage) > 0.f;
}
void UHeldItemComponent::Server_SetHeldItem_Implementation(UItemDataAsset* NewItem)
@@ -0,0 +1,54 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "AnimNotify_ItemAction.generated.h"
/**
* Declenche l'effet de l'objet en main a l'instant precis ou le geste frappe.
*
* ------------------------------------------------------------------
* Pourquoi un notify plutot que le clic
* ------------------------------------------------------------------
*
* Le clic gauche et le COUP sont deux choses distinctes, separees par le temps
* que met le bras a descendre. Faire partir le trace de recolte sur l'appui
* donne une hache qui abat l'arbre avant de l'avoir touche : le joueur voit le
* resultat, puis l'animation qui aurait du le produire. C'est le meme
* decalage qu'un bruit de pas joue au debut de la foulee.
*
* L'instant de l'impact n'est pas une valeur qu'on puisse ecrire en C++ : il
* appartient a l'ANIMATION, il change d'un montage a l'autre, et il rebouge a
* chaque fois qu'on retouche la courbe. Un delai en secondes sur le DataAsset
* serait a re-regler a la main a chaque nouvelle animation livree -- et faux
* des qu'on toucherait au Play Rate. Le marqueur, lui, suit le montage.
*
* ------------------------------------------------------------------
* Reseau : ce notify ne tombe QUE chez le joueur local
* ------------------------------------------------------------------
*
* Le montage d'usage est joue sur les BRAS, qui sont en OnlyOwnerSee et
* n'existent visuellement que chez le proprietaire du pawn
* (UHeldItemComponent::PlayLocalArmsMontage). Le notify part donc sur une
* seule machine, exactement comme le clic qu'il remplace, et le routage vers
* le serveur reste celui d'avant : AFpsPlayer::TriggerHeldItemAction envoie la
* RPC, le serveur fait autorite.
*
* Corollaire a ne pas oublier le jour ou un montage d'usage sera joue aussi sur
* le CORPS (visible des autres joueurs) : ce montage-la tournerait sur les
* quatre machines, et sans la garde IsLocallyControlled() de
* TriggerHeldItemAction chaque coup de hache partirait en quatre exemplaires.
*/
UCLASS(const, hidecategories = Object, collapsecategories, meta = (DisplayName = "Item Action"))
class EMBERWILD_API UAnimNotify_ItemAction : public UAnimNotify
{
GENERATED_BODY()
public:
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation,
const FAnimNotifyEventReference& EventReference) override;
virtual FString GetNotifyName_Implementation() const override;
};
@@ -29,7 +29,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAppearanceApplied);
* pseudo, et pour la meme raison : elle doit survivre a la mort du pawn.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UCharacterAppearanceComponent : public UActorComponent
class EMBERWILD_API UCharacterAppearanceComponent : public UActorComponent
{
GENERATED_BODY()
@@ -56,7 +56,7 @@ enum class ECustomizationCategory : uint8
* Classe Abstract : on cree un WBP_CharacterCustomization qui en herite.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UCharacterCustomizationWidget : public UUserWidget
class EMBERWILD_API UCharacterCustomizationWidget : public UUserWidget
{
GENERATED_BODY()
@@ -94,7 +94,7 @@ struct FCharacterBodySet
* hors bornes a une valeur sure.
*/
UCLASS(BlueprintType)
class SURVIVAL_PROJET_API UCharacterPartsDataAsset : public UPrimaryDataAsset
class EMBERWILD_API UCharacterPartsDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
@@ -40,7 +40,7 @@ enum class ECharacterPreviewFraming : uint8
* moment -- une fois en partie.
*/
UCLASS()
class SURVIVAL_PROJET_API ACharacterPreviewActor : public AActor
class EMBERWILD_API ACharacterPreviewActor : public AActor
{
GENERATED_BODY()
@@ -23,7 +23,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnConfirmDialogCancelled);
* Classe Abstract : on cree un WBP_ConfirmDialog qui en herite.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UConfirmDialogWidget : public UUserWidget
class EMBERWILD_API UConfirmDialogWidget : public UUserWidget
{
GENERATED_BODY()
@@ -139,7 +139,7 @@ struct FCookingInteraction
* interessante.
*/
UCLASS()
class SURVIVAL_PROJET_API ACookingStation : public AActor, public IInteractable
class EMBERWILD_API ACookingStation : public AActor, public IInteractable
{
GENERATED_BODY()
@@ -70,7 +70,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCraftOverflow, UItemDataAsset*
* futur etabli pourra porter le meme composant avec son propre livre.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UCraftingComponent : public UActorComponent
class EMBERWILD_API UCraftingComponent : public UActorComponent
{
GENERATED_BODY()
@@ -20,7 +20,7 @@ class UTextBlock;
* afficher autre chose que ce sur quoi le bouton Fabriquer s'appuie.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UCraftingIngredientRowWidget : public UUserWidget
class EMBERWILD_API UCraftingIngredientRowWidget : public UUserWidget
{
GENERATED_BODY()
@@ -21,7 +21,7 @@ class UCraftingRecipeDataAsset;
* la table de craft du jeu.
*/
UCLASS(BlueprintType)
class SURVIVAL_PROJET_API UCraftingRecipeBook : public UPrimaryDataAsset
class EMBERWILD_API UCraftingRecipeBook : public UPrimaryDataAsset
{
GENERATED_BODY()
@@ -63,7 +63,7 @@ struct FCraftIngredient
* recette la debloquerait pour toutes les parties a la fois.
*/
UCLASS(BlueprintType)
class SURVIVAL_PROJET_API UCraftingRecipeDataAsset : public UPrimaryDataAsset
class EMBERWILD_API UCraftingRecipeDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
@@ -19,7 +19,7 @@ class UTextBlock;
* son propre clic, parce que c'est elle qui recoit l'evenement souris.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UCraftingRecipeSlotWidget : public UUserWidget
class EMBERWILD_API UCraftingRecipeSlotWidget : public UUserWidget
{
GENERATED_BODY()
@@ -28,7 +28,7 @@ class UStaticMeshComponent;
* qui change par instance, comme ItemData sur BP_Pickup.
*/
UCLASS()
class SURVIVAL_PROJET_API ACraftingStation : public AActor, public IInteractable
class EMBERWILD_API ACraftingStation : public AActor, public IInteractable
{
GENERATED_BODY()

Some files were not shown because too many files have changed in this diff Show More