(Feat) Add Animation
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "FpsArmsAnimInstance.h"
|
||||
|
||||
#include "FpsPlayer.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
|
||||
void UFpsArmsAnimInstance::NativeInitializeAnimation()
|
||||
{
|
||||
Super::NativeInitializeAnimation();
|
||||
|
||||
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
|
||||
}
|
||||
|
||||
void UFpsArmsAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
|
||||
{
|
||||
Super::NativeUpdateAnimation(DeltaSeconds);
|
||||
|
||||
// L'AnimInstance est initialisee avec le composant, c'est-a-dire potentiellement
|
||||
// avant que le pawn soit completement en place. On retente donc tant qu'on n'a
|
||||
// personne, plutot que d'abandonner definitivement sur un echec au demarrage.
|
||||
if (!OwningPlayer.IsValid())
|
||||
{
|
||||
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
|
||||
if (!OwningPlayer.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const AFpsPlayer* Player = OwningPlayer.Get();
|
||||
const UCharacterMovementComponent* Movement = Player->GetCharacterMovement();
|
||||
if (!Movement)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Size2D et pas Size : en pleine chute la composante verticale ferait courir
|
||||
// les bras alors que le joueur ne pose pas un pied.
|
||||
GroundSpeed = Movement->Velocity.Size2D();
|
||||
|
||||
// Le test d'acceleration est ce qui distingue « le joueur avance » de « le
|
||||
// joueur a lache la touche et glisse encore ». Sans lui, l'animation de course
|
||||
// continue pendant toute la decceleration, les pieds patinant dans le vide --
|
||||
// invisible sur des bras, mais le meme AnimInstance servira au corps un jour.
|
||||
bIsMoving = GroundSpeed > MovingSpeedThreshold
|
||||
&& !Movement->GetCurrentAcceleration().IsNearlyZero();
|
||||
|
||||
bIsSprinting = Player->IsSprinting();
|
||||
bIsFalling = Movement->IsFalling();
|
||||
bIsCrouching = Movement->IsCrouching();
|
||||
|
||||
// La cadence suit la vitesse REELLE et pas l'etat de sprint : c'est ce qui
|
||||
// fait que les bras accelerent pendant la montee en vitesse, au lieu de
|
||||
// sauter d'une cadence a l'autre a l'instant ou la touche est pressee.
|
||||
// Elle couvre donc aussi l'accroupi, gratuitement.
|
||||
LocomotionPlayRate = FMath::Clamp(
|
||||
GroundSpeed / ReferenceLocomotionSpeed,
|
||||
MinLocomotionPlayRate,
|
||||
MaxLocomotionPlayRate);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include "FpsPlayer.h"
|
||||
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Camera/CameraTypes.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
@@ -14,11 +16,13 @@
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "CraftingComponent.h"
|
||||
#include "HeadBobComponent.h"
|
||||
#include "InteractionComponent.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemDataAsset.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "PickupItem.h"
|
||||
#include "SurvivalGameInstance.h"
|
||||
#include "SurvivalStatsComponent.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
@@ -74,6 +78,31 @@ AFpsPlayer::AFpsPlayer()
|
||||
// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
|
||||
FirstPersonCamera->bUsePawnControlRotation = false;
|
||||
|
||||
// Bras vue premiere personne, accroches a la camera pour suivre le regard.
|
||||
FirstPersonArms = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonArms"));
|
||||
FirstPersonArms->SetupAttachment(FirstPersonCamera);
|
||||
// Symetrique exact du mesh 3e personne ci-dessous : chacun ne voit que ses
|
||||
// propres bras. Sans ca, les bras d'un ami flotteraient devant notre objectif.
|
||||
FirstPersonArms->SetOnlyOwnerSee(true);
|
||||
// Un mesh colle a l'objectif projetterait une ombre enorme en travers du
|
||||
// decor. L'ombre du joueur, elle, vient du mesh 3e personne.
|
||||
FirstPersonArms->SetCastShadow(false);
|
||||
FirstPersonArms->bCastDynamicShadow = false;
|
||||
// Les bras ne sont pas dans le monde, ils sont devant l'objectif : une
|
||||
// collision les ferait pousser des objets et surtout bloquer le trace
|
||||
// d'interaction, qui part exactement d'entre eux.
|
||||
FirstPersonArms->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
// Chez l'hote, le pawn de chaque ami porte AUSSI ce composant, invisible pour
|
||||
// tout le monde. Sans cette option le serveur evaluerait quatre AnimBlueprints
|
||||
// pour quatre meshes que personne ne rend.
|
||||
FirstPersonArms->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;
|
||||
// Point de depart a affiner dans BP_FpsPlayer : la bonne valeur depend de
|
||||
// l'origine du FBX (racine aux pieds ? au bassin ?) et de l'orientation
|
||||
// d'export. Le yaw de -90 est le decalage habituel d'un FBX Blender/Maya.
|
||||
FirstPersonArms->SetRelativeLocationAndRotation(FVector(0.f, 0.f, -160.f), FRotator(0.f, -90.f, 0.f));
|
||||
|
||||
HeadBob = CreateDefaultSubobject<UHeadBobComponent>(TEXT("HeadBob"));
|
||||
|
||||
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
|
||||
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
|
||||
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
|
||||
@@ -131,10 +160,18 @@ void AFpsPlayer::BeginPlay()
|
||||
{
|
||||
if (DefaultMappingContext)
|
||||
{
|
||||
// On ACTIVE le contexte ici, mais on ne le DECLARE pas comme
|
||||
// remappable : c'est USurvivalGameInstance qui s'en charge, parce
|
||||
// que le menu principal n'a pas de pawn et doit quand meme
|
||||
// afficher les touches.
|
||||
// La DECLARATION des contextes remappables appartient au
|
||||
// GameInstance -- le menu principal n'a pas de pawn et doit
|
||||
// pourtant afficher les touches. On la demande quand meme ici, et
|
||||
// AVANT d'activer le contexte : c'est elle qui traduit les touches
|
||||
// vers la disposition clavier du joueur, et un AZERTY doit trouver
|
||||
// ses touches des la premiere image, pas au premier passage dans
|
||||
// l'ecran de reglages. L'appel est idempotent.
|
||||
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||
{
|
||||
GameInstance->RegisterMappableContexts();
|
||||
}
|
||||
|
||||
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
||||
}
|
||||
else
|
||||
@@ -176,6 +213,10 @@ void AFpsPlayer::ApplyUserSettings()
|
||||
bSprintToggleMode = Settings->GetSprintToggle();
|
||||
bCrouchToggleMode = Settings->GetCrouchToggle();
|
||||
|
||||
// Le curseur "secousses de camera" vaut aussi pour le balancement : un joueur
|
||||
// qui le met a zero demande une camera fixe, pas une camera fixe qui respire.
|
||||
HeadBob->SetIntensityScale(CameraShakeMultiplier);
|
||||
|
||||
// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
|
||||
// personnage bloque en sprint : on repart d'un etat propre.
|
||||
if (!bSprintToggleMode && bIsSprinting)
|
||||
@@ -228,7 +269,15 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
|
||||
CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);
|
||||
|
||||
// ==================================================================
|
||||
// 3. FOV de sprint
|
||||
// 3. Balancement de tete
|
||||
// ==================================================================
|
||||
// Integre ici et applique dans ApplyCameraTransform, exactement comme les
|
||||
// ressorts ci-dessus : c'est ce qui permet a CalcCamera de recomposer la
|
||||
// transform sans faire avancer le temps deux fois.
|
||||
HeadBob->UpdateBob(Step);
|
||||
|
||||
// ==================================================================
|
||||
// 4. FOV de sprint
|
||||
// ==================================================================
|
||||
// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
|
||||
// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
|
||||
@@ -257,7 +306,18 @@ void AFpsPlayer::ApplyCameraTransform()
|
||||
// demi-hauteur courante de la capsule, ce saut est annule exactement --
|
||||
// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
|
||||
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
|
||||
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset));
|
||||
|
||||
// Le balancement s'AJOUTE au reste. Son Y est un decalage lateral dans le
|
||||
// repere de la capsule, dont le yaw suit deja le regard (bUseControllerRotationYaw) :
|
||||
// la tete se porte donc bien a droite de ce qu'on regarde, sans conversion.
|
||||
// Le tangage, lui, n'est pas dans la capsule -- c'est voulu, le balancement
|
||||
// reste horizontal meme quand on regarde ses pieds.
|
||||
const FVector BobOffset = HeadBob->GetLocationOffset();
|
||||
|
||||
FirstPersonCamera->SetRelativeLocation(FVector(
|
||||
BobOffset.X,
|
||||
BobOffset.Y,
|
||||
EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset + BobOffset.Z));
|
||||
|
||||
// ==================================================================
|
||||
// Rotation
|
||||
@@ -274,6 +334,9 @@ void AFpsPlayer::ApplyCameraTransform()
|
||||
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
|
||||
ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm;
|
||||
|
||||
// Roulis et tangage du balancement, dans le meme sac que le reste.
|
||||
ViewOffset += HeadBob->GetRotationOffset();
|
||||
|
||||
if (const AController* OwningController = GetController())
|
||||
{
|
||||
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
|
||||
@@ -812,9 +875,35 @@ void AFpsPlayer::StopCrouch()
|
||||
|
||||
void AFpsPlayer::Interact()
|
||||
{
|
||||
// Le geste part AVANT la RPC, pas apres sa reponse : un montage declenche a
|
||||
// l'aller-retour serveur donnerait des bras qui repondent 60 ms apres le clic,
|
||||
// et le joueur lirait ca comme un jeu qui rame. Le prix a payer est un geste
|
||||
// dans le vide si le serveur refuse l'interaction -- infiniment moins genant.
|
||||
//
|
||||
// Le test de type est ici et pas dans le composant d'interaction : celui-ci ne
|
||||
// connait que l'interface IInteractable et n'a pas a apprendre ce qu'est un
|
||||
// pickup. Un coffre ou un etabli n'ont pas a declencher un geste de ramassage.
|
||||
if (Cast<APickupItem>(InteractionComponent->GetFocusedActor()))
|
||||
{
|
||||
PlayArmsMontage(GrabMontage);
|
||||
}
|
||||
|
||||
InteractionComponent->TryInteract();
|
||||
}
|
||||
|
||||
float AFpsPlayer::PlayArmsMontage(UAnimMontage* Montage, float PlayRate)
|
||||
{
|
||||
// IsLocallyControlled : chez l'hote, le pawn de chaque ami passerait aussi par
|
||||
// ici a chaque ramassage et animerait un mesh que personne ne rend.
|
||||
if (!Montage || !IsLocallyControlled())
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
UAnimInstance* ArmsAnim = FirstPersonArms->GetAnimInstance();
|
||||
return ArmsAnim ? ArmsAnim->Montage_Play(Montage, PlayRate) : 0.f;
|
||||
}
|
||||
|
||||
void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
|
||||
{
|
||||
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "HeadBobComponent.h"
|
||||
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
|
||||
UHeadBobComponent::UHeadBobComponent()
|
||||
{
|
||||
// C'est le pawn qui appelle UpdateBob depuis son propre Tick, juste avant de
|
||||
// composer la transform de la camera. Un tick a nous ne servirait qu'a
|
||||
// introduire une incertitude d'ordre.
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
// Valeurs de depart, a affiner a l'oeil dans BP_FpsPlayer. Elles sont ici et
|
||||
// non en initialiseurs de membre pour que le constructeur reste le seul
|
||||
// endroit a lire quand on cherche "combien ca bouge par defaut".
|
||||
IdleProfile = FHeadBobProfile(0.15f, 0.55f, 0.05f, 0.03f, 0.f);
|
||||
WalkProfile = FHeadBobProfile(1.1f, 1.6f, 1.2f, 0.4f, 0.f);
|
||||
SprintProfile = FHeadBobProfile(1.55f, 2.1f, 1.7f, 0.8f, 0.f);
|
||||
CrouchProfile = FHeadBobProfile(0.7f, 1.f, 0.9f, 0.3f, 0.f);
|
||||
AirProfile = FHeadBobProfile(0.f, 0.f, 0.f, 0.f, 0.f);
|
||||
|
||||
// On demarre deja sur le profil d'immobilite : partir de zeros ferait monter
|
||||
// la respiration pendant la premiere seconde de jeu, ce qui se voit.
|
||||
BlendedProfile = IdleProfile;
|
||||
}
|
||||
|
||||
void UHeadBobComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
OwnerCharacter = Cast<ACharacter>(GetOwner());
|
||||
if (!OwnerCharacter)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("UHeadBobComponent : le proprietaire n'est pas un ACharacter, le balancement de tete est desactive."));
|
||||
return;
|
||||
}
|
||||
|
||||
// GetCharacterMovement() est un simple accesseur sur un membre du Character,
|
||||
// mais le stocker evite un appel indirect par frame pour rien.
|
||||
OwnerMovement = OwnerCharacter->GetCharacterMovement();
|
||||
}
|
||||
|
||||
void UHeadBobComponent::UpdateBob(float DeltaTime)
|
||||
{
|
||||
// IsLocallyControlled : chez l'hote, le pawn de chaque ami porte AUSSI ce
|
||||
// composant. Sans cette garde, le serveur calculerait trois balancements pour
|
||||
// des cameras que personne ne regarde.
|
||||
if (!bEnableHeadBob || !OwnerCharacter || !OwnerMovement || !OwnerCharacter->IsLocallyControlled())
|
||||
{
|
||||
// Remise a zero et pas simple sortie : couper l'effet en pleine partie
|
||||
// laisserait sinon la camera figee sur le dernier decalage calcule.
|
||||
LocationOffset = FVector::ZeroVector;
|
||||
RotationOffset = FRotator::ZeroRotator;
|
||||
return;
|
||||
}
|
||||
|
||||
// Meme garde-fou que les ressorts de camera : un hoquet de framerate ferait
|
||||
// sauter la phase de plusieurs pas d'un coup.
|
||||
const float Step = FMath::Min(DeltaTime, 1.f / 30.f);
|
||||
|
||||
BlendTowards(SelectTargetProfile(), Step);
|
||||
|
||||
// La phase avance a la frequence FONDUE, pas a celle du profil vise : c'est
|
||||
// ce qui fait que le rythme des pas accelere progressivement au demarrage du
|
||||
// sprint au lieu de doubler d'une frame a l'autre.
|
||||
StridePhase = FMath::Fmod(StridePhase + BlendedProfile.StrideFrequency * 2.f * PI * Step, 2.f * PI);
|
||||
|
||||
const float Intensity = GlobalIntensity * IntensityScale;
|
||||
|
||||
// Une foulee = deux pas. Le balancement lateral suit la foulee entiere (on
|
||||
// bascule sur un pied puis sur l'autre), la montee-descente bat deux fois plus
|
||||
// vite, une bosse par pied qui touche le sol. C'est ce rapport 1:2 qui dessine
|
||||
// le fameux 8 couche et qui fait qu'on lit une marche plutot qu'un flottement.
|
||||
const float LateralWave = FMath::Sin(StridePhase);
|
||||
const float VerticalWave = FMath::Sin(StridePhase * 2.f);
|
||||
|
||||
// X reste nul : une composante avant-arriere donnerait un effet de zoom qui,
|
||||
// en vue premiere personne, se lit comme du mal de mer plus que comme un pas.
|
||||
LocationOffset = FVector(
|
||||
0.f,
|
||||
LateralWave * BlendedProfile.LateralAmount * Intensity,
|
||||
VerticalWave * BlendedProfile.VerticalAmount * Intensity);
|
||||
|
||||
// Le roulis suit le balancement lateral : la tete penche du cote ou le corps
|
||||
// se porte. Le tangage suit la montee-descente, et vaut 0 par defaut.
|
||||
RotationOffset = FRotator(
|
||||
VerticalWave * BlendedProfile.PitchAmount * Intensity,
|
||||
0.f,
|
||||
LateralWave * BlendedProfile.RollAmount * Intensity);
|
||||
}
|
||||
|
||||
const FHeadBobProfile& UHeadBobComponent::SelectTargetProfile() const
|
||||
{
|
||||
if (OwnerMovement->IsFalling())
|
||||
{
|
||||
return AirProfile;
|
||||
}
|
||||
|
||||
// Vitesse a plat : la composante verticale d'une pente ou d'un ascenseur n'a
|
||||
// rien a voir avec la cadence des pas.
|
||||
const float GroundSpeed = OwnerMovement->Velocity.Size2D();
|
||||
|
||||
// L'immobilite passe AVANT l'accroupissement : accroupi sans bouger, on
|
||||
// respire, on ne marche pas sur place.
|
||||
if (GroundSpeed < IdleSpeedThreshold)
|
||||
{
|
||||
return IdleProfile;
|
||||
}
|
||||
|
||||
if (OwnerCharacter->bIsCrouched)
|
||||
{
|
||||
return CrouchProfile;
|
||||
}
|
||||
|
||||
return (GroundSpeed > SprintSpeedThreshold) ? SprintProfile : WalkProfile;
|
||||
}
|
||||
|
||||
void UHeadBobComponent::BlendTowards(const FHeadBobProfile& Target, float Step)
|
||||
{
|
||||
BlendedProfile.StrideFrequency = FMath::FInterpTo(BlendedProfile.StrideFrequency, Target.StrideFrequency, Step, ProfileBlendSpeed);
|
||||
BlendedProfile.VerticalAmount = FMath::FInterpTo(BlendedProfile.VerticalAmount, Target.VerticalAmount, Step, ProfileBlendSpeed);
|
||||
BlendedProfile.LateralAmount = FMath::FInterpTo(BlendedProfile.LateralAmount, Target.LateralAmount, Step, ProfileBlendSpeed);
|
||||
BlendedProfile.RollAmount = FMath::FInterpTo(BlendedProfile.RollAmount, Target.RollAmount, Step, ProfileBlendSpeed);
|
||||
BlendedProfile.PitchAmount = FMath::FInterpTo(BlendedProfile.PitchAmount, Target.PitchAmount, Step, ProfileBlendSpeed);
|
||||
}
|
||||
|
||||
void UHeadBobComponent::ResetBob()
|
||||
{
|
||||
StridePhase = 0.f;
|
||||
BlendedProfile = IdleProfile;
|
||||
LocationOffset = FVector::ZeroVector;
|
||||
RotationOffset = FRotator::ZeroRotator;
|
||||
}
|
||||
|
||||
void UHeadBobComponent::SetIntensityScale(float NewScale)
|
||||
{
|
||||
IntensityScale = FMath::Max(NewScale, 0.f);
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "KeyboardLayout.h"
|
||||
|
||||
#if PLATFORM_WINDOWS
|
||||
// WindowsHWrapper et pas un <Windows.h> nu : il pose lui-meme les gardes
|
||||
// Pre/PostWindowsApi qui neutralisent les macros et les typedefs de l'API Win32
|
||||
// entrant en collision avec ceux du moteur (INT, FLOAT, TEXT...).
|
||||
#include "Windows/WindowsHWrapper.h"
|
||||
#endif
|
||||
|
||||
namespace KeyboardLayout
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
|
||||
/**
|
||||
* Position physique de chaque touche de reference, en scancode PS/2 set 1.
|
||||
*
|
||||
* Ces codes decrivent l'EMPLACEMENT sur le clavier, pas le caractere grave
|
||||
* dessus : c'est ce qui rend la table independante de la disposition. Ils sont
|
||||
* figes depuis le clavier IBM de 1984 et ne bougeront plus.
|
||||
*
|
||||
* Table ecrite a la main plutot que deduite d'un HKL americain charge a la
|
||||
* volee : LoadKeyboardLayout ajoute la disposition a celles du systeme, et un
|
||||
* jeu n'a pas a toucher a la configuration clavier de la machine.
|
||||
*
|
||||
* Construite au PREMIER APPEL et pas en variable globale : les constantes de
|
||||
* EKeys sont initialisees par le moteur au demarrage, et une table globale
|
||||
* pourrait naitre avant elles.
|
||||
*/
|
||||
static const TMap<FName, uint32>& GetReferenceScanCodes()
|
||||
{
|
||||
static const TMap<FName, uint32> ScanCodes = []
|
||||
{
|
||||
TMap<FName, uint32> Map;
|
||||
Map.Reserve(48);
|
||||
|
||||
// Rangee des chiffres
|
||||
Map.Add(EKeys::Tilde.GetFName(), 0x29);
|
||||
Map.Add(EKeys::One.GetFName(), 0x02);
|
||||
Map.Add(EKeys::Two.GetFName(), 0x03);
|
||||
Map.Add(EKeys::Three.GetFName(), 0x04);
|
||||
Map.Add(EKeys::Four.GetFName(), 0x05);
|
||||
Map.Add(EKeys::Five.GetFName(), 0x06);
|
||||
Map.Add(EKeys::Six.GetFName(), 0x07);
|
||||
Map.Add(EKeys::Seven.GetFName(), 0x08);
|
||||
Map.Add(EKeys::Eight.GetFName(), 0x09);
|
||||
Map.Add(EKeys::Nine.GetFName(), 0x0A);
|
||||
Map.Add(EKeys::Zero.GetFName(), 0x0B);
|
||||
Map.Add(EKeys::Hyphen.GetFName(), 0x0C);
|
||||
Map.Add(EKeys::Equals.GetFName(), 0x0D);
|
||||
|
||||
// Rangee du haut
|
||||
Map.Add(EKeys::Q.GetFName(), 0x10);
|
||||
Map.Add(EKeys::W.GetFName(), 0x11);
|
||||
Map.Add(EKeys::E.GetFName(), 0x12);
|
||||
Map.Add(EKeys::R.GetFName(), 0x13);
|
||||
Map.Add(EKeys::T.GetFName(), 0x14);
|
||||
Map.Add(EKeys::Y.GetFName(), 0x15);
|
||||
Map.Add(EKeys::U.GetFName(), 0x16);
|
||||
Map.Add(EKeys::I.GetFName(), 0x17);
|
||||
Map.Add(EKeys::O.GetFName(), 0x18);
|
||||
Map.Add(EKeys::P.GetFName(), 0x19);
|
||||
Map.Add(EKeys::LeftBracket.GetFName(), 0x1A);
|
||||
Map.Add(EKeys::RightBracket.GetFName(), 0x1B);
|
||||
Map.Add(EKeys::Backslash.GetFName(), 0x2B);
|
||||
|
||||
// Rangee de repos
|
||||
Map.Add(EKeys::A.GetFName(), 0x1E);
|
||||
Map.Add(EKeys::S.GetFName(), 0x1F);
|
||||
Map.Add(EKeys::D.GetFName(), 0x20);
|
||||
Map.Add(EKeys::F.GetFName(), 0x21);
|
||||
Map.Add(EKeys::G.GetFName(), 0x22);
|
||||
Map.Add(EKeys::H.GetFName(), 0x23);
|
||||
Map.Add(EKeys::J.GetFName(), 0x24);
|
||||
Map.Add(EKeys::K.GetFName(), 0x25);
|
||||
Map.Add(EKeys::L.GetFName(), 0x26);
|
||||
Map.Add(EKeys::Semicolon.GetFName(), 0x27);
|
||||
Map.Add(EKeys::Apostrophe.GetFName(), 0x28);
|
||||
|
||||
// Rangee du bas
|
||||
Map.Add(EKeys::Z.GetFName(), 0x2C);
|
||||
Map.Add(EKeys::X.GetFName(), 0x2D);
|
||||
Map.Add(EKeys::C.GetFName(), 0x2E);
|
||||
Map.Add(EKeys::V.GetFName(), 0x2F);
|
||||
Map.Add(EKeys::B.GetFName(), 0x30);
|
||||
Map.Add(EKeys::N.GetFName(), 0x31);
|
||||
Map.Add(EKeys::M.GetFName(), 0x32);
|
||||
Map.Add(EKeys::Comma.GetFName(), 0x33);
|
||||
Map.Add(EKeys::Period.GetFName(), 0x34);
|
||||
Map.Add(EKeys::Slash.GetFName(), 0x35);
|
||||
|
||||
return Map;
|
||||
}();
|
||||
|
||||
return ScanCodes;
|
||||
}
|
||||
|
||||
#endif // PLATFORM_WINDOWS
|
||||
|
||||
FKey RemapKeyToActiveLayout(const FKey& ReferenceKey)
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
const uint32* ScanCode = GetReferenceScanCodes().Find(ReferenceKey.GetFName());
|
||||
if (!ScanCode)
|
||||
{
|
||||
// Touche insensible a la disposition : espace, modificateur, manette.
|
||||
return ReferenceKey;
|
||||
}
|
||||
|
||||
// GetKeyboardLayout(0) = la disposition du thread appelant, donc celle que
|
||||
// le joueur a reellement active -- et pas celle par defaut du systeme, qui
|
||||
// differe des qu'il en a plusieurs et bascule au clavier.
|
||||
const HKL ActiveLayout = ::GetKeyboardLayout(0);
|
||||
|
||||
const UINT VirtualKey = ::MapVirtualKeyExW(*ScanCode, MAPVK_VSC_TO_VK_EX, ActiveLayout);
|
||||
if (VirtualKey == 0)
|
||||
{
|
||||
// La disposition n'a rien a cette position. Rare, mais possible sur les
|
||||
// dispositions non latines : on garde la touche d'origine.
|
||||
return ReferenceKey;
|
||||
}
|
||||
|
||||
// On REPRODUIT ici, a l'identique, le couple que le moteur recevra au
|
||||
// runtime : FWindowsApplication passe le virtual key ET le caractere non
|
||||
// modifie rendu par MAPVK_VK_TO_CHAR. Deduire le CharCode du virtual key
|
||||
// serait faux des que la touche ne porte pas le caractere de son code --
|
||||
// et c'est precisement le cas de la rangee des chiffres en AZERTY, ou
|
||||
// Windows garde VK_1 pour une touche qui produit "&". Le moteur y repond
|
||||
// EKeys::Ampersand ; simuler EKeys::One nous ferait conclure qu'il n'y a
|
||||
// rien a convertir, et les dix touches de la barre resteraient muettes.
|
||||
//
|
||||
// Le bit de poids fort signale une touche morte (l'accent circonflexe d'un
|
||||
// AZERTY, par exemple) : on le masque, seul le caractere nous interesse.
|
||||
const UINT CharCode = ::MapVirtualKeyExW(VirtualKey, MAPVK_VK_TO_CHAR, ActiveLayout) & 0x7FFFFFFF;
|
||||
|
||||
const FKey MappedKey = FInputKeyManager::Get().GetKeyFromCodes(VirtualKey, CharCode);
|
||||
|
||||
// Une touche introuvable dans les deux tables est SYNTHETISEE par le moteur,
|
||||
// et elle sort marquee non bindable -- l'assigner donnerait une action
|
||||
// definitivement muette. C'est le seul cas ou l'on prefere une touche fausse
|
||||
// a une touche morte.
|
||||
if (!MappedKey.IsValid() || !MappedKey.IsBindableToActions())
|
||||
{
|
||||
return ReferenceKey;
|
||||
}
|
||||
|
||||
return MappedKey;
|
||||
#else
|
||||
return ReferenceKey;
|
||||
#endif
|
||||
}
|
||||
|
||||
FString GetActiveLayoutId()
|
||||
{
|
||||
#if PLATFORM_WINDOWS
|
||||
// 16 et pas KL_NAMELENGTH : l'API en ecrit 9 au plus, et un buffer en dur
|
||||
// evite de dependre d'une macro que WIN32_LEAN_AND_MEAN pourrait retirer.
|
||||
WCHAR LayoutName[16] = {};
|
||||
if (::GetKeyboardLayoutNameW(LayoutName))
|
||||
{
|
||||
return FString(LayoutName);
|
||||
}
|
||||
|
||||
return TEXT("inconnue");
|
||||
#else
|
||||
return TEXT("non gere sur cette plateforme");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "MusicPlaylistDataAsset.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "Sound/SoundClass.h"
|
||||
#include "SurvivalAudioSettings.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
|
||||
@@ -298,6 +300,20 @@ void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
|
||||
return;
|
||||
}
|
||||
|
||||
// Un curseur de volume ne doit RIEN faire d'autre que changer le volume.
|
||||
//
|
||||
// Sans PlayWhenSilent, une source devenue inaudible -- curseur Musique a zero --
|
||||
// est declaree TERMINEE par le moteur : OnAudioFinished part, HandleTrackFinished
|
||||
// enchaîne, et le silence entre deux morceaux (MinGapBetweenTracks) demarre. Le
|
||||
// joueur qui remonte son volume n'entend alors plus rien jusqu'a la piste
|
||||
// suivante, alors qu'il n'a fait que bouger un reglage. Avec ce mode le morceau
|
||||
// continue d'avancer sans etre rendu, et redevient simplement audible.
|
||||
//
|
||||
// Pose ICI et pas sur les assets, pour la meme raison que la SoundClass dans
|
||||
// CreateMusicComponent : ajouter une piste a une playlist ne doit rien demander
|
||||
// de plus. Ecrire ce champ ne salit pas l'asset -- rien n'appelle MarkPackageDirty.
|
||||
Track->VirtualizationMode = EVirtualizationMode::PlayWhenSilent;
|
||||
|
||||
if (!MusicComponent)
|
||||
{
|
||||
MusicComponent = CreateMusicComponent(Track);
|
||||
@@ -353,6 +369,14 @@ UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// La classe est imposee ICI plutot que sur chaque asset de morceau : ajouter une
|
||||
// piste a une playlist ne doit pas demander de penser a la taguer, sinon le
|
||||
// curseur Musique cesse d'agir sur elle sans le moindre signe.
|
||||
if (USoundClass* MusicClass = USurvivalAudioSettings::Get()->GetMusicSoundClass())
|
||||
{
|
||||
Component->SoundClassOverride = MusicClass;
|
||||
}
|
||||
|
||||
Component->bIsMusic = true;
|
||||
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||
return Component;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingsAudioWidget.h"
|
||||
|
||||
#include "SettingRowWidget.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
namespace SettingsAudioTab
|
||||
{
|
||||
/** La seule liste de choix de cet onglet. */
|
||||
TArray<FText> MakeMuteAudibleLabels()
|
||||
{
|
||||
return { LOCTEXT("Muet", "Muet"), LOCTEXT("Audible", "Audible") };
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
using namespace SurvivalSettingsRange;
|
||||
using namespace SettingsAudioTab;
|
||||
|
||||
const FText PercentSuffix = LOCTEXT("Pourcent", " %");
|
||||
|
||||
if (MasterVolumeRow)
|
||||
{
|
||||
MasterVolumeRow->SetupAsNumber(LOCTEXT("VolumeGeneral", "Volume général"),
|
||||
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||
MasterVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleMasterVolumeChanged);
|
||||
}
|
||||
|
||||
if (MusicVolumeRow)
|
||||
{
|
||||
MusicVolumeRow->SetupAsNumber(LOCTEXT("Musique", "Musique"),
|
||||
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||
MusicVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleMusicVolumeChanged);
|
||||
}
|
||||
|
||||
if (EffectsVolumeRow)
|
||||
{
|
||||
// Cle "EffetsSonores" et pas "Effets" : l'onglet Graphismes utilise deja
|
||||
// cette derniere pour la qualite des effets visuels, dans le meme namespace.
|
||||
EffectsVolumeRow->SetupAsNumber(LOCTEXT("EffetsSonores", "Effets sonores"),
|
||||
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||
EffectsVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleEffectsVolumeChanged);
|
||||
}
|
||||
|
||||
if (UiVolumeRow)
|
||||
{
|
||||
UiVolumeRow->SetupAsNumber(LOCTEXT("Interface", "Interface"),
|
||||
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||
UiVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleUiVolumeChanged);
|
||||
}
|
||||
|
||||
if (BackgroundSoundRow)
|
||||
{
|
||||
BackgroundSoundRow->SetupAsChoice(LOCTEXT("SonEnArrierePlan", "Jeu en arrière-plan"),
|
||||
MakeMuteAudibleLabels(), 0);
|
||||
BackgroundSoundRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleBackgroundSoundChanged);
|
||||
}
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::RefreshFromSettings()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("USettingsAudioWidget : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, l'onglet restera inerte."));
|
||||
return;
|
||||
}
|
||||
|
||||
// SetNumericValueSilently : diffuser ici ferait boucler l'onglet sur lui-meme.
|
||||
if (MasterVolumeRow)
|
||||
{
|
||||
MasterVolumeRow->SetNumericValueSilently(Settings->GetMasterVolume());
|
||||
}
|
||||
|
||||
if (MusicVolumeRow)
|
||||
{
|
||||
MusicVolumeRow->SetNumericValueSilently(Settings->GetMusicVolume());
|
||||
}
|
||||
|
||||
if (EffectsVolumeRow)
|
||||
{
|
||||
EffectsVolumeRow->SetNumericValueSilently(Settings->GetEffectsVolume());
|
||||
}
|
||||
|
||||
if (UiVolumeRow)
|
||||
{
|
||||
UiVolumeRow->SetNumericValueSilently(Settings->GetUiVolume());
|
||||
}
|
||||
|
||||
if (BackgroundSoundRow)
|
||||
{
|
||||
BackgroundSoundRow->SetIndexSilently(Settings->GetPlayInBackground() ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::ResetToDefaults()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetSurvivalAudioToDefaults();
|
||||
Settings->ApplyAudioSettings();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::HandleMasterVolumeChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !MasterVolumeRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetMasterVolume(MasterVolumeRow->GetNumericValue());
|
||||
|
||||
// Pas de SaveSettings ici : c'est l'ecran de reglages qui ecrit le .ini en se
|
||||
// fermant. Meme regle que les autres onglets.
|
||||
Settings->ApplyAudioSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::HandleMusicVolumeChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !MusicVolumeRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetMusicVolume(MusicVolumeRow->GetNumericValue());
|
||||
Settings->ApplyAudioSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::HandleEffectsVolumeChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !EffectsVolumeRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetEffectsVolume(EffectsVolumeRow->GetNumericValue());
|
||||
Settings->ApplyAudioSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::HandleUiVolumeChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !UiVolumeRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Aucun son de test a jouer : les fleches de la ligne portent deja le son de
|
||||
// clic de l'interface, donc le joueur entend le reglage qu'il est en train de
|
||||
// faire, sur le son exact qu'il regle.
|
||||
Settings->SetUiVolume(UiVolumeRow->GetNumericValue());
|
||||
Settings->ApplyAudioSettings();
|
||||
}
|
||||
|
||||
void USettingsAudioWidget::HandleBackgroundSoundChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetPlayInBackground(NewIndex == 1);
|
||||
Settings->ApplyAudioSettings();
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -186,6 +186,16 @@ void USettingsControlsWidget::ResetToDefaults()
|
||||
}
|
||||
|
||||
Settings->ApplySettings();
|
||||
|
||||
// Reinitialiser rend les touches de l'IMC, donc du QWERTY : sur un clavier
|
||||
// AZERTY le joueur se retrouverait avec un WASD qu'il ne peut pas atteindre.
|
||||
// On repose donc la disposition juste apres. "Par defaut" veut dire "par defaut
|
||||
// POUR TON CLAVIER", pas "par defaut pour le clavier du developpeur".
|
||||
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||
{
|
||||
GameInstance->ApplyKeyboardLayoutToDefaultKeys();
|
||||
}
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
// Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas
|
||||
// encore : leur bouton se grise tout seul tant que la page est absente.
|
||||
// Ordre des pages dans le WidgetSwitcher. Un bouton dont la page manque se
|
||||
// grise tout seul, ce qui reste vrai si un onglet vient a etre retire.
|
||||
constexpr int32 TabIndexGameplay = 0;
|
||||
constexpr int32 TabIndexDisplay = 1;
|
||||
constexpr int32 TabIndexGraphics = 2;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SurvivalAudioSettings.h"
|
||||
|
||||
#include "Sound/SoundClass.h"
|
||||
|
||||
FName USurvivalAudioSettings::GetCategoryName() const
|
||||
{
|
||||
return TEXT("Game");
|
||||
}
|
||||
|
||||
USurvivalAudioSettings* USurvivalAudioSettings::Get()
|
||||
{
|
||||
// GetMutableDefault et non GetDefault : les champs Cached* sont remplis a la
|
||||
// demande, donc les accesseurs ne peuvent pas etre const. Ce ne sont ni des
|
||||
// UPROPERTY ni des donnees serialisees -- muter un CDO n'est un probleme que
|
||||
// pour ce qui part sur le disque.
|
||||
return GetMutableDefault<USurvivalAudioSettings>();
|
||||
}
|
||||
|
||||
USoundClass* USurvivalAudioSettings::Resolve(const TSoftObjectPtr<USoundClass>& Asset, TStrongObjectPtr<USoundClass>& Cache)
|
||||
{
|
||||
if (Cache.IsValid())
|
||||
{
|
||||
return Cache.Get();
|
||||
}
|
||||
|
||||
if (Asset.IsNull())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Synchrone assume : une SoundClass est un objet minuscule, et le chargement
|
||||
// n'a lieu qu'une fois par lancement.
|
||||
//
|
||||
// Reset et pas une affectation : c'est ce Reset qui pose l'ancre GC, et la
|
||||
// raison pour laquelle ces caches ne sont pas des UPROPERTY (voir le header).
|
||||
Cache.Reset(Asset.LoadSynchronous());
|
||||
return Cache.Get();
|
||||
}
|
||||
|
||||
USoundClass* USurvivalAudioSettings::GetMasterSoundClass()
|
||||
{
|
||||
return Resolve(MasterSoundClass, CachedMasterSoundClass);
|
||||
}
|
||||
|
||||
USoundClass* USurvivalAudioSettings::GetMusicSoundClass()
|
||||
{
|
||||
return Resolve(MusicSoundClass, CachedMusicSoundClass);
|
||||
}
|
||||
|
||||
USoundClass* USurvivalAudioSettings::GetEffectsSoundClass()
|
||||
{
|
||||
return Resolve(EffectsSoundClass, CachedEffectsSoundClass);
|
||||
}
|
||||
|
||||
USoundClass* USurvivalAudioSettings::GetUiSoundClass()
|
||||
{
|
||||
return Resolve(UiSoundClass, CachedUiSoundClass);
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "GameMapsSettings.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "KeyboardLayout.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "SessionSubsystem.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
@@ -233,13 +234,131 @@ void USurvivalGameInstance::RegisterMappableContexts()
|
||||
return;
|
||||
}
|
||||
|
||||
bool bRegisteredAnyContext = false;
|
||||
|
||||
for (const TObjectPtr<UInputMappingContext>& Context : MappableContexts)
|
||||
{
|
||||
if (Context)
|
||||
{
|
||||
bRegisteredAnyContext = true;
|
||||
// Idempotent : le moteur ignore un contexte deja connu, on peut donc
|
||||
// rappeler cette fonction sans precaution.
|
||||
InputSettings->RegisterInputMappingContext(Context);
|
||||
}
|
||||
}
|
||||
|
||||
// APRES l'enregistrement et jamais avant : le profil de touches ne connait un
|
||||
// mapping -- donc sa touche par defaut -- qu'une fois son IMC declare. Appeler
|
||||
// la conversion plus tot la ferait tourner sur un profil vide, sans erreur ni
|
||||
// effet, ce qui est le pire des cas a diagnostiquer.
|
||||
//
|
||||
// UNE SEULE FOIS par session, en revanche. Cette fonction est aussi le point
|
||||
// d'entree du rafraichissement de l'onglet Touches, appele apres CHAQUE
|
||||
// remappage : y laisser la conversion reprendrait sa touche au joueur qui a
|
||||
// choisi celle de l'IMC -- la lettre W sur un AZERTY est identique au defaut,
|
||||
// donc indiscernable d'un mapping auquel personne n'a touche.
|
||||
if (bRegisteredAnyContext && !bKeyboardLayoutApplied)
|
||||
{
|
||||
bKeyboardLayoutApplied = true;
|
||||
ApplyKeyboardLayoutToDefaultKeys();
|
||||
}
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::ApplyKeyboardLayoutToDefaultKeys()
|
||||
{
|
||||
const ULocalPlayer* LocalPlayer = GetFirstGamePlayer();
|
||||
if (!LocalPlayer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
|
||||
UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr;
|
||||
const UEnhancedPlayerMappableKeyProfile* Profile = InputSettings ? InputSettings->GetActiveKeyProfile() : nullptr;
|
||||
|
||||
if (!Profile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// On COLLECTE avant d'appliquer, au lieu de mapper au fil de la lecture.
|
||||
// MapPlayerKey ecrit dans les memes lignes que celles qu'on parcourt : muter un
|
||||
// conteneur pendant son iteration invalide les iterateurs des que le stockage
|
||||
// est reorganise. Le bug qui en sort est intermittent et ne ressemble a rien.
|
||||
struct FPendingRemap
|
||||
{
|
||||
FName MappingName;
|
||||
EPlayerMappableKeySlot Slot;
|
||||
FKey LayoutKey;
|
||||
};
|
||||
|
||||
TArray<FPendingRemap> PendingRemaps;
|
||||
|
||||
for (const TPair<FName, FKeyMappingRow>& Row : Profile->GetPlayerMappingRows())
|
||||
{
|
||||
for (const FPlayerKeyMapping& Mapping : Row.Value.Mappings)
|
||||
{
|
||||
// Le joueur a deja choisi cette touche : son choix fait autorite, meme
|
||||
// s'il est incoherent avec sa disposition. C'est aussi ce qui rend la
|
||||
// fonction idempotente -- notre propre conversion compte comme un choix
|
||||
// des qu'elle est posee, donc on ne repasse jamais dessus.
|
||||
if (Mapping.IsCustomized())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const FKey DefaultKey = Mapping.GetDefaultKey();
|
||||
const FKey LayoutKey = KeyboardLayout::RemapKeyToActiveLayout(DefaultKey);
|
||||
|
||||
// Clavier QWERTY, touche insensible a la disposition, ou manette : la
|
||||
// conversion rend la touche d'origine et il n'y a rien a ecrire. Sans ce
|
||||
// test on poserait une surcharge identique au defaut sur chaque mapping,
|
||||
// et "Reinitialiser" n'aurait plus rien a reinitialiser.
|
||||
if (LayoutKey == DefaultKey)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PendingRemaps.Add({ Row.Key, Mapping.GetSlot(), LayoutKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Journalise AVANT de sortir, et donc meme quand il n'y a rien a faire. C'est
|
||||
// la seule trace lisible quand un testeur signale des touches inattendues, et
|
||||
// le cas "0 touche" est justement celui qu'il faut pouvoir distinguer du
|
||||
// silence : sans cette ligne, un clavier QWERTY et un systeme qui n'a pas
|
||||
// tourne du tout produisent exactement le meme log, c'est-a-dire aucun.
|
||||
//
|
||||
// Un KLID se lit en deux temps : "0000040C" est un AZERTY francais, "00000409"
|
||||
// un QWERTY US. Un utilisateur francais sur clavier QWERTY rend bien 00000409,
|
||||
// parce que c'est la DISPOSITION qui est journalisee, pas la langue -- et c'est
|
||||
// exactement la distinction sur laquelle une detection par pays se tromperait.
|
||||
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : disposition clavier [%s], %d touche(s) a traduire depuis le QWERTY de reference."),
|
||||
*KeyboardLayout::GetActiveLayoutId(), PendingRemaps.Num());
|
||||
|
||||
if (PendingRemaps.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const FPendingRemap& Remap : PendingRemaps)
|
||||
{
|
||||
FMapPlayerKeyArgs Args;
|
||||
Args.MappingName = Remap.MappingName;
|
||||
Args.Slot = Remap.Slot;
|
||||
Args.NewKey = Remap.LayoutKey;
|
||||
|
||||
FGameplayTagContainer FailureReason;
|
||||
InputSettings->MapPlayerKey(Args, FailureReason);
|
||||
|
||||
if (!FailureReason.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : conversion de '%s' vers '%s' refusee (%s)."),
|
||||
*Remap.MappingName.ToString(), *Remap.LayoutKey.ToString(), *FailureReason.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Pousse les surcharges dans les contextes deja actifs : le pawn n'a rien a
|
||||
// rebrancher, les touches sont bonnes des la premiere image.
|
||||
InputSettings->ApplySettings();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
#include "AudioDevice.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Misc/App.h"
|
||||
#include "Sound/AudioSettings.h"
|
||||
#include "Sound/SoundClass.h"
|
||||
#include "Sound/SoundMix.h"
|
||||
#include "SurvivalAudioSettings.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -13,6 +20,88 @@ namespace
|
||||
const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
|
||||
return FMath::Clamp(Snapped, Min, Max);
|
||||
}
|
||||
|
||||
/** Pourcentage affiche -> amplitude reelle, courbe perceptive comprise. */
|
||||
float VolumePercentToAmplitude(float Percent)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
|
||||
const float Normalized = FMath::Clamp(Percent, VolumeMin, VolumeMax) / 100.f;
|
||||
return FMath::Pow(Normalized, VolumeCurveExponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Le peripherique audio a qui parler.
|
||||
*
|
||||
* En PIE le monde de jeu peut avoir le sien, distinct de celui de l'editeur :
|
||||
* viser systematiquement le peripherique principal ferait regler le volume de
|
||||
* l'editeur pendant que le jeu, lui, ne bouge pas.
|
||||
*/
|
||||
FAudioDevice* GetSettingsAudioDevice()
|
||||
{
|
||||
if (!GEngine)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (const UWorld* PlayWorld = GEngine->GetCurrentPlayWorld())
|
||||
{
|
||||
if (FAudioDevice* WorldDevice = PlayWorld->GetAudioDeviceRaw())
|
||||
{
|
||||
return WorldDevice;
|
||||
}
|
||||
}
|
||||
|
||||
return GEngine->GetMainAudioDeviceRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Le mix par lequel passent les volumes du joueur.
|
||||
*
|
||||
* C'est celui de Project Settings > Audio > Default Base Sound Mix, et pas une
|
||||
* reference a nous. Deux raisons : le moteur le REPOSE a chaque chargement de
|
||||
* map (UWorld::UpdateWorldComponents -> SetDefaultBaseSoundMix), donc il est
|
||||
* toujours actif sans qu'on ait a le pousser nous-memes ; et un mix de base est
|
||||
* evalue a plein regime en permanence, alors qu'un mix pousse a la main
|
||||
* s'eteindrait au bout de sa duree.
|
||||
*/
|
||||
USoundMix* GetSettingsSoundMix()
|
||||
{
|
||||
const FSoftObjectPath& MixPath = GetDefault<UAudioSettings>()->DefaultBaseSoundMix;
|
||||
return MixPath.IsValid() ? Cast<USoundMix>(MixPath.TryLoad()) : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pose le volume d'une SoundClass, en pourcentage.
|
||||
*
|
||||
* bApplyToChildren merite une explication, parce que se tromper la donne un
|
||||
* volume maitre qui n'agit que sur les sons non tagues : le moteur propage les
|
||||
* proprietes du parent vers les enfants (ParseSoundClasses) AVANT d'appliquer
|
||||
* les ajusteurs du mix (ApplyClassAdjusters). Un ajusteur pose sur la classe
|
||||
* maitre arrive donc trop tard pour redescendre tout seul -- il faut demander
|
||||
* explicitement la recursion. Les feuilles, elles, gardent false : leur
|
||||
* ajusteur n'a personne a qui redescendre.
|
||||
*
|
||||
* Aucun fondu non plus, et ce n'est pas un oubli : toute duree non nulle rend le
|
||||
* curseur inutilisable. Quand on repose un override alors que le precedent n'a
|
||||
* pas fini son fondu, UpdateClassAdjustorOverrideEntry relance l'interpolation
|
||||
* (FDynamicParameter::Set remet CurrTimeSec a zero) mais n'appelle PAS
|
||||
* Update(DeltaTime) cette frame-la. Un curseur qu'on glisse repose l'override a
|
||||
* chaque image : la rampe repart indefiniment de zero et GetValue() rend toujours
|
||||
* la valeur precedente. Le volume ne rattrape son retard qu'une fois le geste
|
||||
* ARRETE. A zero, Set() ecrit directement la valeur -- et le mixeur lisse deja le
|
||||
* gain de chaque source dans son buffer de rendu, donc pas de clic pour autant.
|
||||
*/
|
||||
void ApplyClassVolume(FAudioDevice& AudioDevice, USoundMix& Mix, USoundClass* SoundClass, float Percent, bool bApplyToChildren)
|
||||
{
|
||||
if (!SoundClass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AudioDevice.SetSoundMixClassOverride(&Mix, SoundClass,
|
||||
VolumePercentToAmplitude(Percent), /*Pitch*/ 1.f, /*FadeInTime*/ 0.f, bApplyToChildren);
|
||||
}
|
||||
}
|
||||
|
||||
USurvivalUserSettings::USurvivalUserSettings()
|
||||
@@ -22,6 +111,7 @@ USurvivalUserSettings::USurvivalUserSettings()
|
||||
// fichier s'il existe.
|
||||
SetSurvivalGameplayToDefaults();
|
||||
SetSurvivalDisplayToDefaults();
|
||||
SetSurvivalAudioToDefaults();
|
||||
}
|
||||
|
||||
USurvivalUserSettings* USurvivalUserSettings::Get()
|
||||
@@ -46,6 +136,22 @@ void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
|
||||
Brightness = 100.f;
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetSurvivalAudioToDefaults()
|
||||
{
|
||||
// 80 et non 100 sur le maitre : il faut laisser au joueur de la marge vers le
|
||||
// haut, sinon le seul reglage possible est de baisser.
|
||||
MasterVolume = 80.f;
|
||||
|
||||
// La musique sous les effets, comme partout : elle est continue, eux sont des
|
||||
// evenements, et un evenement doit passer par-dessus le fond.
|
||||
MusicVolume = 60.f;
|
||||
EffectsVolume = 100.f;
|
||||
UiVolume = 80.f;
|
||||
|
||||
// Le defaut du moteur, qu'on se contente d'exposer.
|
||||
bPlayInBackground = false;
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetToDefaults()
|
||||
{
|
||||
// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
|
||||
@@ -54,6 +160,7 @@ void USurvivalUserSettings::SetToDefaults()
|
||||
|
||||
SetSurvivalGameplayToDefaults();
|
||||
SetSurvivalDisplayToDefaults();
|
||||
SetSurvivalAudioToDefaults();
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetBrightness(float Value)
|
||||
@@ -85,6 +192,40 @@ void USurvivalUserSettings::ApplySurvivalSettings()
|
||||
OnSurvivalSettingsApplied.Broadcast();
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::ApplyAudioSettings()
|
||||
{
|
||||
// Ce reglage-la ne passe pas par le mix : c'est l'application entiere qui est
|
||||
// baissee quand la fenetre perd le focus, bien en amont des SoundClass. Le
|
||||
// moteur le stocke dans son propre .ini, d'ou une valeur reposee a chaque
|
||||
// application plutot que memorisee ailleurs.
|
||||
FApp::SetUnfocusedVolumeMultiplier(bPlayInBackground ? 1.f : 0.f);
|
||||
|
||||
FAudioDevice* AudioDevice = GetSettingsAudioDevice();
|
||||
if (!AudioDevice)
|
||||
{
|
||||
// Normal tres tot au demarrage, ou en serveur dedie : les valeurs sont
|
||||
// deja enregistrees, le prochain passage les posera.
|
||||
return;
|
||||
}
|
||||
|
||||
USoundMix* Mix = GetSettingsSoundMix();
|
||||
if (!Mix)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalUserSettings : aucun Default Base Sound Mix dans Project Settings > Audio, les curseurs de volume resteront sans effet."));
|
||||
return;
|
||||
}
|
||||
|
||||
USurvivalAudioSettings* AudioAssets = USurvivalAudioSettings::Get();
|
||||
|
||||
// Le maitre EN PREMIER et avec recursion : il redescend sur les trois autres,
|
||||
// qui reappliquent ensuite la leur par-dessus. Les deux se multiplient, ce qui
|
||||
// est exactement ce qu'on attend d'un volume general.
|
||||
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMasterSoundClass(), MasterVolume, /*bApplyToChildren*/ true);
|
||||
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMusicSoundClass(), MusicVolume, /*bApplyToChildren*/ false);
|
||||
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetEffectsSoundClass(), EffectsVolume, /*bApplyToChildren*/ false);
|
||||
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetUiSoundClass(), UiVolume, /*bApplyToChildren*/ false);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::ApplyNonResolutionSettings()
|
||||
{
|
||||
Super::ApplyNonResolutionSettings();
|
||||
@@ -92,6 +233,7 @@ void USurvivalUserSettings::ApplyNonResolutionSettings()
|
||||
// ApplySettings() passe forcement par ici : brancher la diffusion a ce
|
||||
// niveau garantit que nos reglages suivent, quel que soit le chemin.
|
||||
ApplySurvivalSettings();
|
||||
ApplyAudioSettings();
|
||||
}
|
||||
|
||||
float USurvivalUserSettings::GetLookMultiplierX() const
|
||||
@@ -173,3 +315,32 @@ void USurvivalUserSettings::SetCrouchToggle(bool bValue)
|
||||
{
|
||||
bCrouchToggle = bValue;
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetMasterVolume(float Value)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
MasterVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetMusicVolume(float Value)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
MusicVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetEffectsVolume(float Value)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
EffectsVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetUiVolume(float Value)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
UiVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetPlayInBackground(bool bValue)
|
||||
{
|
||||
bPlayInBackground = bValue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user