(Feat) Add Animation
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "FpsArmsAnimInstance.generated.h"
|
||||
|
||||
class AFpsPlayer;
|
||||
|
||||
/**
|
||||
* AnimInstance des bras en vue premiere personne.
|
||||
*
|
||||
* Elle ne DECIDE de rien : elle lit l'etat du pawn une fois par frame et le
|
||||
* republie sous forme de variables. C'est la machine a etats du Blueprint enfant
|
||||
* (ABP_FpsArms) qui choisit l'animation. La separation est volontaire -- une
|
||||
* transition d'animation se regle a l'oeil, avec des durees de fondu, et ca n'a
|
||||
* rien a faire dans du C++ qu'il faut recompiler.
|
||||
*
|
||||
* Corollaire : ces variables ne sont JAMAIS repliquees. Les bras ne sont vus que
|
||||
* par leur proprietaire, donc chaque machine calcule les siens a partir d'un etat
|
||||
* de pawn que la replication a deja synchronise (vitesse, sprint, accroupi).
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API UFpsArmsAnimInstance : 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. Sert aussi a moduler le PlayRate de la course. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
float GroundSpeed = 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;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||
bool bIsCrouching = false;
|
||||
|
||||
/**
|
||||
* Vitesse de lecture du cycle de deplacement, a brancher sur la broche
|
||||
* Play Rate du noeud d'animation.
|
||||
*
|
||||
* Le projet a trois vitesses de jeu (accroupi 200, marche 400, sprint 650)
|
||||
* pour un seul cycle d'animation. Plutot qu'un etat par vitesse -- trois
|
||||
* seuils, donc trois occasions de claquer quand on les franchit -- on etire
|
||||
* le meme cycle. La cadence des bras suit alors la vitesse en continu, y
|
||||
* compris pendant l'acceleration.
|
||||
*
|
||||
* Un vrai etat Sprint n'aura de sens que le jour ou l'animateur livrera une
|
||||
* animation de sprint a la POSTURE differente. Le meme cycle joue plus vite
|
||||
* n'est pas une animation de sprint.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* A regler a l'oeil dans ABP_FpsArms : c'est la vitesse ou les bras ont l'air
|
||||
* naturels sans etirement. Par defaut la vitesse de marche du pawn, donc le
|
||||
* sprint accelere d'environ 60 %.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "1"))
|
||||
float ReferenceLocomotionSpeed = 400.f;
|
||||
|
||||
/**
|
||||
* Bornes du Play Rate. Sans le plancher, un joueur qui rampe a 20 cm/s
|
||||
* regarderait des bras quasi immobiles ; sans le plafond, une future vitesse
|
||||
* bonus donnerait des bras en accelere qui sortent du registre credible.
|
||||
*/
|
||||
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 un mesh anime.
|
||||
*
|
||||
* 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;
|
||||
};
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "FpsPlayer.generated.h"
|
||||
|
||||
class APickupItem;
|
||||
class UAnimMontage;
|
||||
class UCameraComponent;
|
||||
class UCraftingComponent;
|
||||
class UHeadBobComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class UInteractionComponent;
|
||||
@@ -88,6 +90,26 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Camera")
|
||||
UCameraComponent* GetFirstPersonCamera() const { return FirstPersonCamera; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Camera")
|
||||
UHeadBobComponent* GetHeadBob() const { return HeadBob; }
|
||||
|
||||
/** Les bras vus par le joueur local. C'est ici que viendra l'objet en main. */
|
||||
UFUNCTION(BlueprintPure, Category = "Animation")
|
||||
USkeletalMeshComponent* GetFirstPersonArms() const { return FirstPersonArms; }
|
||||
|
||||
/**
|
||||
* Joue un montage sur les bras du joueur LOCAL, et seulement lui.
|
||||
*
|
||||
* Rien n'est replique et c'est voulu : les bras sont en OnlyOwnerSee, donc
|
||||
* les animer chez le serveur ou chez un ami ferait tourner un squelette que
|
||||
* personne ne rend. Le geste vu par les autres joueurs viendra du mesh 3e
|
||||
* personne, avec ses propres animations, le jour ou l'animateur les livrera.
|
||||
*
|
||||
* @return la duree du montage en secondes, 0 s'il n'a pas pu etre joue.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Animation")
|
||||
float PlayArmsMontage(UAnimMontage* Montage, float PlayRate = 1.f);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||
UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
|
||||
|
||||
@@ -198,6 +220,40 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||
|
||||
/**
|
||||
* Bras vue première personne, visibles du seul propriétaire.
|
||||
*
|
||||
* Attachés à la CAMÉRA et non à la capsule : ils doivent suivre le tangage du
|
||||
* regard, sinon lever les yeux les laisserait à l'horizontale. Bénéfice
|
||||
* gratuit au passage — ils héritent du ressort d'impact et de la bascule
|
||||
* d'accroupissement, donc les bras encaissent l'atterrissage avec la caméra
|
||||
* sans une ligne de code de plus.
|
||||
*
|
||||
* Sa transform relative se règle À L'ŒIL dans BP_FpsPlayer : les valeurs du
|
||||
* constructeur ne sont qu'un point de départ, elles dépendent entièrement de
|
||||
* l'origine du FBX livré par l'animateur.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation")
|
||||
TObjectPtr<USkeletalMeshComponent> FirstPersonArms;
|
||||
|
||||
/**
|
||||
* Montage joué sur les bras au ramassage d'un objet (MTG_GrabItem).
|
||||
* Vide = aucun geste, le ramassage fonctionne quand même.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
|
||||
TObjectPtr<UAnimMontage> GrabMontage;
|
||||
|
||||
/**
|
||||
* Balancement de tête procédural : respiration à l'arrêt, pas à la marche,
|
||||
* foulée au sprint. Tous ses réglages sont dans le Details panel du composant,
|
||||
* pas ici : cinq profils de cinq valeurs noieraient la catégorie "Camera".
|
||||
*
|
||||
* Il ne touche pas la caméra lui-même, il expose deux décalages que
|
||||
* ApplyCameraTransform additionne — cette fonction reste le seul écrivain.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UHeadBobComponent> HeadBob;
|
||||
|
||||
/** Détecte et déclenche les interactions avec le monde. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "HeadBobComponent.generated.h"
|
||||
|
||||
class ACharacter;
|
||||
class UCharacterMovementComponent;
|
||||
|
||||
/**
|
||||
* Les reglages d'un etat de deplacement : immobile, marche, sprint, accroupi, en l'air.
|
||||
*
|
||||
* Regroupes dans une struct plutot qu'a plat sur le composant pour deux raisons.
|
||||
* D'abord le Details panel : cinq etats x cinq valeurs a plat feraient vingt-cinq
|
||||
* lignes melangees, alors que la struct donne un repli par etat. Ensuite le code :
|
||||
* passer d'un etat a l'autre est une interpolation champ a champ, et une struct
|
||||
* permet de l'ecrire une seule fois au lieu d'une par valeur.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHeadBobProfile
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FHeadBobProfile() = default;
|
||||
|
||||
FHeadBobProfile(float InStrideFrequency, float InVerticalAmount, float InLateralAmount, float InRollAmount, float InPitchAmount)
|
||||
: StrideFrequency(InStrideFrequency)
|
||||
, VerticalAmount(InVerticalAmount)
|
||||
, LateralAmount(InLateralAmount)
|
||||
, RollAmount(InRollAmount)
|
||||
, PitchAmount(InPitchAmount)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Foulees par seconde. Une foulee compte DEUX pas : l'oscillation verticale
|
||||
* bat donc au double de cette valeur, une bosse par pied qui touche le sol.
|
||||
* A 1.1, les pas tombent a 2.2 Hz, ce qui correspond a une marche normale.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "4"))
|
||||
float StrideFrequency = 1.1f;
|
||||
|
||||
/** Amplitude verticale en cm, mesuree du repos au sommet (le debattement total vaut le double). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
|
||||
float VerticalAmount = 1.6f;
|
||||
|
||||
/** Amplitude laterale en cm : le balancement gauche-droite du buste. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
|
||||
float LateralAmount = 1.2f;
|
||||
|
||||
/** Roulis en degres au pic du balancement lateral. La tete penche dans le sens du pas. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
|
||||
float RollAmount = 0.4f;
|
||||
|
||||
/**
|
||||
* Tangage en degres au pic vertical. A 0 par defaut, et ce n'est pas un oubli :
|
||||
* le tangage deplace le reticule, donc il degrade la visee. A n'ouvrir que si
|
||||
* l'on assume un jeu ou l'on ne vise pas au pixel.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
|
||||
float PitchAmount = 0.f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Balancement de tete procedural, sans la moindre animation authoree.
|
||||
*
|
||||
* Le principe tient en une phase unique, avancee chaque frame par la frequence de
|
||||
* l'etat courant, et une amplitude qui glisse d'un etat a l'autre. C'est ce qui
|
||||
* rend le passage marche -> sprint continu : ce n'est pas une animation qu'on
|
||||
* coupe pour en lancer une autre, ce sont deux jeux de coefficients entre lesquels
|
||||
* on interpole pendant que la phase, elle, ne s'interrompt jamais. Remettre la
|
||||
* phase a zero a chaque changement d'etat produirait exactement le "clac" qu'on
|
||||
* cherche a eviter.
|
||||
*
|
||||
* Le composant ne touche PAS a la camera. Il expose un decalage de position et un
|
||||
* decalage de rotation, que AFpsPlayer::ApplyCameraTransform additionne aux siens.
|
||||
* C'est la condition pour que cette fonction reste le seul endroit du projet qui
|
||||
* ecrit la transform de la camera -- elle est rappelee depuis CalcCamera, et deux
|
||||
* ecrivains se marcheraient dessus une frame sur deux.
|
||||
*
|
||||
* Rien n'est replique et rien n'a a l'etre : le balancement n'existe que dans les
|
||||
* yeux du joueur local. Les bras suivent gratuitement, puisqu'ils sont attaches a
|
||||
* la camera.
|
||||
*/
|
||||
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||
class SURVIVAL_PROJET_API UHeadBobComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UHeadBobComponent();
|
||||
|
||||
/**
|
||||
* Fait avancer la phase et recalcule les decalages.
|
||||
*
|
||||
* Appelee par le pawn depuis son Tick, et surtout PAS depuis un TickComponent a
|
||||
* lui : l'ordre de tick entre un acteur et ses composants n'est pas garanti sans
|
||||
* dependance explicite, et le resultat serait un decalage lu une frame apres
|
||||
* avoir ete calcule -- invisible a 120 fps, franchement mou a 30.
|
||||
*/
|
||||
void UpdateBob(float DeltaTime);
|
||||
|
||||
/** Decalage a ajouter a la position RELATIVE de la camera (X avant, Y droite, Z haut), en cm. */
|
||||
UFUNCTION(BlueprintPure, Category = "Head Bob")
|
||||
FVector GetLocationOffset() const { return LocationOffset; }
|
||||
|
||||
/** Decalage a ajouter a la rotation de la camera, en degres. */
|
||||
UFUNCTION(BlueprintPure, Category = "Head Bob")
|
||||
FRotator GetRotationOffset() const { return RotationOffset; }
|
||||
|
||||
/**
|
||||
* Remet le balancement au repos sans transition.
|
||||
* Pour une teleportation ou une prise de controle : on ne veut pas que la tete
|
||||
* finisse le pas d'avant a l'autre bout de la carte.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Head Bob")
|
||||
void ResetBob();
|
||||
|
||||
/**
|
||||
* Facteur global pousse par les reglages joueur (curseur "secousses de camera").
|
||||
* A 0 la tete ne bouge plus du tout : c'est un confort d'accessibilite, pas une
|
||||
* option cosmetique, et il doit valoir pour le balancement comme pour les chocs.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Head Bob")
|
||||
void SetIntensityScale(float NewScale);
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reglages generaux
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Coupe entierement l'effet, sans avoir a remettre les cinq profils a zero. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob")
|
||||
bool bEnableHeadBob = true;
|
||||
|
||||
/**
|
||||
* Multiplicateur d'amplitude commun aux cinq profils.
|
||||
* C'est le curseur a bouger pour un reglage d'ensemble ("un peu moins de tout")
|
||||
* sans avoir a retoucher vingt valeurs une par une.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "3"))
|
||||
float GlobalIntensity = 1.f;
|
||||
|
||||
/**
|
||||
* Vitesse de fondu d'un profil vers un autre (facteur d'interpolation
|
||||
* exponentielle, par seconde). Plus haut = le sprint prend son amplitude plus
|
||||
* vite. Trop haut, le changement d'etat redevient une cassure.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0.1", UIMax = "20"))
|
||||
float ProfileBlendSpeed = 6.f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Seuils de choix d'etat
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* En dessous de cette vitesse horizontale (cm/s), on est considere immobile.
|
||||
* Le seuil se lit sur la vitesse REELLE et non sur la touche enfoncee : marcher
|
||||
* dans un mur laisse l'input actif alors que le corps ne bouge plus, et sans ce
|
||||
* detail la tete continuerait de gigoter face a la cloison.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
|
||||
float IdleSpeedThreshold = 10.f;
|
||||
|
||||
/**
|
||||
* Au-dessus de cette vitesse horizontale (cm/s), on bascule sur le profil de
|
||||
* sprint. A regler entre WalkSpeed et SprintSpeed du pawn -- 400 et 650 par
|
||||
* defaut, d'ou ces 520 : assez haut pour que la phase d'acceleration ne compte
|
||||
* pas comme du sprint, assez bas pour que le sprint reel soit toujours detecte.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
|
||||
float SprintSpeedThreshold = 520.f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Profils, un par etat. Valeurs par defaut dans le constructeur.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Immobile : une respiration lente, quasi purement verticale. C'est ce qui evite la statue. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||
FHeadBobProfile IdleProfile;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||
FHeadBobProfile WalkProfile;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||
FHeadBobProfile SprintProfile;
|
||||
|
||||
/** Accroupi : plus lent et plus discret que la marche, le corps est ramasse. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||
FHeadBobProfile CrouchProfile;
|
||||
|
||||
/**
|
||||
* En l'air : tout a zero, y compris la frequence -- la phase se fige donc en vol
|
||||
* et repart d'ou elle en etait a la reception, ce qui evite que le premier pas
|
||||
* apres un saut tombe sur un pied arbitraire. Le profil reste editable malgre
|
||||
* tout, au cas ou l'on veuille un flottement pendant la chute.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||
FHeadBobProfile AirProfile;
|
||||
|
||||
private:
|
||||
/** Choisit le profil vise a partir de l'etat reel du CharacterMovement. */
|
||||
const FHeadBobProfile& SelectTargetProfile() const;
|
||||
|
||||
/** Rapproche BlendedProfile de Target, champ par champ. */
|
||||
void BlendTowards(const FHeadBobProfile& Target, float Step);
|
||||
|
||||
/**
|
||||
* Position dans le cycle de foulee, en radians, ramenee dans [0, 2*PI[.
|
||||
* Le repli n'est pas cosmetique : sans lui la valeur grossit indefiniment et la
|
||||
* precision du float se degrade, jusqu'a un balancement qui devient saccade au
|
||||
* bout d'une longue partie.
|
||||
*/
|
||||
float StridePhase = 0.f;
|
||||
|
||||
/** Profil courant, resultat du fondu entre les profils d'etat. */
|
||||
FHeadBobProfile BlendedProfile;
|
||||
|
||||
FVector LocationOffset = FVector::ZeroVector;
|
||||
|
||||
FRotator RotationOffset = FRotator::ZeroRotator;
|
||||
|
||||
/** Pousse par les reglages joueur, se multiplie a GlobalIntensity. */
|
||||
float IntensityScale = 1.f;
|
||||
|
||||
/** Caches : ces deux pointeurs sont lus a chaque frame, pas question d'y caster. */
|
||||
TObjectPtr<ACharacter> OwnerCharacter;
|
||||
|
||||
TObjectPtr<UCharacterMovementComponent> OwnerMovement;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InputCoreTypes.h"
|
||||
|
||||
/**
|
||||
* Traduction d'une touche de reference vers la disposition clavier du systeme.
|
||||
*
|
||||
* LE PROBLEME. Un IMC stocke des FKey -- EKeys::W --, c'est-a-dire des LETTRES.
|
||||
* Or Windows livre au moteur un virtual key code deja traduit par la disposition
|
||||
* active : sur un clavier AZERTY, la touche en haut a gauche du bloc de
|
||||
* deplacement envoie VK_Z, qu'Unreal lit donc comme EKeys::Z. Le mapping "W" ne
|
||||
* se declenche jamais, et le joueur pousse son personnage en arriere en croyant
|
||||
* avancer. Rien de tout ca n'est visible en developpant sur un clavier QWERTY.
|
||||
*
|
||||
* LA REPONSE. On ne raisonne plus en lettres mais en POSITIONS. Chaque touche
|
||||
* par defaut de l'IMC est lue comme une position physique sur un clavier QWERTY
|
||||
* US -- c'est le referentiel dans lequel les IMC sont ecrits -- et l'on demande
|
||||
* a l'OS quelle touche occupe cette position sur la disposition courante. La
|
||||
* geometrie de la main est preservee : le bloc WASD devient ZQSD en AZERTY,
|
||||
* reste WASD en QWERTZ (seuls Y et Z y sont echanges), et suit les positions en
|
||||
* Dvorak ou Colemak. Aucune table par pays a maintenir, et les dispositions
|
||||
* qu'on n'a jamais vues sont traitees comme les autres.
|
||||
*
|
||||
* PORTEE. Seule la zone alphanumerique bouge d'une disposition a l'autre. Les
|
||||
* modificateurs, la barre d'espace, Echap, les fonctions, les fleches, le pave
|
||||
* numerique et la manette occupent la meme position partout : ils ne figurent
|
||||
* pas dans la table de reference et ressortent donc inchanges. C'est aussi ce
|
||||
* qui garantit qu'on ne casse rien -- une touche inconnue est rendue telle quelle.
|
||||
*
|
||||
* WINDOWS UNIQUEMENT pour l'instant. Sur les autres plateformes la touche est
|
||||
* rendue inchangee, soit exactement le comportement d'avant.
|
||||
*/
|
||||
namespace KeyboardLayout
|
||||
{
|
||||
/**
|
||||
* Rend la touche qui occupe, sur la disposition active, la position que
|
||||
* ReferenceKey occupe sur un clavier QWERTY US.
|
||||
*
|
||||
* Rend ReferenceKey inchangee si la touche n'est pas concernee par la
|
||||
* disposition (espace, modificateurs, manette...), si la plateforme n'est pas
|
||||
* geree, ou si l'OS ne sait pas repondre. Jamais de touche invalide en sortie.
|
||||
*/
|
||||
SURVIVAL_PROJET_API FKey RemapKeyToActiveLayout(const FKey& ReferenceKey);
|
||||
|
||||
/**
|
||||
* L'identifiant de la disposition active, pour le journal : "0000040C" pour
|
||||
* le francais, "00000409" pour l'anglais US. Sert au diagnostic quand un
|
||||
* testeur signale des touches inattendues -- c'est la premiere chose a lire.
|
||||
*/
|
||||
SURVIVAL_PROJET_API FString GetActiveLayoutId();
|
||||
}
|
||||
@@ -114,7 +114,15 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
|
||||
|
||||
/** Volume joueur, 0 a 1. A brancher sur l'onglet Audio des reglages plus tard. */
|
||||
/**
|
||||
* Attenuation propre au LECTEUR, 0 a 1. Ce n'est PAS le reglage du joueur.
|
||||
*
|
||||
* Le volume choisi dans les reglages passe par la SoundClass de musique (voir
|
||||
* USurvivalAudioSettings), pas par ici : deux chemins vers le meme volume
|
||||
* finiraient par se multiplier sans que personne ne s'en souvienne. Celui-ci
|
||||
* reste disponible pour ce qui est de l'ordre du gameplay -- baisser la musique
|
||||
* pendant un dialogue, par exemple.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void SetMusicVolume(float NewVolume);
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SettingsTabWidget.h"
|
||||
#include "SettingsAudioWidget.generated.h"
|
||||
|
||||
class USettingRowWidget;
|
||||
|
||||
/**
|
||||
* L'onglet "Audio" : quatre volumes et le son en arriere-plan.
|
||||
*
|
||||
* Meme forme que USettingsGameplayWidget -- l'onglet ne stocke rien, chaque ligne
|
||||
* ecrit dans USurvivalUserSettings puis demande l'application immediate. La
|
||||
* difference tient au verbe appele : ApplyAudioSettings et non
|
||||
* ApplySurvivalSettings, parce que les volumes n'interessent pas le pawn et que
|
||||
* rediffuser aux widgets a chaque cran de curseur ne servirait a rien.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_SettingsAudio qui en herite, avec un
|
||||
* WBP_SettingRow par ligne, nomme exactement comme le BindWidget correspondant.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingsAudioWidget : public USettingsTabWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
virtual void RefreshFromSettings() override;
|
||||
virtual void ResetToDefaults() override;
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> MasterVolumeRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> MusicVolumeRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> EffectsVolumeRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> UiVolumeRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> BackgroundSoundRow;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleMasterVolumeChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleMusicVolumeChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleEffectsVolumeChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleUiVolumeChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleBackgroundSoundChanged(int32 NewIndex);
|
||||
};
|
||||
@@ -124,8 +124,7 @@ protected:
|
||||
TObjectPtr<UButton> ResetButton;
|
||||
|
||||
/**
|
||||
* Les trois onglets qui existent sont OBLIGATOIRES : un WBP_Settings sans eux
|
||||
* ne compile pas.
|
||||
* Les cinq onglets sont OBLIGATOIRES : un WBP_Settings sans eux ne compile pas.
|
||||
*
|
||||
* BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un
|
||||
* bouton parfaitement inerte, sans le moindre message. Pour un element dont
|
||||
@@ -143,12 +142,7 @@ protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> ControlsTabButton;
|
||||
|
||||
/**
|
||||
* Celui-la reste optionnel : sa page n'existe pas encore. Il passera en
|
||||
* BindWidget le jour ou l'onglet Audio sera ecrit -- en attendant, il se
|
||||
* grise tout seul si tu le poses.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> AudioTabButton;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
// Sound/SoundClass.h en entier, et pas une simple declaration anticipee :
|
||||
// TStrongObjectPtr exige un type COMPLET, la ou TObjectPtr s'en passait.
|
||||
#include "Sound/SoundClass.h"
|
||||
#include "UObject/StrongObjectPtr.h"
|
||||
#include "SurvivalAudioSettings.generated.h"
|
||||
|
||||
/**
|
||||
* Ou le C++ trouve les SoundClass du jeu. Reglable dans Project Settings, section
|
||||
* "Survival Audio".
|
||||
*
|
||||
* UDeveloperSettings et pas des chemins codes en dur dans le .cpp : renommer ou
|
||||
* deplacer SC_Music casserait silencieusement le curseur Musique, sans la moindre
|
||||
* erreur de compilation. Ici la reference est un asset comme un autre, suivie par
|
||||
* le redirecteur de l'editeur et par le cooker.
|
||||
*
|
||||
* Ces references sont SOFT : un UDeveloperSettings est construit tres tot, bien
|
||||
* avant que le gestionnaire d'assets ne soit pret. Une reference dure forcerait le
|
||||
* chargement des SoundClass au demarrage du moteur, ce qui est exactement le genre
|
||||
* de dependance qui fait echouer un cook.
|
||||
*
|
||||
* A NOTER : le SoundMix, lui, n'est PAS ici. Il est deja declare dans
|
||||
* Project Settings > Audio > Default Base Sound Mix, d'ou le moteur le reactive a
|
||||
* chaque chargement de map -- c'est precisement ce qu'on veut, et le redeclarer ici
|
||||
* ouvrirait la porte a deux references qui divergent.
|
||||
*/
|
||||
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Survival Audio"))
|
||||
class SURVIVAL_PROJET_API USurvivalAudioSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Range la page sous "Game" et non sous "Plugins". */
|
||||
virtual FName GetCategoryName() const override;
|
||||
|
||||
/**
|
||||
* Le CDO, seul exemplaire qui existe.
|
||||
*
|
||||
* Non const alors que rien n'est modifie du point de vue du joueur : les
|
||||
* accesseurs ci-dessous memorisent l'asset resolu dans un champ Transient, comme
|
||||
* le fait UAudioSettings pour sa propre SoundClass par defaut.
|
||||
*/
|
||||
static USurvivalAudioSettings* Get();
|
||||
|
||||
/**
|
||||
* Le sommet de la hierarchie : toutes les autres classes en descendent, donc son
|
||||
* volume les multiplie toutes.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||
TSoftObjectPtr<USoundClass> MasterSoundClass;
|
||||
|
||||
/**
|
||||
* Imposee au lecteur de musique par UMusicSubsystem, donc les morceaux eux-memes
|
||||
* n'ont rien a declarer -- ajouter une piste a une playlist suffit.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||
TSoftObjectPtr<USoundClass> MusicSoundClass;
|
||||
|
||||
/**
|
||||
* A declarer aussi comme Default Sound Class dans Project Settings > Audio :
|
||||
* tout son qui n'annonce pas sa classe atterrit alors ici, ce qui evite qu'un
|
||||
* bruitage oublie echappe au curseur Effets.
|
||||
*/
|
||||
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||
TSoftObjectPtr<USoundClass> EffectsSoundClass;
|
||||
|
||||
/** A poser a la main sur les sons d'interface (survol, validation). */
|
||||
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||
TSoftObjectPtr<USoundClass> UiSoundClass;
|
||||
|
||||
USoundClass* GetMasterSoundClass();
|
||||
USoundClass* GetMusicSoundClass();
|
||||
USoundClass* GetEffectsSoundClass();
|
||||
USoundClass* GetUiSoundClass();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Charge au premier appel, puis rend le pointeur memorise.
|
||||
*
|
||||
* Le cache n'est pas qu'une optimisation : il fait aussi office d'ancre pour le
|
||||
* ramasse-miettes. Une SoundClass chargee par un LoadSynchronous et referencee
|
||||
* nulle part serait collectee, et le curseur cesserait d'agir en pleine partie.
|
||||
*/
|
||||
USoundClass* Resolve(const TSoftObjectPtr<USoundClass>& Asset, TStrongObjectPtr<USoundClass>& Cache);
|
||||
|
||||
/**
|
||||
* TStrongObjectPtr et surtout PAS UPROPERTY(Transient) : c'est ce qui plantait
|
||||
* le jeu package au demarrage, alors que tout allait bien dans l'editeur.
|
||||
*
|
||||
* Le CDO d'un UDeveloperSettings naît avant CloseDisregardForGC, donc il vit dans
|
||||
* le pool des objets PERMANENTS. Le moteur ne les scanne jamais -- ils sont
|
||||
* immortels par construction -- et exige en echange qu'ils ne referencent que des
|
||||
* objets du root set, sans quoi ils garderaient un pointeur sur un objet collecte.
|
||||
* VerifyGCAssumptions le controle et c'est une erreur FATALE :
|
||||
*
|
||||
* Disregard for GC object Default__SurvivalAudioSettings referencing SoundClass
|
||||
* SC_Master which is not part of root set. Property: CachedMasterSoundClass
|
||||
* Fatal error: Encountered 3 object(s) breaking Disregard for GC assumptions.
|
||||
*
|
||||
* Invisible dans l'editeur, ou les SoundClass sont deja chargees quand le pool se
|
||||
* ferme, donc permanentes elles aussi. En jeu package elles arrivent apres, et le
|
||||
* jeu meurt au premier ramassage -- pendant le chargement de la premiere map.
|
||||
*
|
||||
* TStrongObjectPtr ancre l'asset exactement comme le faisait la UPROPERTY, mais
|
||||
* range sa reference dans GGCObjectReferencer, le seul objet que VerifyGCAssumptions
|
||||
* saute explicitement. Le CDO ne reference donc plus rien, et l'asset reste vivant.
|
||||
*/
|
||||
TStrongObjectPtr<USoundClass> CachedMasterSoundClass;
|
||||
|
||||
TStrongObjectPtr<USoundClass> CachedMusicSoundClass;
|
||||
|
||||
TStrongObjectPtr<USoundClass> CachedEffectsSoundClass;
|
||||
|
||||
TStrongObjectPtr<USoundClass> CachedUiSoundClass;
|
||||
};
|
||||
@@ -51,6 +51,31 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void RegisterMappableContexts();
|
||||
|
||||
/**
|
||||
* Traduit les touches par defaut vers la disposition clavier du joueur.
|
||||
*
|
||||
* Les IMC sont ecrits en QWERTY US -- EKeys::W pour avancer -- et Windows livre
|
||||
* au moteur une touche DEJA traduite par la disposition : en AZERTY, la touche
|
||||
* du haut du bloc de deplacement arrive comme EKeys::Z et le mapping "W" ne se
|
||||
* declenche jamais. On repose donc chaque touche a la position equivalente.
|
||||
* Le detail du mecanisme est dans KeyboardLayout.h.
|
||||
*
|
||||
* Ne touche QUE les mappings que le joueur n'a pas remappes lui-meme : un
|
||||
* choix explicite l'emporte toujours sur une deduction. Et sur un clavier
|
||||
* QWERTY la conversion rend la touche d'origine, donc rien n'est ecrit du tout
|
||||
* et le profil reste vierge.
|
||||
*
|
||||
* Declenchee UNE FOIS par RegisterMappableContexts(), au premier enregistrement
|
||||
* des contextes ; ensuite seul "Reinitialiser" la redemande. Elle appartient a
|
||||
* l'initialisation, pas au rafraichissement d'ecran -- l'onglet Touches se
|
||||
* rafraichit apres chaque remappage, et repasser dessus reprendrait sa touche
|
||||
* au joueur qui aurait justement choisi celle de l'IMC (la lettre W sur un
|
||||
* AZERTY, par exemple : identique au defaut, donc indiscernable d'un mapping
|
||||
* jamais touche).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void ApplyKeyboardLayoutToDefaultKeys();
|
||||
|
||||
/**
|
||||
* Un depart VOULU vers le menu commence : le filet de secours reseau doit
|
||||
* se taire.
|
||||
@@ -208,6 +233,16 @@ private:
|
||||
/** Un retour au menu est deja en cours, voulu ou non. */
|
||||
bool bReturningToMainMenu = false;
|
||||
|
||||
/**
|
||||
* La disposition clavier a deja ete appliquee dans cette session.
|
||||
*
|
||||
* Pas de UPROPERTY(config) : ce n'est PAS "l'a-t-on deja fait un jour". Les
|
||||
* surcharges posees, elles, sont persistees par Enhanced Input, et elles
|
||||
* suffisent a rendre l'operation sans effet au lancement suivant. Ce drapeau
|
||||
* ne protege que du re-declenchement en cours de session.
|
||||
*/
|
||||
bool bKeyboardLayoutApplied = false;
|
||||
|
||||
/**
|
||||
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
|
||||
* supprime une camera dans la map, et on retomberait sur autre chose.
|
||||
|
||||
@@ -74,6 +74,23 @@ namespace SurvivalSettingsRange
|
||||
*/
|
||||
constexpr float LookMultiplierAtMin = 0.05f;
|
||||
constexpr float LookMultiplierAtMax = 1.2f;
|
||||
|
||||
/** Les quatre volumes partagent la meme echelle : 0 a 100 %. */
|
||||
constexpr float VolumeMin = 0.f;
|
||||
constexpr float VolumeMax = 100.f;
|
||||
constexpr float VolumeStep = 1.f;
|
||||
constexpr float VolumeArrowStep = 10.f;
|
||||
|
||||
/**
|
||||
* Exposant applique au pourcentage avant d'en faire une amplitude.
|
||||
*
|
||||
* L'oreille repond au logarithme de l'amplitude, pas a l'amplitude : un
|
||||
* curseur envoye tel quel donne un reglage inutilisable, ou tout se joue dans
|
||||
* les vingt derniers pourcents et ou la moitie basse ne s'entend deja plus.
|
||||
* Elever au carre est l'approximation habituelle -- 50 % rendent 0,25 en
|
||||
* amplitude, soit environ la moitie du volume PERCU. 0 reste le silence exact.
|
||||
*/
|
||||
constexpr float VolumeCurveExponent = 2.f;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,10 +145,25 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void ApplySurvivalSettings();
|
||||
|
||||
/**
|
||||
* Pousse les volumes sur le peripherique audio.
|
||||
*
|
||||
* Volontairement SEPAREE d'ApplySurvivalSettings : cinq commandes vers le
|
||||
* thread audio a chaque clic de fleche sur le champ de vision seraient du
|
||||
* bruit pur. Les deux sont appelees ensemble par ApplyNonResolutionSettings,
|
||||
* donc le chemin de demarrage n'en oublie aucune.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void ApplyAudioSettings();
|
||||
|
||||
/** Remet les seuls reglages de jeu a leur valeur d'usine. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetSurvivalGameplayToDefaults();
|
||||
|
||||
/** Remet a l'usine les seuls reglages de l'onglet Audio. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetSurvivalAudioToDefaults();
|
||||
|
||||
virtual void SetToDefaults() override;
|
||||
virtual void ApplyNonResolutionSettings() override;
|
||||
|
||||
@@ -215,6 +247,45 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
|
||||
void SetSurvivalDisplayToDefaults();
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Audio
|
||||
//
|
||||
// Les quatre volumes sont des POURCENTAGES, comme le bouge de camera : c'est
|
||||
// ce que la ligne de reglage affiche, et la conversion en amplitude (courbe
|
||||
// comprise) n'appartient qu'a ApplyAudioSettings.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||
float GetMasterVolume() const { return MasterVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetMasterVolume(float Value);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||
float GetMusicVolume() const { return MusicVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetMusicVolume(float Value);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||
float GetEffectsVolume() const { return EffectsVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetEffectsVolume(float Value);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||
float GetUiVolume() const { return UiVolume; }
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetUiVolume(float Value);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||
bool GetPlayInBackground() const { return bPlayInBackground; }
|
||||
|
||||
/** Vrai = le jeu continue de s'entendre quand la fenetre n'a plus le focus. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||
void SetPlayInBackground(bool bValue);
|
||||
|
||||
private:
|
||||
/** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */
|
||||
UPROPERTY(config)
|
||||
@@ -245,4 +316,24 @@ private:
|
||||
/** Pourcentage, converti en gamma a l'application. */
|
||||
UPROPERTY(config)
|
||||
float Brightness;
|
||||
|
||||
/** Pourcentages. Le maitre multiplie les trois autres via la hierarchie des SoundClass. */
|
||||
UPROPERTY(config)
|
||||
float MasterVolume;
|
||||
|
||||
UPROPERTY(config)
|
||||
float MusicVolume;
|
||||
|
||||
UPROPERTY(config)
|
||||
float EffectsVolume;
|
||||
|
||||
UPROPERTY(config)
|
||||
float UiVolume;
|
||||
|
||||
/**
|
||||
* Le moteur coupe le son des qu'il perd le focus, et c'est son defaut d'usine.
|
||||
* En coop c'est justement le mauvais defaut pour qui bascule sur Discord.
|
||||
*/
|
||||
UPROPERTY(config)
|
||||
bool bPlayInBackground;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user