Skip to content

Instantly share code, notes, and snippets.

@ryancole
Created October 1, 2018 02:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryancole/5f9551925971b4f20ab42d1949d5bd6f to your computer and use it in GitHub Desktop.
Save ryancole/5f9551925971b4f20ab42d1949d5bd6f to your computer and use it in GitHub Desktop.
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.
#include "ShooterGame.h"
#include "Weapons/ShooterWeapon.h"
#include "Weapons/ShooterDamageType.h"
#include "UI/ShooterHUD.h"
#include "Online/ShooterPlayerState.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimInstance.h"
#include "Sound/SoundNodeLocalPlayer.h"
#include "AudioThread.h"
static int32 NetVisualizeRelevancyTestPoints = 0;
FAutoConsoleVariableRef CVarNetVisualizeRelevancyTestPoints(
TEXT("p.NetVisualizeRelevancyTestPoints"),
NetVisualizeRelevancyTestPoints,
TEXT("")
TEXT("0: Disable, 1: Enable"),
ECVF_Cheat);
static int32 NetEnablePauseRelevancy = 1;
FAutoConsoleVariableRef CVarNetEnablePauseRelevancy(
TEXT("p.NetEnablePauseRelevancy"),
NetEnablePauseRelevancy,
TEXT("")
TEXT("0: Disable, 1: Enable"),
ECVF_Cheat);
FOnShooterCharacterWeaponChange AShooterCharacter::NotifyWeaponChange;
AShooterCharacter::AShooterCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UShooterCharacterMovement>(ACharacter::CharacterMovementComponentName))
{
Mesh1P = ObjectInitializer.CreateDefaultSubobject<USkeletalMeshComponent>(this, TEXT("PawnMesh1P"));
Mesh1P->SetupAttachment(GetCapsuleComponent());
Mesh1P->bOnlyOwnerSee = true;
Mesh1P->bOwnerNoSee = false;
Mesh1P->bCastDynamicShadow = false;
Mesh1P->bReceivesDecals = false;
Mesh1P->MeshComponentUpdateFlag = EMeshComponentUpdateFlag::OnlyTickPoseWhenRendered;
Mesh1P->PrimaryComponentTick.TickGroup = TG_PrePhysics;
Mesh1P->SetCollisionObjectType(ECC_Pawn);
Mesh1P->SetCollisionEnabled(ECollisionEnabled::NoCollision);
Mesh1P->SetCollisionResponseToAllChannels(ECR_Ignore);
GetMesh()->bOnlyOwnerSee = false;
GetMesh()->bOwnerNoSee = true;
GetMesh()->bReceivesDecals = false;
GetMesh()->SetCollisionObjectType(ECC_Pawn);
GetMesh()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
GetMesh()->SetCollisionResponseToChannel(COLLISION_WEAPON, ECR_Block);
GetMesh()->SetCollisionResponseToChannel(COLLISION_PROJECTILE, ECR_Block);
GetMesh()->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_PROJECTILE, ECR_Block);
GetCapsuleComponent()->SetCollisionResponseToChannel(COLLISION_WEAPON, ECR_Ignore);
TargetingSpeedModifier = 0.5f;
bIsTargeting = false;
RunningSpeedModifier = 1.5f;
bWantsToRun = false;
bWantsToFire = false;
LowHealthPercentage = 0.5f;
BaseTurnRate = 45.f;
BaseLookUpRate = 45.f;
}
void AShooterCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
if (Role == ROLE_Authority)
{
Health = GetMaxHealth();
SpawnDefaultInventory();
}
// set initial mesh visibility (3rd person view)
UpdatePawnMeshes();
// create material instance for setting team colors (3rd person view)
for (int32 iMat = 0; iMat < GetMesh()->GetNumMaterials(); iMat++)
{
MeshMIDs.Add(GetMesh()->CreateAndSetMaterialInstanceDynamic(iMat));
}
// play respawn effects
if (GetNetMode() != NM_DedicatedServer)
{
if (RespawnFX)
{
UGameplayStatics::SpawnEmitterAtLocation(this, RespawnFX, GetActorLocation(), GetActorRotation());
}
if (RespawnSound)
{
UGameplayStatics::PlaySoundAtLocation(this, RespawnSound, GetActorLocation());
}
}
}
void AShooterCharacter::Destroyed()
{
Super::Destroyed();
DestroyInventory();
}
void AShooterCharacter::PawnClientRestart()
{
Super::PawnClientRestart();
// switch mesh to 1st person view
UpdatePawnMeshes();
// reattach weapon if needed
SetCurrentWeapon(CurrentWeapon);
// set team colors for 1st person view
UMaterialInstanceDynamic* Mesh1PMID = Mesh1P->CreateAndSetMaterialInstanceDynamic(0);
UpdateTeamColors(Mesh1PMID);
}
void AShooterCharacter::PossessedBy(class AController* InController)
{
Super::PossessedBy(InController);
// [server] as soon as PlayerState is assigned, set team colors of this pawn for local player
UpdateTeamColorsAllMIDs();
}
void AShooterCharacter::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
// [client] as soon as PlayerState is assigned, set team colors of this pawn for local player
if (PlayerState != NULL)
{
UpdateTeamColorsAllMIDs();
}
}
FRotator AShooterCharacter::GetAimOffsets() const
{
const FVector AimDirWS = GetBaseAimRotation().Vector();
const FVector AimDirLS = ActorToWorld().InverseTransformVectorNoScale(AimDirWS);
const FRotator AimRotLS = AimDirLS.Rotation();
return AimRotLS;
}
bool AShooterCharacter::IsEnemyFor(AController* TestPC) const
{
if (TestPC == Controller || TestPC == NULL)
{
return false;
}
AShooterPlayerState* TestPlayerState = Cast<AShooterPlayerState>(TestPC->PlayerState);
AShooterPlayerState* MyPlayerState = Cast<AShooterPlayerState>(PlayerState);
bool bIsEnemy = true;
if (GetWorld()->GetGameState())
{
const AShooterGameMode* DefGame = GetWorld()->GetGameState()->GetDefaultGameMode<AShooterGameMode>();
if (DefGame && MyPlayerState && TestPlayerState)
{
bIsEnemy = DefGame->CanDealDamage(TestPlayerState, MyPlayerState);
}
}
return bIsEnemy;
}
//////////////////////////////////////////////////////////////////////////
// Meshes
void AShooterCharacter::UpdatePawnMeshes()
{
bool const bFirstPerson = IsFirstPerson();
Mesh1P->MeshComponentUpdateFlag = !bFirstPerson ? EMeshComponentUpdateFlag::OnlyTickPoseWhenRendered : EMeshComponentUpdateFlag::AlwaysTickPoseAndRefreshBones;
Mesh1P->SetOwnerNoSee(!bFirstPerson);
GetMesh()->MeshComponentUpdateFlag = bFirstPerson ? EMeshComponentUpdateFlag::OnlyTickPoseWhenRendered : EMeshComponentUpdateFlag::AlwaysTickPoseAndRefreshBones;
GetMesh()->SetOwnerNoSee(bFirstPerson);
}
void AShooterCharacter::UpdateTeamColors(UMaterialInstanceDynamic* UseMID)
{
if (UseMID)
{
AShooterPlayerState* MyPlayerState = Cast<AShooterPlayerState>(PlayerState);
if (MyPlayerState != NULL)
{
float MaterialParam = (float)MyPlayerState->GetTeamNum();
UseMID->SetScalarParameterValue(TEXT("Team Color Index"), MaterialParam);
}
}
}
void AShooterCharacter::OnCameraUpdate(const FVector& CameraLocation, const FRotator& CameraRotation)
{
USkeletalMeshComponent* DefMesh1P = Cast<USkeletalMeshComponent>(GetClass()->GetDefaultSubobjectByName(TEXT("PawnMesh1P")));
const FMatrix DefMeshLS = FRotationTranslationMatrix(DefMesh1P->RelativeRotation, DefMesh1P->RelativeLocation);
const FMatrix LocalToWorld = ActorToWorld().ToMatrixWithScale();
// Mesh rotating code expect uniform scale in LocalToWorld matrix
const FRotator RotCameraPitch(CameraRotation.Pitch, 0.0f, 0.0f);
const FRotator RotCameraYaw(0.0f, CameraRotation.Yaw, 0.0f);
const FMatrix LeveledCameraLS = FRotationTranslationMatrix(RotCameraYaw, CameraLocation) * LocalToWorld.Inverse();
const FMatrix PitchedCameraLS = FRotationMatrix(RotCameraPitch) * LeveledCameraLS;
const FMatrix MeshRelativeToCamera = DefMeshLS * LeveledCameraLS.Inverse();
const FMatrix PitchedMesh = MeshRelativeToCamera * PitchedCameraLS;
Mesh1P->SetRelativeLocationAndRotation(PitchedMesh.GetOrigin(), PitchedMesh.Rotator());
}
//////////////////////////////////////////////////////////////////////////
// Damage & death
void AShooterCharacter::FellOutOfWorld(const class UDamageType& dmgType)
{
Die(Health, FDamageEvent(dmgType.GetClass()), NULL, NULL);
}
void AShooterCharacter::Suicide()
{
KilledBy(this);
}
void AShooterCharacter::KilledBy(APawn* EventInstigator)
{
if (Role == ROLE_Authority && !bIsDying)
{
AController* Killer = NULL;
if (EventInstigator != NULL)
{
Killer = EventInstigator->Controller;
LastHitBy = NULL;
}
Die(Health, FDamageEvent(UDamageType::StaticClass()), Killer, NULL);
}
}
float AShooterCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, class AActor* DamageCauser)
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->HasGodMode())
{
return 0.f;
}
if (Health <= 0.f)
{
return 0.f;
}
// Modify based on game rules.
AShooterGameMode* const Game = GetWorld()->GetAuthGameMode<AShooterGameMode>();
Damage = Game ? Game->ModifyDamage(Damage, this, DamageEvent, EventInstigator, DamageCauser) : 0.f;
const float ActualDamage = Super::TakeDamage(Damage, DamageEvent, EventInstigator, DamageCauser);
if (ActualDamage > 0.f)
{
Health -= ActualDamage;
if (Health <= 0)
{
Die(ActualDamage, DamageEvent, EventInstigator, DamageCauser);
}
else
{
PlayHit(ActualDamage, DamageEvent, EventInstigator ? EventInstigator->GetPawn() : NULL, DamageCauser);
}
MakeNoise(1.0f, EventInstigator ? EventInstigator->GetPawn() : this);
}
return ActualDamage;
}
bool AShooterCharacter::CanDie(float KillingDamage, FDamageEvent const& DamageEvent, AController* Killer, AActor* DamageCauser) const
{
if (bIsDying // already dying
|| IsPendingKill() // already destroyed
|| Role != ROLE_Authority // not authority
|| GetWorld()->GetAuthGameMode<AShooterGameMode>() == NULL
|| GetWorld()->GetAuthGameMode<AShooterGameMode>()->GetMatchState() == MatchState::LeavingMap) // level transition occurring
{
return false;
}
return true;
}
bool AShooterCharacter::Die(float KillingDamage, FDamageEvent const& DamageEvent, AController* Killer, AActor* DamageCauser)
{
if (!CanDie(KillingDamage, DamageEvent, Killer, DamageCauser))
{
return false;
}
Health = FMath::Min(0.0f, Health);
// if this is an environmental death then refer to the previous killer so that they receive credit (knocked into lava pits, etc)
UDamageType const* const DamageType = DamageEvent.DamageTypeClass ? DamageEvent.DamageTypeClass->GetDefaultObject<UDamageType>() : GetDefault<UDamageType>();
Killer = GetDamageInstigator(Killer, *DamageType);
AController* const KilledPlayer = (Controller != NULL) ? Controller : Cast<AController>(GetOwner());
GetWorld()->GetAuthGameMode<AShooterGameMode>()->Killed(Killer, KilledPlayer, this, DamageType);
NetUpdateFrequency = GetDefault<AShooterCharacter>()->NetUpdateFrequency;
GetCharacterMovement()->ForceReplicationUpdate();
OnDeath(KillingDamage, DamageEvent, Killer ? Killer->GetPawn() : NULL, DamageCauser);
return true;
}
void AShooterCharacter::OnDeath(float KillingDamage, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser)
{
if (bIsDying)
{
return;
}
bReplicateMovement = false;
TearOff();
bIsDying = true;
if (Role == ROLE_Authority)
{
ReplicateHit(KillingDamage, DamageEvent, PawnInstigator, DamageCauser, true);
// play the force feedback effect on the client player controller
AShooterPlayerController* PC = Cast<AShooterPlayerController>(Controller);
if (PC && DamageEvent.DamageTypeClass)
{
UShooterDamageType *DamageType = Cast<UShooterDamageType>(DamageEvent.DamageTypeClass->GetDefaultObject());
if (DamageType && DamageType->KilledForceFeedback && PC->IsVibrationEnabled())
{
PC->ClientPlayForceFeedback(DamageType->KilledForceFeedback, false, false, "Damage");
}
}
}
// cannot use IsLocallyControlled here, because even local client's controller may be NULL here
if (GetNetMode() != NM_DedicatedServer && DeathSound && Mesh1P && Mesh1P->IsVisible())
{
UGameplayStatics::PlaySoundAtLocation(this, DeathSound, GetActorLocation());
}
// remove all weapons
DestroyInventory();
// switch back to 3rd person view
UpdatePawnMeshes();
DetachFromControllerPendingDestroy();
StopAllAnimMontages();
if (LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying())
{
LowHealthWarningPlayer->Stop();
}
if (RunLoopAC)
{
RunLoopAC->Stop();
}
if (GetMesh())
{
static FName CollisionProfileName(TEXT("Ragdoll"));
GetMesh()->SetCollisionProfileName(CollisionProfileName);
}
SetActorEnableCollision(true);
// Death anim
float DeathAnimDuration = PlayAnimMontage(DeathAnim);
// Ragdoll
if (DeathAnimDuration > 0.f)
{
// Trigger ragdoll a little before the animation early so the character doesn't
// blend back to its normal position.
const float TriggerRagdollTime = DeathAnimDuration - 0.7f;
// Enable blend physics so the bones are properly blending against the montage.
GetMesh()->bBlendPhysics = true;
// Use a local timer handle as we don't need to store it for later but we don't need to look for something to clear
FTimerHandle TimerHandle;
GetWorldTimerManager().SetTimer(TimerHandle, this, &AShooterCharacter::SetRagdollPhysics, FMath::Max(0.1f, TriggerRagdollTime), false);
}
else
{
SetRagdollPhysics();
}
// disable collisions on capsule
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCapsuleComponent()->SetCollisionResponseToAllChannels(ECR_Ignore);
}
void AShooterCharacter::PlayHit(float DamageTaken, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser)
{
if (Role == ROLE_Authority)
{
ReplicateHit(DamageTaken, DamageEvent, PawnInstigator, DamageCauser, false);
// play the force feedback effect on the client player controller
AShooterPlayerController* PC = Cast<AShooterPlayerController>(Controller);
if (PC && DamageEvent.DamageTypeClass)
{
UShooterDamageType *DamageType = Cast<UShooterDamageType>(DamageEvent.DamageTypeClass->GetDefaultObject());
if (DamageType && DamageType->HitForceFeedback && PC->IsVibrationEnabled())
{
PC->ClientPlayForceFeedback(DamageType->HitForceFeedback, false, false, "Damage");
}
}
}
if (DamageTaken > 0.f)
{
ApplyDamageMomentum(DamageTaken, DamageEvent, PawnInstigator, DamageCauser);
}
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
AShooterHUD* MyHUD = MyPC ? Cast<AShooterHUD>(MyPC->GetHUD()) : NULL;
if (MyHUD)
{
MyHUD->NotifyWeaponHit(DamageTaken, DamageEvent, PawnInstigator);
}
if (PawnInstigator && PawnInstigator != this && PawnInstigator->IsLocallyControlled())
{
AShooterPlayerController* InstigatorPC = Cast<AShooterPlayerController>(PawnInstigator->Controller);
AShooterHUD* InstigatorHUD = InstigatorPC ? Cast<AShooterHUD>(InstigatorPC->GetHUD()) : NULL;
if (InstigatorHUD)
{
InstigatorHUD->NotifyEnemyHit();
}
}
}
void AShooterCharacter::SetRagdollPhysics()
{
bool bInRagdoll = false;
if (IsPendingKill())
{
bInRagdoll = false;
}
else if (!GetMesh() || !GetMesh()->GetPhysicsAsset())
{
bInRagdoll = false;
}
else
{
// initialize physics/etc
GetMesh()->SetSimulatePhysics(true);
GetMesh()->WakeAllRigidBodies();
GetMesh()->bBlendPhysics = true;
bInRagdoll = true;
}
GetCharacterMovement()->StopMovementImmediately();
GetCharacterMovement()->DisableMovement();
GetCharacterMovement()->SetComponentTickEnabled(false);
if (!bInRagdoll)
{
// hide and set short lifespan
TurnOff();
SetActorHiddenInGame(true);
SetLifeSpan(1.0f);
}
else
{
SetLifeSpan(10.0f);
}
}
void AShooterCharacter::ReplicateHit(float Damage, struct FDamageEvent const& DamageEvent, class APawn* PawnInstigator, class AActor* DamageCauser, bool bKilled)
{
const float TimeoutTime = GetWorld()->GetTimeSeconds() + 0.5f;
FDamageEvent const& LastDamageEvent = LastTakeHitInfo.GetDamageEvent();
if ((PawnInstigator == LastTakeHitInfo.PawnInstigator.Get()) && (LastDamageEvent.DamageTypeClass == LastTakeHitInfo.DamageTypeClass) && (LastTakeHitTimeTimeout == TimeoutTime))
{
// same frame damage
if (bKilled && LastTakeHitInfo.bKilled)
{
// Redundant death take hit, just ignore it
return;
}
// otherwise, accumulate damage done this frame
Damage += LastTakeHitInfo.ActualDamage;
}
LastTakeHitInfo.ActualDamage = Damage;
LastTakeHitInfo.PawnInstigator = Cast<AShooterCharacter>(PawnInstigator);
LastTakeHitInfo.DamageCauser = DamageCauser;
LastTakeHitInfo.SetDamageEvent(DamageEvent);
LastTakeHitInfo.bKilled = bKilled;
LastTakeHitInfo.EnsureReplication();
LastTakeHitTimeTimeout = TimeoutTime;
}
void AShooterCharacter::OnRep_LastTakeHitInfo()
{
if (LastTakeHitInfo.bKilled)
{
OnDeath(LastTakeHitInfo.ActualDamage, LastTakeHitInfo.GetDamageEvent(), LastTakeHitInfo.PawnInstigator.Get(), LastTakeHitInfo.DamageCauser.Get());
}
else
{
PlayHit(LastTakeHitInfo.ActualDamage, LastTakeHitInfo.GetDamageEvent(), LastTakeHitInfo.PawnInstigator.Get(), LastTakeHitInfo.DamageCauser.Get());
}
}
//Pawn::PlayDying sets this lifespan, but when that function is called on client, dead pawn's role is still SimulatedProxy despite bTearOff being true.
void AShooterCharacter::TornOff()
{
SetLifeSpan(25.f);
}
bool AShooterCharacter::IsMoving()
{
return FMath::Abs(GetLastMovementInputVector().Size()) > 0.f;
}
//////////////////////////////////////////////////////////////////////////
// Inventory
void AShooterCharacter::SpawnDefaultInventory()
{
if (Role < ROLE_Authority)
{
return;
}
int32 NumWeaponClasses = DefaultInventoryClasses.Num();
for (int32 i = 0; i < NumWeaponClasses; i++)
{
if (DefaultInventoryClasses[i])
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AShooterWeapon* NewWeapon = GetWorld()->SpawnActor<AShooterWeapon>(DefaultInventoryClasses[i], SpawnInfo);
AddWeapon(NewWeapon);
}
}
// equip first weapon in inventory
if (Inventory.Num() > 0)
{
EquipWeapon(Inventory[0]);
}
}
void AShooterCharacter::DestroyInventory()
{
if (Role < ROLE_Authority)
{
return;
}
// remove all weapons from inventory and destroy them
for (int32 i = Inventory.Num() - 1; i >= 0; i--)
{
AShooterWeapon* Weapon = Inventory[i];
if (Weapon)
{
RemoveWeapon(Weapon);
Weapon->Destroy();
}
}
}
void AShooterCharacter::AddWeapon(AShooterWeapon* Weapon)
{
if (Weapon && Role == ROLE_Authority)
{
Weapon->OnEnterInventory(this);
Inventory.AddUnique(Weapon);
}
}
void AShooterCharacter::RemoveWeapon(AShooterWeapon* Weapon)
{
if (Weapon && Role == ROLE_Authority)
{
Weapon->OnLeaveInventory();
Inventory.RemoveSingle(Weapon);
}
}
AShooterWeapon* AShooterCharacter::FindWeapon(TSubclassOf<AShooterWeapon> WeaponClass)
{
for (int32 i = 0; i < Inventory.Num(); i++)
{
if (Inventory[i] && Inventory[i]->IsA(WeaponClass))
{
return Inventory[i];
}
}
return NULL;
}
void AShooterCharacter::EquipWeapon(AShooterWeapon* Weapon)
{
if (Weapon)
{
if (Role == ROLE_Authority)
{
SetCurrentWeapon(Weapon, CurrentWeapon);
}
else
{
ServerEquipWeapon(Weapon);
}
}
}
bool AShooterCharacter::ServerEquipWeapon_Validate(AShooterWeapon* Weapon)
{
return true;
}
void AShooterCharacter::ServerEquipWeapon_Implementation(AShooterWeapon* Weapon)
{
EquipWeapon(Weapon);
}
void AShooterCharacter::OnRep_CurrentWeapon(AShooterWeapon* LastWeapon)
{
SetCurrentWeapon(CurrentWeapon, LastWeapon);
}
void AShooterCharacter::SetCurrentWeapon(AShooterWeapon* NewWeapon, AShooterWeapon* LastWeapon)
{
AShooterWeapon* LocalLastWeapon = nullptr;
if (LastWeapon != NULL)
{
LocalLastWeapon = LastWeapon;
}
else if (NewWeapon != CurrentWeapon)
{
LocalLastWeapon = CurrentWeapon;
}
// unequip previous
if (LocalLastWeapon)
{
LocalLastWeapon->OnUnEquip();
}
CurrentWeapon = NewWeapon;
// equip new one
if (NewWeapon)
{
NewWeapon->SetOwningPawn(this); // Make sure weapon's MyPawn is pointing back to us. During replication, we can't guarantee APawn::CurrentWeapon will rep after AWeapon::MyPawn!
NewWeapon->OnEquip(LastWeapon);
}
NotifyWeaponChange.Broadcast(this, CurrentWeapon, LocalLastWeapon);
}
//////////////////////////////////////////////////////////////////////////
// Weapon usage
void AShooterCharacter::StartWeaponFire()
{
if (!bWantsToFire)
{
bWantsToFire = true;
if (CurrentWeapon)
{
CurrentWeapon->StartFire();
}
}
}
void AShooterCharacter::StopWeaponFire()
{
if (bWantsToFire)
{
bWantsToFire = false;
if (CurrentWeapon)
{
CurrentWeapon->StopFire();
}
}
}
bool AShooterCharacter::CanFire() const
{
return IsAlive();
}
bool AShooterCharacter::CanReload() const
{
return true;
}
void AShooterCharacter::SetTargeting(bool bNewTargeting)
{
bIsTargeting = bNewTargeting;
if (TargetingSound)
{
UGameplayStatics::SpawnSoundAttached(TargetingSound, GetRootComponent());
}
if (Role < ROLE_Authority)
{
ServerSetTargeting(bNewTargeting);
}
}
bool AShooterCharacter::ServerSetTargeting_Validate(bool bNewTargeting)
{
return true;
}
void AShooterCharacter::ServerSetTargeting_Implementation(bool bNewTargeting)
{
SetTargeting(bNewTargeting);
}
//////////////////////////////////////////////////////////////////////////
// Movement
void AShooterCharacter::SetRunning(bool bNewRunning, bool bToggle)
{
bWantsToRun = bNewRunning;
bWantsToRunToggled = bNewRunning && bToggle;
if (Role < ROLE_Authority)
{
ServerSetRunning(bNewRunning, bToggle);
}
}
bool AShooterCharacter::ServerSetRunning_Validate(bool bNewRunning, bool bToggle)
{
return true;
}
void AShooterCharacter::ServerSetRunning_Implementation(bool bNewRunning, bool bToggle)
{
SetRunning(bNewRunning, bToggle);
}
void AShooterCharacter::UpdateRunSounds()
{
const bool bIsRunSoundPlaying = RunLoopAC != nullptr && RunLoopAC->IsActive();
const bool bWantsRunSoundPlaying = IsRunning() && IsMoving();
// Don't bother playing the sounds unless we're running and moving.
if (!bIsRunSoundPlaying && bWantsRunSoundPlaying)
{
if (RunLoopAC != nullptr)
{
RunLoopAC->Play();
}
else if (RunLoopSound != nullptr)
{
RunLoopAC = UGameplayStatics::SpawnSoundAttached(RunLoopSound, GetRootComponent());
if (RunLoopAC != nullptr)
{
RunLoopAC->bAutoDestroy = false;
}
}
}
else if (bIsRunSoundPlaying && !bWantsRunSoundPlaying)
{
RunLoopAC->Stop();
if (RunStopSound != nullptr)
{
UGameplayStatics::SpawnSoundAttached(RunStopSound, GetRootComponent());
}
}
}
//////////////////////////////////////////////////////////////////////////
// Animations
float AShooterCharacter::PlayAnimMontage(class UAnimMontage* AnimMontage, float InPlayRate, FName StartSectionName)
{
USkeletalMeshComponent* UseMesh = GetPawnMesh();
if (AnimMontage && UseMesh && UseMesh->AnimScriptInstance)
{
return UseMesh->AnimScriptInstance->Montage_Play(AnimMontage, InPlayRate);
}
return 0.0f;
}
void AShooterCharacter::StopAnimMontage(class UAnimMontage* AnimMontage)
{
USkeletalMeshComponent* UseMesh = GetPawnMesh();
if (AnimMontage && UseMesh && UseMesh->AnimScriptInstance &&
UseMesh->AnimScriptInstance->Montage_IsPlaying(AnimMontage))
{
UseMesh->AnimScriptInstance->Montage_Stop(AnimMontage->BlendOut.GetBlendTime(), AnimMontage);
}
}
void AShooterCharacter::StopAllAnimMontages()
{
USkeletalMeshComponent* UseMesh = GetPawnMesh();
if (UseMesh && UseMesh->AnimScriptInstance)
{
UseMesh->AnimScriptInstance->Montage_Stop(0.0f);
}
}
//////////////////////////////////////////////////////////////////////////
// Input
void AShooterCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
check(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &AShooterCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AShooterCharacter::MoveRight);
PlayerInputComponent->BindAxis("MoveUp", this, &AShooterCharacter::MoveUp);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("TurnRate", this, &AShooterCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
PlayerInputComponent->BindAxis("LookUpRate", this, &AShooterCharacter::LookUpAtRate);
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AShooterCharacter::OnStartFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this, &AShooterCharacter::OnStopFire);
PlayerInputComponent->BindAction("Targeting", IE_Pressed, this, &AShooterCharacter::OnStartTargeting);
PlayerInputComponent->BindAction("Targeting", IE_Released, this, &AShooterCharacter::OnStopTargeting);
PlayerInputComponent->BindAction("NextWeapon", IE_Pressed, this, &AShooterCharacter::OnNextWeapon);
PlayerInputComponent->BindAction("PrevWeapon", IE_Pressed, this, &AShooterCharacter::OnPrevWeapon);
PlayerInputComponent->BindAction("Reload", IE_Pressed, this, &AShooterCharacter::OnReload);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AShooterCharacter::OnStartJump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &AShooterCharacter::OnStopJump);
PlayerInputComponent->BindAction("Run", IE_Pressed, this, &AShooterCharacter::OnStartRunning);
PlayerInputComponent->BindAction("RunToggle", IE_Pressed, this, &AShooterCharacter::OnStartRunningToggle);
PlayerInputComponent->BindAction("Run", IE_Released, this, &AShooterCharacter::OnStopRunning);
}
void AShooterCharacter::MoveForward(float Val)
{
if (Controller && Val != 0.f)
{
// Limit pitch when walking or falling
const bool bLimitRotation = (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling());
const FRotator Rotation = bLimitRotation ? GetActorRotation() : Controller->GetControlRotation();
const FVector Direction = FRotationMatrix(Rotation).GetScaledAxis(EAxis::X);
AddMovementInput(Direction, Val);
}
}
void AShooterCharacter::MoveRight(float Val)
{
if (Val != 0.f)
{
const FQuat Rotation = GetActorQuat();
const FVector Direction = FQuatRotationMatrix(Rotation).GetScaledAxis(EAxis::Y);
AddMovementInput(Direction, Val);
}
}
void AShooterCharacter::MoveUp(float Val)
{
if (Val != 0.f)
{
// Not when walking or falling.
if (GetCharacterMovement()->IsMovingOnGround() || GetCharacterMovement()->IsFalling())
{
return;
}
AddMovementInput(FVector::UpVector, Val);
}
}
void AShooterCharacter::TurnAtRate(float Val)
{
// calculate delta for this frame from the rate information
AddControllerYawInput(Val * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}
void AShooterCharacter::LookUpAtRate(float Val)
{
// calculate delta for this frame from the rate information
AddControllerPitchInput(Val * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}
void AShooterCharacter::OnStartFire()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (IsRunning())
{
SetRunning(false, false);
}
StartWeaponFire();
}
}
void AShooterCharacter::OnStopFire()
{
StopWeaponFire();
}
void AShooterCharacter::OnStartTargeting()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (IsRunning())
{
SetRunning(false, false);
}
SetTargeting(true);
}
}
void AShooterCharacter::OnStopTargeting()
{
SetTargeting(false);
}
void AShooterCharacter::OnNextWeapon()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (Inventory.Num() >= 2 && (CurrentWeapon == NULL || CurrentWeapon->GetCurrentState() != EWeaponState::Equipping))
{
const int32 CurrentWeaponIdx = Inventory.IndexOfByKey(CurrentWeapon);
AShooterWeapon* NextWeapon = Inventory[(CurrentWeaponIdx + 1) % Inventory.Num()];
EquipWeapon(NextWeapon);
}
}
}
void AShooterCharacter::OnPrevWeapon()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (Inventory.Num() >= 2 && (CurrentWeapon == NULL || CurrentWeapon->GetCurrentState() != EWeaponState::Equipping))
{
const int32 CurrentWeaponIdx = Inventory.IndexOfByKey(CurrentWeapon);
AShooterWeapon* PrevWeapon = Inventory[(CurrentWeaponIdx - 1 + Inventory.Num()) % Inventory.Num()];
EquipWeapon(PrevWeapon);
}
}
}
void AShooterCharacter::OnReload()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (CurrentWeapon)
{
CurrentWeapon->StartReload();
}
}
}
void AShooterCharacter::OnStartRunning()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (IsTargeting())
{
SetTargeting(false);
}
StopWeaponFire();
SetRunning(true, false);
}
}
void AShooterCharacter::OnStartRunningToggle()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
if (IsTargeting())
{
SetTargeting(false);
}
StopWeaponFire();
SetRunning(true, true);
}
}
void AShooterCharacter::OnStopRunning()
{
SetRunning(false, false);
}
bool AShooterCharacter::IsRunning() const
{
if (!GetCharacterMovement())
{
return false;
}
return (bWantsToRun || bWantsToRunToggled) && !GetVelocity().IsZero() && (GetVelocity().GetSafeNormal2D() | GetActorForwardVector()) > -0.1;
}
void AShooterCharacter::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (bWantsToRunToggled && !IsRunning())
{
SetRunning(false, false);
}
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->HasHealthRegen())
{
if (this->Health < this->GetMaxHealth())
{
this->Health += 5 * DeltaSeconds;
if (Health > this->GetMaxHealth())
{
Health = this->GetMaxHealth();
}
}
}
if (GEngine->UseSound())
{
if (LowHealthSound)
{
if ((this->Health > 0 && this->Health < this->GetMaxHealth() * LowHealthPercentage) && (!LowHealthWarningPlayer || !LowHealthWarningPlayer->IsPlaying()))
{
LowHealthWarningPlayer = UGameplayStatics::SpawnSoundAttached(LowHealthSound, GetRootComponent(),
NAME_None, FVector(ForceInit), EAttachLocation::KeepRelativeOffset, true);
LowHealthWarningPlayer->SetVolumeMultiplier(0.0f);
}
else if ((this->Health > this->GetMaxHealth() * LowHealthPercentage || this->Health < 0) && LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying())
{
LowHealthWarningPlayer->Stop();
}
if (LowHealthWarningPlayer && LowHealthWarningPlayer->IsPlaying())
{
const float MinVolume = 0.3f;
const float VolumeMultiplier = (1.0f - (this->Health / (this->GetMaxHealth() * LowHealthPercentage)));
LowHealthWarningPlayer->SetVolumeMultiplier(MinVolume + (1.0f - MinVolume) * VolumeMultiplier);
}
}
UpdateRunSounds();
}
const APlayerController* PC = Cast<APlayerController>(GetController());
const bool bLocallyControlled = (PC ? PC->IsLocalController() : false);
const uint32 UniqueID = GetUniqueID();
FAudioThread::RunCommandOnAudioThread([UniqueID, bLocallyControlled]()
{
USoundNodeLocalPlayer::GetLocallyControlledActorCache().Add(UniqueID, bLocallyControlled);
});
TArray<FVector> PointsToTest;
BuildPauseReplicationCheckPoints(PointsToTest);
if (NetVisualizeRelevancyTestPoints == 1)
{
for (FVector PointToTest : PointsToTest)
{
DrawDebugSphere(GetWorld(), PointToTest, 10.0f, 8, FColor::Red);
}
}
}
void AShooterCharacter::BeginDestroy()
{
Super::BeginDestroy();
if (!GExitPurge)
{
const uint32 UniqueID = GetUniqueID();
FAudioThread::RunCommandOnAudioThread([UniqueID]()
{
USoundNodeLocalPlayer::GetLocallyControlledActorCache().Remove(UniqueID);
});
}
}
void AShooterCharacter::OnStartJump()
{
AShooterPlayerController* MyPC = Cast<AShooterPlayerController>(Controller);
if (MyPC && MyPC->IsGameInputAllowed())
{
bPressedJump = true;
}
}
void AShooterCharacter::OnStopJump()
{
bPressedJump = false;
StopJumping();
}
//////////////////////////////////////////////////////////////////////////
// Replication
void AShooterCharacter::PreReplication(IRepChangedPropertyTracker & ChangedPropertyTracker)
{
Super::PreReplication(ChangedPropertyTracker);
// Only replicate this property for a short duration after it changes so join in progress players don't get spammed with fx when joining late
DOREPLIFETIME_ACTIVE_OVERRIDE(AShooterCharacter, LastTakeHitInfo, GetWorld() && GetWorld()->GetTimeSeconds() < LastTakeHitTimeTimeout);
}
void AShooterCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// only to local owner: weapon change requests are locally instigated, other clients don't need it
DOREPLIFETIME_CONDITION(AShooterCharacter, Inventory, COND_OwnerOnly);
// everyone except local owner: flag change is locally instigated
DOREPLIFETIME_CONDITION(AShooterCharacter, bIsTargeting, COND_SkipOwner);
DOREPLIFETIME_CONDITION(AShooterCharacter, bWantsToRun, COND_SkipOwner);
DOREPLIFETIME_CONDITION(AShooterCharacter, LastTakeHitInfo, COND_Custom);
// everyone
DOREPLIFETIME(AShooterCharacter, CurrentWeapon);
DOREPLIFETIME(AShooterCharacter, Health);
}
bool AShooterCharacter::IsReplicationPausedForConnection(const FNetViewer& ConnectionOwnerNetViewer)
{
if (NetEnablePauseRelevancy == 1)
{
APlayerController* PC = Cast<APlayerController>(ConnectionOwnerNetViewer.InViewer);
check(PC);
FVector ViewLocation;
FRotator ViewRotation;
PC->GetPlayerViewPoint(ViewLocation, ViewRotation);
FCollisionQueryParams CollisionParams(SCENE_QUERY_STAT(LineOfSight), true, PC->GetPawn());
CollisionParams.AddIgnoredActor(this);
TArray<FVector> PointsToTest;
BuildPauseReplicationCheckPoints(PointsToTest);
for (FVector PointToTest : PointsToTest)
{
if (!GetWorld()->LineTraceTestByChannel(PointToTest, ViewLocation, ECC_Visibility, CollisionParams))
{
return false;
}
}
return true;
}
return false;
}
void AShooterCharacter::OnReplicationPausedChanged(bool bIsReplicationPaused)
{
GetMesh()->SetHiddenInGame(bIsReplicationPaused, true);
}
AShooterWeapon* AShooterCharacter::GetWeapon() const
{
return CurrentWeapon;
}
int32 AShooterCharacter::GetInventoryCount() const
{
return Inventory.Num();
}
AShooterWeapon* AShooterCharacter::GetInventoryWeapon(int32 index) const
{
return Inventory[index];
}
USkeletalMeshComponent* AShooterCharacter::GetPawnMesh() const
{
return IsFirstPerson() ? Mesh1P : GetMesh();
}
USkeletalMeshComponent* AShooterCharacter::GetSpecifcPawnMesh(bool WantFirstPerson) const
{
return WantFirstPerson == true ? Mesh1P : GetMesh();
}
FName AShooterCharacter::GetWeaponAttachPoint() const
{
return WeaponAttachPoint;
}
float AShooterCharacter::GetTargetingSpeedModifier() const
{
return TargetingSpeedModifier;
}
bool AShooterCharacter::IsTargeting() const
{
return bIsTargeting;
}
float AShooterCharacter::GetRunningSpeedModifier() const
{
return RunningSpeedModifier;
}
bool AShooterCharacter::IsFiring() const
{
return bWantsToFire;
};
bool AShooterCharacter::IsFirstPerson() const
{
return IsAlive() && Controller && Controller->IsLocalPlayerController();
}
int32 AShooterCharacter::GetMaxHealth() const
{
return GetClass()->GetDefaultObject<AShooterCharacter>()->Health;
}
bool AShooterCharacter::IsAlive() const
{
return Health > 0;
}
float AShooterCharacter::GetLowHealthPercentage() const
{
return LowHealthPercentage;
}
void AShooterCharacter::UpdateTeamColorsAllMIDs()
{
for (int32 i = 0; i < MeshMIDs.Num(); ++i)
{
UpdateTeamColors(MeshMIDs[i]);
}
}
void AShooterCharacter::BuildPauseReplicationCheckPoints(TArray<FVector>& RelevancyCheckPoints)
{
FBoxSphereBounds Bounds = GetCapsuleComponent()->CalcBounds(GetCapsuleComponent()->GetComponentTransform());
FBox BoundingBox = Bounds.GetBox();
float XDiff = Bounds.BoxExtent.X * 2;
float YDiff = Bounds.BoxExtent.Y * 2;
RelevancyCheckPoints.Add(BoundingBox.Min);
RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X + XDiff, BoundingBox.Min.Y, BoundingBox.Min.Z));
RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X, BoundingBox.Min.Y + YDiff, BoundingBox.Min.Z));
RelevancyCheckPoints.Add(FVector(BoundingBox.Min.X + XDiff, BoundingBox.Min.Y + YDiff, BoundingBox.Min.Z));
RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X - XDiff, BoundingBox.Max.Y, BoundingBox.Max.Z));
RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X, BoundingBox.Max.Y - YDiff, BoundingBox.Max.Z));
RelevancyCheckPoints.Add(FVector(BoundingBox.Max.X - XDiff, BoundingBox.Max.Y - YDiff, BoundingBox.Max.Z));
RelevancyCheckPoints.Add(BoundingBox.Max);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment