Remove unused Unreal starter variants
This commit is contained in:
@@ -15,8 +15,6 @@ public class AgrarianGame : ModuleRules
|
||||
"InputCore",
|
||||
"EnhancedInput",
|
||||
"AIModule",
|
||||
"StateTreeModule",
|
||||
"GameplayStateTreeModule",
|
||||
"UMG",
|
||||
"Landscape",
|
||||
"Slate",
|
||||
@@ -33,20 +31,7 @@ public class AgrarianGame : ModuleRules
|
||||
}
|
||||
|
||||
PublicIncludePaths.AddRange(new string[] {
|
||||
"AgrarianGame",
|
||||
"AgrarianGame/Variant_Platforming",
|
||||
"AgrarianGame/Variant_Platforming/Animation",
|
||||
"AgrarianGame/Variant_Combat",
|
||||
"AgrarianGame/Variant_Combat/AI",
|
||||
"AgrarianGame/Variant_Combat/Animation",
|
||||
"AgrarianGame/Variant_Combat/Gameplay",
|
||||
"AgrarianGame/Variant_Combat/Interfaces",
|
||||
"AgrarianGame/Variant_Combat/UI",
|
||||
"AgrarianGame/Variant_SideScrolling",
|
||||
"AgrarianGame/Variant_SideScrolling/AI",
|
||||
"AgrarianGame/Variant_SideScrolling/Gameplay",
|
||||
"AgrarianGame/Variant_SideScrolling/Interfaces",
|
||||
"AgrarianGame/Variant_SideScrolling/UI"
|
||||
"AgrarianGame"
|
||||
});
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatAIController.h"
|
||||
#include "Components/StateTreeAIComponent.h"
|
||||
|
||||
ACombatAIController::ACombatAIController()
|
||||
{
|
||||
// create the StateTree AI Component
|
||||
StateTreeAI = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
|
||||
check(StateTreeAI);
|
||||
|
||||
// ensure we start the StateTree when we possess the pawn
|
||||
bStartAILogicOnPossess = true;
|
||||
|
||||
// ensure we're attached to the possessed character.
|
||||
// this is necessary for EnvQueries to work correctly
|
||||
bAttachToPawn = true;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AIController.h"
|
||||
#include "CombatAIController.generated.h"
|
||||
|
||||
class UStateTreeAIComponent;
|
||||
|
||||
/**
|
||||
* A basic AI Controller capable of running StateTree
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatAIController : public AAIController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** StateTree Component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStateTreeAIComponent* StateTreeAI;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatAIController();
|
||||
};
|
||||
@@ -1,343 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatEnemy.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "CombatAIController.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "Engine/DamageEvents.h"
|
||||
#include "CombatLifeBar.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
|
||||
ACombatEnemy::ACombatEnemy()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// bind the attack montage ended delegate
|
||||
OnAttackMontageEnded.BindUObject(this, &ACombatEnemy::AttackMontageEnded);
|
||||
|
||||
// set the AI Controller class by default
|
||||
AIControllerClass = ACombatAIController::StaticClass();
|
||||
|
||||
// use an AI Controller regardless of whether we're placed or spawned
|
||||
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
|
||||
|
||||
// ignore the controller's yaw rotation
|
||||
bUseControllerRotationYaw = false;
|
||||
|
||||
// create the life bar
|
||||
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
|
||||
LifeBar->SetupAttachment(RootComponent);
|
||||
|
||||
// set the collision capsule size
|
||||
GetCapsuleComponent()->SetCapsuleSize(35.0f, 90.0f);
|
||||
|
||||
// set the character movement properties
|
||||
GetCharacterMovement()->bUseControllerDesiredRotation = true;
|
||||
|
||||
// reset HP to maximum
|
||||
CurrentHP = MaxHP;
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAIComboAttack()
|
||||
{
|
||||
// ignore if we're already playing an attack animation
|
||||
if (bIsAttacking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// choose how many times we're going to attack
|
||||
TargetComboCount = FMath::RandRange(1, ComboSectionNames.Num() - 1);
|
||||
|
||||
// reset the attack counter
|
||||
CurrentComboAttack = 0;
|
||||
|
||||
// play the attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAIChargedAttack()
|
||||
{
|
||||
// ignore if we're already playing an attack animation
|
||||
if (bIsAttacking)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// choose how many loops are we going to charge for
|
||||
TargetChargeLoops = FMath::RandRange(MinChargeLoops, MaxChargeLoops);
|
||||
|
||||
// reset the charge loop counter
|
||||
CurrentChargeLoop = 0;
|
||||
|
||||
// play the attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
|
||||
{
|
||||
// reset the attacking flag
|
||||
bIsAttacking = false;
|
||||
|
||||
// call the attack completed delegate so the StateTree can continue execution
|
||||
OnAttackCompleted.ExecuteIfBound();
|
||||
}
|
||||
|
||||
const FVector& ACombatEnemy::GetLastDangerLocation() const
|
||||
{
|
||||
return LastDangerLocation;
|
||||
}
|
||||
|
||||
float ACombatEnemy::GetLastDangerTime() const
|
||||
{
|
||||
return LastDangerTime;
|
||||
}
|
||||
|
||||
void ACombatEnemy::DoAttackTrace(FName DamageSourceBone)
|
||||
{
|
||||
// sweep for objects in front of the character to be hit by the attack
|
||||
TArray<FHitResult> OutHits;
|
||||
|
||||
// start at the provided socket location, sweep forward
|
||||
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
|
||||
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
|
||||
|
||||
// enemies only affect Pawn collision objects; they don't knock back boxes
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
||||
|
||||
// use a sphere shape for the sweep
|
||||
FCollisionShape CollisionShape;
|
||||
CollisionShape.SetSphere(MeleeTraceRadius);
|
||||
|
||||
// ignore self
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
||||
{
|
||||
// iterate over each object hit
|
||||
for (const FHitResult& CurrentHit : OutHits)
|
||||
{
|
||||
/** does the actor have the player tag? */
|
||||
if (CurrentHit.GetActor()->ActorHasTag(FName("Player")))
|
||||
{
|
||||
// check if the actor is damageable
|
||||
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
||||
|
||||
if (Damageable)
|
||||
{
|
||||
// knock upwards and away from the impact normal
|
||||
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
|
||||
|
||||
// pass the damage event to the actor
|
||||
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::CheckCombo()
|
||||
{
|
||||
// increase the combo counter
|
||||
++CurrentComboAttack;
|
||||
|
||||
// do we still have attacks to play in this string?
|
||||
if (CurrentComboAttack < TargetComboCount)
|
||||
{
|
||||
// jump to the next attack section
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(ComboSectionNames[CurrentComboAttack], ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::CheckChargedAttack()
|
||||
{
|
||||
// increase the charge loop counter
|
||||
++CurrentChargeLoop;
|
||||
|
||||
// jump to either the loop or attack section of the montage depending on whether we hit the loop target
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(CurrentChargeLoop >= TargetChargeLoops ? ChargeAttackSection : ChargeLoopSection, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
||||
{
|
||||
|
||||
// pass the damage event to the actor
|
||||
FDamageEvent DamageEvent;
|
||||
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
|
||||
|
||||
// only process knockback and effects if we received nonzero damage
|
||||
if (ActualDamage > 0.0f)
|
||||
{
|
||||
// apply the knockback impulse
|
||||
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
|
||||
|
||||
// is the character ragdolling?
|
||||
if (GetMesh()->IsSimulatingPhysics())
|
||||
{
|
||||
// apply an impulse to the ragdoll
|
||||
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
|
||||
}
|
||||
|
||||
// stop the attack montages to interrupt the attack
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_Stop(0.1f, ComboAttackMontage);
|
||||
AnimInstance->Montage_Stop(0.1f, ChargedAttackMontage);
|
||||
}
|
||||
|
||||
// pass control to BP to play effects, etc.
|
||||
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::HandleDeath()
|
||||
{
|
||||
// hide the life bar
|
||||
LifeBar->SetHiddenInGame(true);
|
||||
|
||||
// disable the collision capsule to avoid being hit again while dead
|
||||
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
|
||||
// disable character movement
|
||||
GetCharacterMovement()->DisableMovement();
|
||||
|
||||
// enable full ragdoll physics
|
||||
GetMesh()->SetSimulatePhysics(true);
|
||||
|
||||
// call the died delegate to notify any subscribers
|
||||
OnEnemyDied.Broadcast();
|
||||
|
||||
// set up the death timer
|
||||
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &ACombatEnemy::RemoveFromLevel, DeathRemovalTime);
|
||||
}
|
||||
|
||||
void ACombatEnemy::ApplyHealing(float Healing, AActor* Healer)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatEnemy::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
||||
{
|
||||
// ensure we're being attacked by the player
|
||||
if (DangerSource && DangerSource->ActorHasTag(FName("Player")))
|
||||
{
|
||||
// save the danger location and game time
|
||||
LastDangerLocation = DangerLocation;
|
||||
LastDangerTime = GetWorld()->GetTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemy::RemoveFromLevel()
|
||||
{
|
||||
// destroy this actor
|
||||
Destroy();
|
||||
}
|
||||
|
||||
float ACombatEnemy::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
|
||||
{
|
||||
// only process damage if the character is still alive
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// reduce the current HP
|
||||
CurrentHP -= Damage;
|
||||
|
||||
// have we run out of HP?
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
// die
|
||||
HandleDeath();
|
||||
}
|
||||
else
|
||||
{
|
||||
// update the life bar
|
||||
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
|
||||
|
||||
// enable partial ragdoll physics, but keep the pelvis vertical
|
||||
GetMesh()->SetPhysicsBlendWeight(0.5f);
|
||||
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
|
||||
}
|
||||
|
||||
// return the received damage amount
|
||||
return Damage;
|
||||
}
|
||||
|
||||
void ACombatEnemy::Landed(const FHitResult& Hit)
|
||||
{
|
||||
Super::Landed(Hit);
|
||||
|
||||
// is the character still alive?
|
||||
if (CurrentHP >= 0.0f)
|
||||
{
|
||||
// disable ragdoll physics
|
||||
GetMesh()->SetPhysicsBlendWeight(0.0f);
|
||||
}
|
||||
|
||||
// call the landed Delegate for StateTree
|
||||
OnEnemyLanded.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void ACombatEnemy::BeginPlay()
|
||||
{
|
||||
// reset HP to maximum
|
||||
CurrentHP = MaxHP;
|
||||
|
||||
// we top the HP before BeginPlay so StateTree picks it up at the right value
|
||||
Super::BeginPlay();
|
||||
|
||||
// get the life bar widget from the widget comp
|
||||
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
|
||||
check(LifeBarWidget);
|
||||
|
||||
// fill the life bar
|
||||
LifeBarWidget->SetLifePercentage(1.0f);
|
||||
}
|
||||
|
||||
void ACombatEnemy::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the death timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "CombatAttacker.h"
|
||||
#include "CombatDamageable.h"
|
||||
#include "Animation/AnimMontage.h"
|
||||
#include "Engine/TimerHandle.h"
|
||||
#include "CombatEnemy.generated.h"
|
||||
|
||||
class UWidgetComponent;
|
||||
class UCombatLifeBar;
|
||||
class UAnimMontage;
|
||||
|
||||
/** Completed attack animation delegate for StateTree */
|
||||
DECLARE_DELEGATE(FOnEnemyAttackCompleted);
|
||||
|
||||
/** Landed delegate for StateTree */
|
||||
DECLARE_DELEGATE(FOnEnemyLanded);
|
||||
|
||||
/** Enemy died delegate */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEnemyDied);
|
||||
|
||||
/**
|
||||
* An AI-controlled character with combat capabilities.
|
||||
* Its bundled AI Controller runs logic through StateTree
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatEnemy : public ACharacter, public ICombatAttacker, public ICombatDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Life bar widget component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UWidgetComponent* LifeBar;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatEnemy();
|
||||
|
||||
protected:
|
||||
|
||||
/** Max amount of HP the character will have on respawn */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
float MaxHP = 3.0f;
|
||||
|
||||
public:
|
||||
|
||||
/** Current amount of HP the character has */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Damage", meta = (ClampMin = 0, ClampMax = 100))
|
||||
float CurrentHP = 0.0f;
|
||||
|
||||
protected:
|
||||
|
||||
/** Name of the pelvis bone, for damage ragdoll physics */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
FName PelvisBoneName;
|
||||
|
||||
/** Pointer to the life bar widget */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
UCombatLifeBar* LifeBarWidget;
|
||||
|
||||
/** If true, the character is currently playing an attack animation */
|
||||
bool bIsAttacking = false;
|
||||
|
||||
/** Distance ahead of the character that melee attack sphere collision traces will extend */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units = "cm"))
|
||||
float MeleeTraceDistance = 75.0f;
|
||||
|
||||
/** Radius of the sphere trace for melee attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units = "cm"))
|
||||
float MeleeTraceRadius = 50.0f;
|
||||
|
||||
/** Amount of damage a melee attack will deal */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 100))
|
||||
float MeleeDamage = 1.0f;
|
||||
|
||||
/** Amount of knockback impulse a melee attack will apply */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
|
||||
float MeleeKnockbackImpulse = 150.0f;
|
||||
|
||||
/** Amount of upwards impulse a melee attack will apply */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
|
||||
float MeleeLaunchImpulse = 350.0f;
|
||||
|
||||
/** AnimMontage that will play for combo attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
|
||||
UAnimMontage* ComboAttackMontage;
|
||||
|
||||
/** Names of the AnimMontage sections that correspond to each stage of the combo attack */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
|
||||
TArray<FName> ComboSectionNames;
|
||||
|
||||
/** Target number of attacks in the combo attack string we're playing */
|
||||
int32 TargetComboCount = 0;
|
||||
|
||||
/** Index of the current stage of the melee attack combo */
|
||||
int32 CurrentComboAttack = 0;
|
||||
|
||||
/** AnimMontage that will play for charged attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
UAnimMontage* ChargedAttackMontage;
|
||||
|
||||
/** Name of the AnimMontage section that corresponds to the charge loop */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
FName ChargeLoopSection;
|
||||
|
||||
/** Name of the AnimMontage section that corresponds to the attack */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
FName ChargeAttackSection;
|
||||
|
||||
/** Minimum number of charge animation loops that will be played by the AI */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged", meta = (ClampMin = 1, ClampMax = 20))
|
||||
int32 MinChargeLoops = 2;
|
||||
|
||||
/** Maximum number of charge animation loops that will be played by the AI */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged", meta = (ClampMin = 1, ClampMax = 20))
|
||||
int32 MaxChargeLoops = 5;
|
||||
|
||||
/** Target number of charge animation loops to play in this charged attack */
|
||||
int32 TargetChargeLoops = 0;
|
||||
|
||||
/** Number of charge animation loop currently playing */
|
||||
int32 CurrentChargeLoop = 0;
|
||||
|
||||
/** Time to wait before removing this character from the level after it dies */
|
||||
UPROPERTY(EditAnywhere, Category="Death")
|
||||
float DeathRemovalTime = 5.0f;
|
||||
|
||||
/** Enemy death timer */
|
||||
FTimerHandle DeathTimer;
|
||||
|
||||
/** Attack montage ended delegate */
|
||||
FOnMontageEnded OnAttackMontageEnded;
|
||||
|
||||
/** Last recorded location we're being attacked from */
|
||||
FVector LastDangerLocation = FVector::ZeroVector;
|
||||
|
||||
/** Last recorded game time we were attacked */
|
||||
float LastDangerTime = -1000.0f;
|
||||
|
||||
public:
|
||||
/** Attack completed internal delegate to notify StateTree tasks */
|
||||
FOnEnemyAttackCompleted OnAttackCompleted;
|
||||
|
||||
/** Landed internal delegate to notify StateTree tasks. We use this instead of the built-in Landed delegate so we can bind to a Lambda in StateTree tasks */
|
||||
FOnEnemyLanded OnEnemyLanded;
|
||||
|
||||
/** Enemy died delegate. Allows external subscribers to respond to enemy death */
|
||||
UPROPERTY(BlueprintAssignable, Category="Events")
|
||||
FOnEnemyDied OnEnemyDied;
|
||||
|
||||
public:
|
||||
|
||||
/** Performs an AI-initiated combo attack. Number of hits will be decided by this character */
|
||||
void DoAIComboAttack();
|
||||
|
||||
/** Performs an AI-initiated charged attack. Charge time will be decided by this character */
|
||||
void DoAIChargedAttack();
|
||||
|
||||
/** Called from a delegate when the attack montage ends */
|
||||
void AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
|
||||
|
||||
/** Returns the last recorded location we were attacked from */
|
||||
const FVector& GetLastDangerLocation() const;
|
||||
|
||||
/** Returns the last game time we were attacked */
|
||||
float GetLastDangerTime() const;
|
||||
|
||||
public:
|
||||
|
||||
// ~begin ICombatAttacker interface
|
||||
|
||||
/** Performs an attack's collision check */
|
||||
virtual void DoAttackTrace(FName DamageSourceBone) override;
|
||||
|
||||
/** Performs a combo attack's check to continue the string */
|
||||
UFUNCTION(BlueprintCallable, Category="Attacker")
|
||||
virtual void CheckCombo() override;
|
||||
|
||||
/** Performs a charged attack's check to loop the charge animation */
|
||||
UFUNCTION(BlueprintCallable, Category="Attacker")
|
||||
virtual void CheckChargedAttack() override;
|
||||
|
||||
// ~end ICombatAttacker interface
|
||||
|
||||
// ~begin ICombatDamageable interface
|
||||
|
||||
/** Handles damage and knockback events */
|
||||
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
|
||||
|
||||
/** Handles death events */
|
||||
virtual void HandleDeath() override;
|
||||
|
||||
/** Handles healing events */
|
||||
virtual void ApplyHealing(float Healing, AActor* Healer) override;
|
||||
|
||||
/** Allows the enemy to react to incoming attacks */
|
||||
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
|
||||
|
||||
// ~end ICombatDamageable interface
|
||||
|
||||
protected:
|
||||
|
||||
/** Removes this character from the level after it dies */
|
||||
void RemoveFromLevel();
|
||||
|
||||
public:
|
||||
|
||||
/** Overrides the default TakeDamage functionality */
|
||||
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
|
||||
|
||||
/** Overrides landing to reset damage ragdoll physics */
|
||||
virtual void Landed(const FHitResult& Hit) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Blueprint handler to play damage received effects */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
|
||||
void ReceivedDamage(float Damage, const FVector& ImpactPoint, const FVector& DamageDirection);
|
||||
|
||||
protected:
|
||||
|
||||
/** Gameplay initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** EndPlay cleanup */
|
||||
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatEnemySpawner.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Components/ArrowComponent.h"
|
||||
#include "TimerManager.h"
|
||||
#include "CombatEnemy.h"
|
||||
|
||||
ACombatEnemySpawner::ACombatEnemySpawner()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the root
|
||||
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
|
||||
// create the reference spawn capsule
|
||||
SpawnCapsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Spawn Capsule"));
|
||||
SpawnCapsule->SetupAttachment(RootComponent);
|
||||
|
||||
SpawnCapsule->SetRelativeLocation(FVector(0.0f, 0.0f, 90.0f));
|
||||
SpawnCapsule->SetCapsuleSize(35.0f, 90.0f);
|
||||
SpawnCapsule->SetCollisionProfileName(FName("NoCollision"));
|
||||
|
||||
SpawnDirection = CreateDefaultSubobject<UArrowComponent>(TEXT("Spawn Direction"));
|
||||
SpawnDirection->SetupAttachment(RootComponent);
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// should we spawn an enemy right away?
|
||||
if (bShouldSpawnEnemiesImmediately)
|
||||
{
|
||||
// schedule the first enemy spawn
|
||||
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnEnemy, InitialSpawnDelay);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the spawn timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(SpawnTimer);
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::SpawnEnemy()
|
||||
{
|
||||
// ensure the enemy class is valid
|
||||
if (IsValid(EnemyClass))
|
||||
{
|
||||
// spawn the enemy at the reference capsule's transform
|
||||
FActorSpawnParameters SpawnParams;
|
||||
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
||||
|
||||
ACombatEnemy* SpawnedEnemy = GetWorld()->SpawnActor<ACombatEnemy>(EnemyClass, SpawnCapsule->GetComponentTransform(), SpawnParams);
|
||||
|
||||
// was the enemy successfully created?
|
||||
if (SpawnedEnemy)
|
||||
{
|
||||
// subscribe to the death delegate
|
||||
SpawnedEnemy->OnEnemyDied.AddDynamic(this, &ACombatEnemySpawner::OnEnemyDied);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::OnEnemyDied()
|
||||
{
|
||||
// decrease the spawn counter
|
||||
--SpawnCount;
|
||||
|
||||
// is this the last enemy we should spawn?
|
||||
if (SpawnCount <= 0)
|
||||
{
|
||||
// schedule the activation on depleted message
|
||||
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnerDepleted, ActivationDelay);
|
||||
return;
|
||||
}
|
||||
|
||||
// schedule the next enemy spawn
|
||||
GetWorld()->GetTimerManager().SetTimer(SpawnTimer, this, &ACombatEnemySpawner::SpawnEnemy, RespawnDelay);
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::SpawnerDepleted()
|
||||
{
|
||||
// process the actors to activate list
|
||||
for (AActor* CurrentActor : ActorsToActivateWhenDepleted)
|
||||
{
|
||||
// check if the actor is activatable
|
||||
if (ICombatActivatable* CombatActivatable = Cast<ICombatActivatable>(CurrentActor))
|
||||
{
|
||||
// activate the actor
|
||||
CombatActivatable->ActivateInteraction(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::ToggleInteraction(AActor* ActivationInstigator)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::ActivateInteraction(AActor* ActivationInstigator)
|
||||
{
|
||||
// ensure we're only activated once, and only if we've deferred enemy spawning
|
||||
if (bHasBeenActivated || bShouldSpawnEnemiesImmediately)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the activation flag
|
||||
bHasBeenActivated = true;
|
||||
|
||||
// spawn the first enemy
|
||||
SpawnEnemy();
|
||||
}
|
||||
|
||||
void ACombatEnemySpawner::DeactivateInteraction(AActor* ActivationInstigator)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CombatActivatable.h"
|
||||
#include "CombatEnemySpawner.generated.h"
|
||||
|
||||
class UCapsuleComponent;
|
||||
class UArrowComponent;
|
||||
class ACombatEnemy;
|
||||
|
||||
/**
|
||||
* A basic Actor in charge of spawning Enemy Characters and monitoring their deaths.
|
||||
* Enemies will be spawned one by one, and the spawner will wait until the enemy dies before spawning a new one.
|
||||
* The spawner can be remotely activated through the ICombatActivatable interface
|
||||
* When the last spawned enemy dies, the spawner can also activate other ICombatActivatables
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatEnemySpawner : public AActor, public ICombatActivatable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UCapsuleComponent* SpawnCapsule;
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UArrowComponent* SpawnDirection;
|
||||
|
||||
protected:
|
||||
|
||||
/** Type of enemy to spawn */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner")
|
||||
TSubclassOf<ACombatEnemy> EnemyClass;
|
||||
|
||||
/** If true, the first enemy will be spawned as soon as the game starts */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner")
|
||||
bool bShouldSpawnEnemiesImmediately = true;
|
||||
|
||||
/** Time to wait before spawning the first enemy on game start */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 10))
|
||||
float InitialSpawnDelay = 5.0f;
|
||||
|
||||
/** Number of enemies to spawn */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 100))
|
||||
int32 SpawnCount = 1;
|
||||
|
||||
/** Time to wait before spawning the next enemy after the current one dies */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Enemy Spawner", meta = (ClampMin = 0, ClampMax = 10))
|
||||
float RespawnDelay = 5.0f;
|
||||
|
||||
/** Time to wait after this spawner is depleted before activating the actor list */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Activation", meta = (ClampMin = 0, ClampMax = 10))
|
||||
float ActivationDelay = 1.0f;
|
||||
|
||||
/** List of actors to activate after the last enemy dies */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Activation")
|
||||
TArray<AActor*> ActorsToActivateWhenDepleted;
|
||||
|
||||
/** Flag to ensure this is only activated once */
|
||||
bool bHasBeenActivated = false;
|
||||
|
||||
/** Timer to spawn enemies after a delay */
|
||||
FTimerHandle SpawnTimer;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatEnemySpawner();
|
||||
|
||||
public:
|
||||
|
||||
/** Initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Cleanup */
|
||||
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Spawn an enemy and subscribe to its death event */
|
||||
void SpawnEnemy();
|
||||
|
||||
/** Called when the spawned enemy has died */
|
||||
UFUNCTION()
|
||||
void OnEnemyDied();
|
||||
|
||||
/** Called after the last spawned enemy has died */
|
||||
void SpawnerDepleted();
|
||||
|
||||
public:
|
||||
|
||||
// ~begin ICombatActivatable interface
|
||||
|
||||
/** Toggles the Spawner */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void ToggleInteraction(AActor* ActivationInstigator) override;
|
||||
|
||||
/** Activates the Spawner */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void ActivateInteraction(AActor* ActivationInstigator) override;
|
||||
|
||||
/** Deactivates the Spawner */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void DeactivateInteraction(AActor* ActivationInstigator) override;
|
||||
|
||||
// ~end IActivatable interface
|
||||
};
|
||||
@@ -1,325 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatStateTreeUtility.h"
|
||||
#include "StateTreeExecutionContext.h"
|
||||
#include "StateTreeExecutionTypes.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "AIController.h"
|
||||
#include "CombatEnemy.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "StateTreeAsyncExecutionContext.h"
|
||||
|
||||
bool FStateTreeCharacterGroundedCondition::TestCondition(FStateTreeExecutionContext& Context) const
|
||||
{
|
||||
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// is the character currently grounded?
|
||||
bool bCondition = InstanceData.Character->GetMovementComponent()->IsMovingOnGround();
|
||||
|
||||
return InstanceData.bMustBeOnAir ? !bCondition : bCondition;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeCharacterGroundedCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Is Character Grounded</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
bool FStateTreeIsInDangerCondition::TestCondition(FStateTreeExecutionContext& Context) const
|
||||
{
|
||||
const FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// ensure we have a valid enemy character
|
||||
if (InstanceData.Character)
|
||||
{
|
||||
// is the last detected danger event within the reaction threshold?
|
||||
const float ReactionDelta = InstanceData.Character->GetWorld()->GetTimeSeconds() - InstanceData.Character->GetLastDangerTime();
|
||||
|
||||
if (ReactionDelta < InstanceData.MaxReactionTime && ReactionDelta > InstanceData.MinReactionTime)
|
||||
{
|
||||
// do a dot product check to determine if the danger location is within the character's detection cone
|
||||
const FVector DangerDir = (InstanceData.Character->GetLastDangerLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal2D();
|
||||
|
||||
const float DangerDot = FVector::DotProduct(DangerDir, InstanceData.Character->GetActorForwardVector());
|
||||
const float ConeAngleCos = FMath::Cos(FMath::DegreesToRadians(InstanceData.DangerSightConeAngle));
|
||||
|
||||
return DangerDot > ConeAngleCos;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeIsInDangerCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Is Character In Danger</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeComboAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// bind to the on attack completed delegate
|
||||
InstanceData.Character->OnAttackCompleted.BindLambda(
|
||||
[WeakContext = Context.MakeWeakExecutionContext()]()
|
||||
{
|
||||
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// tell the character to do a combo attack
|
||||
InstanceData.Character->DoAIComboAttack();
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
void FStateTreeComboAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// unbind the on attack completed delegate
|
||||
InstanceData.Character->OnAttackCompleted.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeComboAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Do Combo Attack</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeChargedAttackTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// bind to the on attack completed delegate
|
||||
InstanceData.Character->OnAttackCompleted.BindLambda(
|
||||
[WeakContext = Context.MakeWeakExecutionContext()]()
|
||||
{
|
||||
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
|
||||
}
|
||||
);
|
||||
|
||||
// tell the character to do a charged attack
|
||||
InstanceData.Character->DoAIChargedAttack();
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
void FStateTreeChargedAttackTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// unbind the on attack completed delegate
|
||||
InstanceData.Character->OnAttackCompleted.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeChargedAttackTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Do Charged Attack</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeWaitForLandingTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// bind to the on enemy landed delegate
|
||||
InstanceData.Character->OnEnemyLanded.BindLambda(
|
||||
[WeakContext = Context.MakeWeakExecutionContext()]()
|
||||
{
|
||||
WeakContext.FinishTask(EStateTreeFinishTaskType::Succeeded);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
void FStateTreeWaitForLandingTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// unbind the on enemy landed delegate
|
||||
InstanceData.Character->OnEnemyLanded.Unbind();
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeWaitForLandingTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Wait for Landing</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// set the AI Controller's focus
|
||||
InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards);
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned to another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// clear the AI Controller's focus
|
||||
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Face Towards Actor</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// set the AI Controller's focus
|
||||
InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation);
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned to another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// clear the AI Controller's focus
|
||||
InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay);
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Face Towards Location</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeSetCharacterSpeedTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// have we transitioned from another state?
|
||||
if (Transition.ChangeType == EStateTreeStateChangeType::Changed)
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// set the character's max ground speed
|
||||
InstanceData.Character->GetCharacterMovement()->MaxWalkSpeed = InstanceData.Speed;
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeSetCharacterSpeedTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Set Character Speed</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
EStateTreeRunStatus FStateTreeGetPlayerInfoTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// get the character possessed by the first local player
|
||||
InstanceData.TargetPlayerCharacter = Cast<ACharacter>(UGameplayStatics::GetPlayerPawn(InstanceData.Character, 0));
|
||||
|
||||
// do we have a valid target?
|
||||
if (InstanceData.TargetPlayerCharacter)
|
||||
{
|
||||
// update the last known location
|
||||
InstanceData.TargetPlayerLocation = InstanceData.TargetPlayerCharacter->GetActorLocation();
|
||||
}
|
||||
|
||||
// update the distance
|
||||
InstanceData.DistanceToTarget = FVector::Distance(InstanceData.TargetPlayerLocation, InstanceData.Character->GetActorLocation());
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeGetPlayerInfoTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Get Player Info</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
@@ -1,365 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "StateTreeTaskBase.h"
|
||||
#include "StateTreeConditionBase.h"
|
||||
|
||||
#include "CombatStateTreeUtility.generated.h"
|
||||
|
||||
class ACharacter;
|
||||
class AAIController;
|
||||
class ACombatEnemy;
|
||||
|
||||
/**
|
||||
* Instance data struct for the FStateTreeCharacterGroundedCondition condition
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeCharacterGroundedConditionInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Character to check grounded status on */
|
||||
UPROPERTY(EditAnywhere, Category = "Context")
|
||||
ACharacter* Character;
|
||||
|
||||
/** If true, the condition passes if the character is not grounded instead */
|
||||
UPROPERTY(EditAnywhere, Category = "Condition")
|
||||
bool bMustBeOnAir = false;
|
||||
};
|
||||
STATETREE_POD_INSTANCEDATA(FStateTreeCharacterGroundedConditionInstanceData);
|
||||
|
||||
/**
|
||||
* StateTree condition to check if the character is grounded
|
||||
*/
|
||||
USTRUCT(DisplayName = "Character is Grounded")
|
||||
struct FStateTreeCharacterGroundedCondition : public FStateTreeConditionCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Set the instance data type */
|
||||
using FInstanceDataType = FStateTreeCharacterGroundedConditionInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Default constructor */
|
||||
FStateTreeCharacterGroundedCondition() = default;
|
||||
|
||||
/** Tests the StateTree condition */
|
||||
virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
/** Provides the description string */
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the FStateTreeIsInDangerCondition condition
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeIsInDangerConditionInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Character to check danger status on */
|
||||
UPROPERTY(EditAnywhere, Category = "Context")
|
||||
ACombatEnemy* Character;
|
||||
|
||||
/** Minimum time to wait before reacting to the danger event */
|
||||
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "s"))
|
||||
float MinReactionTime = 0.35f;
|
||||
|
||||
/** Maximum time to wait before ignoring the danger event */
|
||||
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "s"))
|
||||
float MaxReactionTime = 0.75f;
|
||||
|
||||
/** Line of sight half angle for detecting incoming danger, in degrees*/
|
||||
UPROPERTY(EditAnywhere, Category = "Parameters", meta = (Units = "degrees"))
|
||||
float DangerSightConeAngle = 120.0f;
|
||||
};
|
||||
STATETREE_POD_INSTANCEDATA(FStateTreeIsInDangerConditionInstanceData);
|
||||
|
||||
/**
|
||||
* StateTree condition to check if the character is about to be hit by an attack
|
||||
*/
|
||||
USTRUCT(DisplayName = "Character is in Danger")
|
||||
struct FStateTreeIsInDangerCondition : public FStateTreeConditionCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Set the instance data type */
|
||||
using FInstanceDataType = FStateTreeIsInDangerConditionInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Default constructor */
|
||||
FStateTreeIsInDangerCondition() = default;
|
||||
|
||||
/** Tests the StateTree condition */
|
||||
virtual bool TestCondition(FStateTreeExecutionContext& Context) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
/** Provides the description string */
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the Combat StateTree tasks
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeAttackInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Character that will perform the attack */
|
||||
UPROPERTY(EditAnywhere, Category = Context)
|
||||
TObjectPtr<ACombatEnemy> Character;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to perform a combo attack
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Combo Attack", Category="Combat"))
|
||||
struct FStateTreeComboAttackTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeAttackInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
/** Runs when the owning state is ended */
|
||||
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to perform a charged attack
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Charged Attack", Category="Combat"))
|
||||
struct FStateTreeChargedAttackTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeAttackInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
/** Runs when the owning state is ended */
|
||||
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to wait for the character to land
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Wait for Landing", Category="Combat"))
|
||||
struct FStateTreeWaitForLandingTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeAttackInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
/** Runs when the owning state is ended */
|
||||
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the Face Towards Actor StateTree task
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeFaceActorInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** AI Controller that will determine the focused actor */
|
||||
UPROPERTY(EditAnywhere, Category = Context)
|
||||
TObjectPtr<AAIController> Controller;
|
||||
|
||||
/** Actor that will be faced towards */
|
||||
UPROPERTY(EditAnywhere, Category = Input)
|
||||
TObjectPtr<AActor> ActorToFaceTowards;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to face an AI-Controlled Pawn towards an Actor
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Face Towards Actor", Category="Combat"))
|
||||
struct FStateTreeFaceActorTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeFaceActorInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
/** Runs when the owning state is ended */
|
||||
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the Face Towards Location StateTree task
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeFaceLocationInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** AI Controller that will determine the focused location */
|
||||
UPROPERTY(EditAnywhere, Category = Context)
|
||||
TObjectPtr<AAIController> Controller;
|
||||
|
||||
/** Location that will be faced towards */
|
||||
UPROPERTY(EditAnywhere, Category = Parameter)
|
||||
FVector FaceLocation = FVector::ZeroVector;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to face an AI-Controlled Pawn towards a world location
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Face Towards Location", Category="Combat"))
|
||||
struct FStateTreeFaceLocationTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeFaceLocationInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
/** Runs when the owning state is ended */
|
||||
virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the Set Character Speed StateTree task
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeSetCharacterSpeedInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Character that will be affected */
|
||||
UPROPERTY(EditAnywhere, Category = Context)
|
||||
TObjectPtr<ACharacter> Character;
|
||||
|
||||
/** Max ground speed to set for the character */
|
||||
UPROPERTY(EditAnywhere, Category = Parameter)
|
||||
float Speed = 600.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to change a Character's ground speed
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Set Character Speed", Category="Combat"))
|
||||
struct FStateTreeSetCharacterSpeedTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeSetCharacterSpeedInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs when the owning state is entered */
|
||||
virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Instance data struct for the Get Player Info task
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeGetPlayerInfoInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Character that owns this task */
|
||||
UPROPERTY(EditAnywhere, Category = Context)
|
||||
TObjectPtr<ACharacter> Character;
|
||||
|
||||
/** Character that owns this task */
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<ACharacter> TargetPlayerCharacter;
|
||||
|
||||
/** Last known location for the target */
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
FVector TargetPlayerLocation = FVector::ZeroVector;
|
||||
|
||||
/** Distance to the target */
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
float DistanceToTarget = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to get information about the player character
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="GetPlayerInfo", Category="Combat"))
|
||||
struct FStateTreeGetPlayerInfoTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeGetPlayerInfoInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs while the owning state is active */
|
||||
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Combat/AI/EnvQueryContext_Danger.h"
|
||||
#include "Variant_Combat/AI/CombatEnemy.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Point.h"
|
||||
|
||||
void UEnvQueryContext_Danger::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
|
||||
{
|
||||
// get the querying enemy
|
||||
if (ACombatEnemy* QuerierActor = Cast<ACombatEnemy>(QueryInstance.Owner.Get()))
|
||||
{
|
||||
// add the last recorded danger location to the context
|
||||
UEnvQueryItemType_Point::SetContextHelper(ContextData, QuerierActor->GetLastDangerLocation());
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryContext.h"
|
||||
#include "EnvQueryContext_Danger.generated.h"
|
||||
|
||||
/**
|
||||
* UEnvQueryContext_Danger
|
||||
* Returns the enemy character's last known danger location
|
||||
*/
|
||||
UCLASS()
|
||||
class AGRARIANGAME_API UEnvQueryContext_Danger : public UEnvQueryContext
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Provides the context locations or actors for this EnvQuery */
|
||||
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
|
||||
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "EnvQueryContext_Player.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "EnvironmentQuery/EnvQueryTypes.h"
|
||||
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
void UEnvQueryContext_Player::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
|
||||
{
|
||||
// get the player pawn for the first local player
|
||||
AActor* PlayerPawn = UGameplayStatics::GetPlayerPawn(QueryInstance.Owner.Get(), 0);
|
||||
check(PlayerPawn);
|
||||
|
||||
// add the actor data to the context
|
||||
UEnvQueryItemType_Actor::SetContextHelper(ContextData, PlayerPawn);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "EnvironmentQuery/EnvQueryContext.h"
|
||||
#include "EnvQueryContext_Player.generated.h"
|
||||
|
||||
/**
|
||||
* UEnvQueryContext_Player
|
||||
* Basic EnvQuery Context that returns the first local player
|
||||
*/
|
||||
UCLASS()
|
||||
class UEnvQueryContext_Player : public UEnvQueryContext
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Provides the context locations or actors for this EnvQuery */
|
||||
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "AnimNotify_CheckChargedAttack.h"
|
||||
#include "CombatAttacker.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
|
||||
void UAnimNotify_CheckChargedAttack::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
|
||||
{
|
||||
// cast the owner to the attacker interface
|
||||
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
|
||||
{
|
||||
// tell the actor to check for a charged attack loop
|
||||
AttackerInterface->CheckChargedAttack();
|
||||
}
|
||||
}
|
||||
|
||||
FString UAnimNotify_CheckChargedAttack::GetNotifyName_Implementation() const
|
||||
{
|
||||
return FString("Check Charged Attack");
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Animation/AnimNotifies/AnimNotify.h"
|
||||
#include "AnimNotify_CheckChargedAttack.generated.h"
|
||||
|
||||
/**
|
||||
* AnimNotify to perform a charged attack hold check.
|
||||
*/
|
||||
UCLASS()
|
||||
class UAnimNotify_CheckChargedAttack : public UAnimNotify
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Perform the Anim Notify */
|
||||
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
|
||||
|
||||
/** Get the notify name */
|
||||
virtual FString GetNotifyName_Implementation() const override;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "AnimNotify_CheckCombo.h"
|
||||
#include "CombatAttacker.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
|
||||
void UAnimNotify_CheckCombo::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
|
||||
{
|
||||
// cast the owner to the attacker interface
|
||||
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
|
||||
{
|
||||
// tell the actor to check for combo string
|
||||
AttackerInterface->CheckCombo();
|
||||
}
|
||||
}
|
||||
|
||||
FString UAnimNotify_CheckCombo::GetNotifyName_Implementation() const
|
||||
{
|
||||
return FString("Check Combo String");
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Animation/AnimNotifies/AnimNotify.h"
|
||||
#include "AnimNotify_CheckCombo.generated.h"
|
||||
|
||||
/**
|
||||
* AnimNotify to perform a combo string check.
|
||||
*/
|
||||
UCLASS()
|
||||
class UAnimNotify_CheckCombo : public UAnimNotify
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Perform the Anim Notify */
|
||||
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
|
||||
|
||||
/** Get the notify name */
|
||||
virtual FString GetNotifyName_Implementation() const override;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "AnimNotify_DoAttackTrace.h"
|
||||
#include "CombatAttacker.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
|
||||
void UAnimNotify_DoAttackTrace::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
|
||||
{
|
||||
// cast the owner to the attacker interface
|
||||
if (ICombatAttacker* AttackerInterface = Cast<ICombatAttacker>(MeshComp->GetOwner()))
|
||||
{
|
||||
AttackerInterface->DoAttackTrace(AttackBoneName);
|
||||
}
|
||||
}
|
||||
|
||||
FString UAnimNotify_DoAttackTrace::GetNotifyName_Implementation() const
|
||||
{
|
||||
return FString("Do Attack Trace");
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Animation/AnimNotifies/AnimNotify.h"
|
||||
#include "AnimNotify_DoAttackTrace.generated.h"
|
||||
|
||||
/**
|
||||
* AnimNotify to tell the actor to perform an attack trace check to look for targets to damage.
|
||||
*/
|
||||
UCLASS()
|
||||
class UAnimNotify_DoAttackTrace : public UAnimNotify
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Source bone for the attack trace */
|
||||
UPROPERTY(EditAnywhere, Category="Attack")
|
||||
FName AttackBoneName;
|
||||
|
||||
public:
|
||||
|
||||
/** Perform the Anim Notify */
|
||||
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
|
||||
|
||||
/** Get the notify name */
|
||||
virtual FString GetNotifyName_Implementation() const override;
|
||||
};
|
||||
@@ -1,547 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatCharacter.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Components/WidgetComponent.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "CombatLifeBar.h"
|
||||
#include "Engine/DamageEvents.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "CombatPlayerController.h"
|
||||
|
||||
ACombatCharacter::ACombatCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// bind the attack montage ended delegate
|
||||
OnAttackMontageEnded.BindUObject(this, &ACombatCharacter::AttackMontageEnded);
|
||||
|
||||
// Set size for collision capsule
|
||||
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
|
||||
|
||||
// Configure character movement
|
||||
GetCharacterMovement()->MaxWalkSpeed = 400.0f;
|
||||
|
||||
// create the camera boom
|
||||
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
||||
CameraBoom->SetupAttachment(RootComponent);
|
||||
|
||||
CameraBoom->TargetArmLength = DefaultCameraDistance;
|
||||
CameraBoom->bUsePawnControlRotation = true;
|
||||
CameraBoom->bEnableCameraLag = true;
|
||||
CameraBoom->bEnableCameraRotationLag = true;
|
||||
|
||||
// create the orbiting camera
|
||||
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||
FollowCamera->bUsePawnControlRotation = false;
|
||||
|
||||
// create the life bar widget component
|
||||
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
|
||||
LifeBar->SetupAttachment(RootComponent);
|
||||
|
||||
// set the player tag
|
||||
Tags.Add(FName("Player"));
|
||||
}
|
||||
|
||||
void ACombatCharacter::Move(const FInputActionValue& Value)
|
||||
{
|
||||
// input is a Vector2D
|
||||
FVector2D MovementVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoMove(MovementVector.X, MovementVector.Y);
|
||||
}
|
||||
|
||||
void ACombatCharacter::Look(const FInputActionValue& Value)
|
||||
{
|
||||
FVector2D LookAxisVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
||||
}
|
||||
|
||||
void ACombatCharacter::ComboAttackPressed()
|
||||
{
|
||||
// route the input
|
||||
DoComboAttackStart();
|
||||
}
|
||||
|
||||
void ACombatCharacter::ChargedAttackPressed()
|
||||
{
|
||||
// route the input
|
||||
DoChargedAttackStart();
|
||||
}
|
||||
|
||||
void ACombatCharacter::ChargedAttackReleased()
|
||||
{
|
||||
// route the input
|
||||
DoChargedAttackEnd();
|
||||
}
|
||||
|
||||
void ACombatCharacter::ToggleCamera()
|
||||
{
|
||||
// call the BP hook
|
||||
BP_ToggleCamera();
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoMove(float Right, float Forward)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
{
|
||||
// find out which way is forward
|
||||
const FRotator Rotation = GetController()->GetControlRotation();
|
||||
const FRotator YawRotation(0, Rotation.Yaw, 0);
|
||||
|
||||
// get forward vector
|
||||
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
|
||||
|
||||
// get right vector
|
||||
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
|
||||
|
||||
// add movement
|
||||
AddMovementInput(ForwardDirection, Forward);
|
||||
AddMovementInput(RightDirection, Right);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoLook(float Yaw, float Pitch)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
{
|
||||
// add yaw and pitch input to controller
|
||||
AddControllerYawInput(Yaw);
|
||||
AddControllerPitchInput(Pitch);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoComboAttackStart()
|
||||
{
|
||||
// are we already playing an attack animation?
|
||||
if (bIsAttacking)
|
||||
{
|
||||
// cache the input time so we can check it later
|
||||
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// perform a combo attack
|
||||
ComboAttack();
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoComboAttackEnd()
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoChargedAttackStart()
|
||||
{
|
||||
// raise the charging attack flag
|
||||
bIsChargingAttack = true;
|
||||
|
||||
if (bIsAttacking)
|
||||
{
|
||||
// cache the input time so we can check it later
|
||||
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ChargedAttack();
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoChargedAttackEnd()
|
||||
{
|
||||
// lower the charging attack flag
|
||||
bIsChargingAttack = false;
|
||||
|
||||
// if we've done the charge loop at least once, release the charged attack right away
|
||||
if (bHasLoopedChargedAttack)
|
||||
{
|
||||
CheckChargedAttack();
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::ResetHP()
|
||||
{
|
||||
// reset the current HP total
|
||||
CurrentHP = MaxHP;
|
||||
|
||||
// update the life bar
|
||||
LifeBarWidget->SetLifePercentage(1.0f);
|
||||
}
|
||||
|
||||
void ACombatCharacter::ComboAttack()
|
||||
{
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// reset the combo count
|
||||
ComboCount = 0;
|
||||
|
||||
// notify enemies they are about to be attacked
|
||||
NotifyEnemiesOfIncomingAttack();
|
||||
|
||||
// play the attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ACombatCharacter::ChargedAttack()
|
||||
{
|
||||
// raise the attacking flag
|
||||
bIsAttacking = true;
|
||||
|
||||
// reset the charge loop flag
|
||||
bHasLoopedChargedAttack = false;
|
||||
|
||||
// notify enemies they are about to be attacked
|
||||
NotifyEnemiesOfIncomingAttack();
|
||||
|
||||
// play the charged attack montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// subscribe to montage completed and interrupted events
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
// set the end delegate for the montage
|
||||
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
|
||||
{
|
||||
// reset the attacking flag
|
||||
bIsAttacking = false;
|
||||
|
||||
// check if we have a non-stale cached input
|
||||
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= AttackInputCacheTimeTolerance)
|
||||
{
|
||||
// are we holding the charged attack button?
|
||||
if (bIsChargingAttack)
|
||||
{
|
||||
// do a charged attack
|
||||
ChargedAttack();
|
||||
}
|
||||
else
|
||||
{
|
||||
// do a regular attack
|
||||
ComboAttack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::DoAttackTrace(FName DamageSourceBone)
|
||||
{
|
||||
// sweep for objects in front of the character to be hit by the attack
|
||||
TArray<FHitResult> OutHits;
|
||||
|
||||
// start at the provided socket location, sweep forward
|
||||
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
|
||||
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
|
||||
|
||||
// check for pawn and world dynamic collision object types
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
|
||||
|
||||
// use a sphere shape for the sweep
|
||||
FCollisionShape CollisionShape;
|
||||
CollisionShape.SetSphere(MeleeTraceRadius);
|
||||
|
||||
// ignore self
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
||||
{
|
||||
// iterate over each object hit
|
||||
for (const FHitResult& CurrentHit : OutHits)
|
||||
{
|
||||
// check if we've hit a damageable actor
|
||||
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
||||
|
||||
if (Damageable)
|
||||
{
|
||||
// knock upwards and away from the impact normal
|
||||
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
|
||||
|
||||
// pass the damage event to the actor
|
||||
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
|
||||
|
||||
// call the BP handler to play effects, etc.
|
||||
DealtDamage(MeleeDamage, CurrentHit.ImpactPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::CheckCombo()
|
||||
{
|
||||
// are we playing a non-charge attack animation?
|
||||
if (bIsAttacking && !bIsChargingAttack)
|
||||
{
|
||||
// is the last attack input not stale?
|
||||
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= ComboInputCacheTimeTolerance)
|
||||
{
|
||||
// consume the attack input so we don't accidentally trigger it twice
|
||||
CachedAttackInputTime = 0.0f;
|
||||
|
||||
// increase the combo counter
|
||||
++ComboCount;
|
||||
|
||||
// do we still have a combo section to play?
|
||||
if (ComboCount < ComboSectionNames.Num())
|
||||
{
|
||||
// notify enemies they are about to be attacked
|
||||
NotifyEnemiesOfIncomingAttack();
|
||||
|
||||
// jump to the next combo section
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(ComboSectionNames[ComboCount], ComboAttackMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::CheckChargedAttack()
|
||||
{
|
||||
// raise the looped charged attack flag
|
||||
bHasLoopedChargedAttack = true;
|
||||
|
||||
// jump to either the loop or the attack section depending on whether we're still holding the charge button
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
AnimInstance->Montage_JumpToSection(bIsChargingAttack ? ChargeLoopSection : ChargeAttackSection, ChargedAttackMontage);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::NotifyEnemiesOfIncomingAttack()
|
||||
{
|
||||
// sweep for objects in front of the character to be hit by the attack
|
||||
TArray<FHitResult> OutHits;
|
||||
|
||||
// start at the actor location, sweep forward
|
||||
const FVector TraceStart = GetActorLocation();
|
||||
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * DangerTraceDistance);
|
||||
|
||||
// check for pawn object types only
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
||||
|
||||
// use a sphere shape for the sweep
|
||||
FCollisionShape CollisionShape;
|
||||
CollisionShape.SetSphere(DangerTraceRadius);
|
||||
|
||||
// ignore self
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
||||
{
|
||||
// iterate over each object hit
|
||||
for (const FHitResult& CurrentHit : OutHits)
|
||||
{
|
||||
// check if we've hit a damageable actor
|
||||
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
||||
|
||||
if (Damageable)
|
||||
{
|
||||
// notify the enemy
|
||||
Damageable->NotifyDanger(GetActorLocation(), this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
||||
{
|
||||
// pass the damage event to the actor
|
||||
FDamageEvent DamageEvent;
|
||||
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
|
||||
|
||||
// only process knockback and effects if we received nonzero damage
|
||||
if (ActualDamage > 0.0f)
|
||||
{
|
||||
// apply the knockback impulse
|
||||
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
|
||||
|
||||
// is the character ragdolling?
|
||||
if (GetMesh()->IsSimulatingPhysics())
|
||||
{
|
||||
// apply an impulse to the ragdoll
|
||||
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
|
||||
}
|
||||
|
||||
// pass control to BP to play effects, etc.
|
||||
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ACombatCharacter::HandleDeath()
|
||||
{
|
||||
// disable movement while we're dead
|
||||
GetCharacterMovement()->DisableMovement();
|
||||
|
||||
// enable full ragdoll physics
|
||||
GetMesh()->SetSimulatePhysics(true);
|
||||
|
||||
// hide the life bar
|
||||
LifeBar->SetHiddenInGame(true);
|
||||
|
||||
// pull back the camera
|
||||
GetCameraBoom()->TargetArmLength = DeathCameraDistance;
|
||||
|
||||
// schedule respawning
|
||||
GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &ACombatCharacter::RespawnCharacter, RespawnTime, false);
|
||||
}
|
||||
|
||||
void ACombatCharacter::ApplyHealing(float Healing, AActor* Healer)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatCharacter::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatCharacter::RespawnCharacter()
|
||||
{
|
||||
// destroy the character and let it be respawned by the Player Controller
|
||||
Destroy();
|
||||
}
|
||||
|
||||
float ACombatCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
|
||||
{
|
||||
// only process damage if the character is still alive
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// reduce the current HP
|
||||
CurrentHP -= Damage;
|
||||
|
||||
// have we run out of HP?
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
// die
|
||||
HandleDeath();
|
||||
}
|
||||
else
|
||||
{
|
||||
// update the life bar
|
||||
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
|
||||
|
||||
// enable partial ragdoll physics, but keep the pelvis vertical
|
||||
GetMesh()->SetPhysicsBlendWeight(0.5f);
|
||||
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
|
||||
}
|
||||
|
||||
// return the received damage amount
|
||||
return Damage;
|
||||
}
|
||||
|
||||
void ACombatCharacter::Landed(const FHitResult& Hit)
|
||||
{
|
||||
Super::Landed(Hit);
|
||||
|
||||
// is the character still alive?
|
||||
if (CurrentHP >= 0.0f)
|
||||
{
|
||||
// disable ragdoll physics
|
||||
GetMesh()->SetPhysicsBlendWeight(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// get the life bar from the widget component
|
||||
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
|
||||
check(LifeBarWidget);
|
||||
|
||||
// initialize the camera
|
||||
GetCameraBoom()->TargetArmLength = DefaultCameraDistance;
|
||||
|
||||
// save the relative transform for the mesh so we can reset the ragdoll later
|
||||
MeshStartingTransform = GetMesh()->GetRelativeTransform();
|
||||
|
||||
// set the life bar color
|
||||
LifeBarWidget->SetBarColor(LifeBarColor);
|
||||
|
||||
// reset HP to maximum
|
||||
ResetHP();
|
||||
}
|
||||
|
||||
void ACombatCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the respawn timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(RespawnTimer);
|
||||
}
|
||||
|
||||
void ACombatCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
||||
|
||||
// Set up action bindings
|
||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
|
||||
{
|
||||
// Moving
|
||||
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Move);
|
||||
|
||||
// Looking
|
||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
|
||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
|
||||
|
||||
// Combo Attack
|
||||
EnhancedInputComponent->BindAction(ComboAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ComboAttackPressed);
|
||||
|
||||
// Charged Attack
|
||||
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ChargedAttackPressed);
|
||||
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Completed, this, &ACombatCharacter::ChargedAttackReleased);
|
||||
|
||||
// Camera Side Toggle
|
||||
EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Triggered, this, &ACombatCharacter::ToggleCamera);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatCharacter::NotifyControllerChanged()
|
||||
{
|
||||
Super::NotifyControllerChanged();
|
||||
|
||||
// update the respawn transform on the Player Controller
|
||||
if (ACombatPlayerController* PC = Cast<ACombatPlayerController>(GetController()))
|
||||
{
|
||||
PC->SetRespawnTransform(GetActorTransform());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "CombatAttacker.h"
|
||||
#include "CombatDamageable.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "CombatCharacter.generated.h"
|
||||
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
struct FInputActionValue;
|
||||
class UCombatLifeBar;
|
||||
class UWidgetComponent;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogCombatCharacter, Log, All);
|
||||
|
||||
/**
|
||||
* An enhanced Third Person Character with melee combat capabilities:
|
||||
* - Combo attack string
|
||||
* - Press and hold charged attack
|
||||
* - Damage dealing and reaction
|
||||
* - Death
|
||||
* - Respawning
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatCharacter : public ACharacter, public ICombatAttacker, public ICombatDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Camera boom positioning the camera behind the character */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
USpringArmComponent* CameraBoom;
|
||||
|
||||
/** Follow camera */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UCameraComponent* FollowCamera;
|
||||
|
||||
/** Life bar widget component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UWidgetComponent* LifeBar;
|
||||
|
||||
protected:
|
||||
|
||||
/** Jump Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* JumpAction;
|
||||
|
||||
/** Move Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* MoveAction;
|
||||
|
||||
/** Look Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* LookAction;
|
||||
|
||||
/** Mouse Look Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* MouseLookAction;
|
||||
|
||||
/** Combo Attack Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* ComboAttackAction;
|
||||
|
||||
/** Charged Attack Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* ChargedAttackAction;
|
||||
|
||||
/** Toggle Camera Side Input Action */
|
||||
UPROPERTY(EditAnywhere, Category ="Input")
|
||||
UInputAction* ToggleCameraAction;
|
||||
|
||||
/** Max amount of HP the character will have on respawn */
|
||||
UPROPERTY(EditAnywhere, Category="Damage", meta = (ClampMin = 0, ClampMax = 100))
|
||||
float MaxHP = 5.0f;
|
||||
|
||||
/** Current amount of HP the character has */
|
||||
UPROPERTY(VisibleAnywhere, Category="Damage")
|
||||
float CurrentHP = 0.0f;
|
||||
|
||||
/** Life bar widget fill color */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
FLinearColor LifeBarColor;
|
||||
|
||||
/** Name of the pelvis bone, for damage ragdoll physics */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
FName PelvisBoneName;
|
||||
|
||||
/** Pointer to the life bar widget */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
TObjectPtr<UCombatLifeBar> LifeBarWidget;
|
||||
|
||||
/** Max amount of time that may elapse for a non-combo attack input to not be considered stale */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
|
||||
float AttackInputCacheTimeTolerance = 1.0f;
|
||||
|
||||
/** Time at which an attack button was last pressed */
|
||||
float CachedAttackInputTime = 0.0f;
|
||||
|
||||
/** If true, the character is currently playing an attack animation */
|
||||
bool bIsAttacking = false;
|
||||
|
||||
/** Distance ahead of the character that melee attack sphere collision traces will extend */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units="cm"))
|
||||
float MeleeTraceDistance = 75.0f;
|
||||
|
||||
/** Radius of the sphere trace for melee attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 200, Units = "cm"))
|
||||
float MeleeTraceRadius = 75.0f;
|
||||
|
||||
/** Distance ahead of the character that enemies will be notified of incoming attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 500, Units="cm"))
|
||||
float DangerTraceDistance = 300.0f;
|
||||
|
||||
/** Radius of the sphere trace to notify enemies of incoming attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Trace", meta = (ClampMin = 0, ClampMax = 200, Units = "cm"))
|
||||
float DangerTraceRadius = 100.0f;
|
||||
|
||||
/** Amount of damage a melee attack will deal */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 100))
|
||||
float MeleeDamage = 1.0f;
|
||||
|
||||
/** Amount of knockback impulse a melee attack will apply */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
|
||||
float MeleeKnockbackImpulse = 250.0f;
|
||||
|
||||
/** Amount of upwards impulse a melee attack will apply */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Damage", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm/s"))
|
||||
float MeleeLaunchImpulse = 300.0f;
|
||||
|
||||
/** AnimMontage that will play for combo attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
|
||||
UAnimMontage* ComboAttackMontage;
|
||||
|
||||
/** Names of the AnimMontage sections that correspond to each stage of the combo attack */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo")
|
||||
TArray<FName> ComboSectionNames;
|
||||
|
||||
/** Max amount of time that may elapse for a combo attack input to not be considered stale */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Combo", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
|
||||
float ComboInputCacheTimeTolerance = 0.45f;
|
||||
|
||||
/** Index of the current stage of the melee attack combo */
|
||||
int32 ComboCount = 0;
|
||||
|
||||
/** AnimMontage that will play for charged attacks */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
UAnimMontage* ChargedAttackMontage;
|
||||
|
||||
/** Name of the AnimMontage section that corresponds to the charge loop */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
FName ChargeLoopSection;
|
||||
|
||||
/** Name of the AnimMontage section that corresponds to the attack */
|
||||
UPROPERTY(EditAnywhere, Category="Melee Attack|Charged")
|
||||
FName ChargeAttackSection;
|
||||
|
||||
/** Flag that determines if the player is currently holding the charged attack input */
|
||||
bool bIsChargingAttack = false;
|
||||
|
||||
/** If true, the charged attack hold check has been tested at least once */
|
||||
bool bHasLoopedChargedAttack = false;
|
||||
|
||||
/** Camera boom length while the character is dead */
|
||||
UPROPERTY(EditAnywhere, Category="Camera", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
|
||||
float DeathCameraDistance = 400.0f;
|
||||
|
||||
/** Camera boom length when the character respawns */
|
||||
UPROPERTY(EditAnywhere, Category="Camera", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
|
||||
float DefaultCameraDistance = 100.0f;
|
||||
|
||||
/** Time to wait before respawning the character */
|
||||
UPROPERTY(EditAnywhere, Category="Respawn", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
|
||||
float RespawnTime = 3.0f;
|
||||
|
||||
/** Attack montage ended delegate */
|
||||
FOnMontageEnded OnAttackMontageEnded;
|
||||
|
||||
/** Character respawn timer */
|
||||
FTimerHandle RespawnTimer;
|
||||
|
||||
/** Copy of the mesh's transform so we can reset it after ragdoll animations */
|
||||
FTransform MeshStartingTransform;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatCharacter();
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for looking input */
|
||||
void Look(const FInputActionValue& Value);
|
||||
|
||||
/** Called for combo attack input */
|
||||
void ComboAttackPressed();
|
||||
|
||||
/** Called for combo attack input pressed */
|
||||
void ChargedAttackPressed();
|
||||
|
||||
/** Called for combo attack input released */
|
||||
void ChargedAttackReleased();
|
||||
|
||||
/** Called for toggle camera side input */
|
||||
void ToggleCamera();
|
||||
|
||||
/** BP hook to animate the camera side switch */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
|
||||
void BP_ToggleCamera();
|
||||
|
||||
public:
|
||||
|
||||
/** Handles move inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoMove(float Right, float Forward);
|
||||
|
||||
/** Handles look inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoLook(float Yaw, float Pitch);
|
||||
|
||||
/** Handles combo attack pressed from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoComboAttackStart();
|
||||
|
||||
/** Handles combo attack released from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoComboAttackEnd();
|
||||
|
||||
/** Handles charged attack pressed from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoChargedAttackStart();
|
||||
|
||||
/** Handles charged attack released from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoChargedAttackEnd();
|
||||
|
||||
protected:
|
||||
|
||||
/** Resets the character's current HP to maximum */
|
||||
void ResetHP();
|
||||
|
||||
/** Performs a combo attack */
|
||||
void ComboAttack();
|
||||
|
||||
/** Performs a charged attack */
|
||||
void ChargedAttack();
|
||||
|
||||
/** Called from a delegate when the attack montage ends */
|
||||
void AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted);
|
||||
|
||||
|
||||
public:
|
||||
|
||||
// ~begin CombatAttacker interface
|
||||
|
||||
/** Performs the collision check for an attack */
|
||||
virtual void DoAttackTrace(FName DamageSourceBone) override;
|
||||
|
||||
/** Performs the combo string check */
|
||||
virtual void CheckCombo() override;
|
||||
|
||||
/** Performs the charged attack hold check */
|
||||
virtual void CheckChargedAttack() override;
|
||||
|
||||
// ~end CombatAttacker interface
|
||||
|
||||
// ~begin CombatDamageable interface
|
||||
|
||||
/** Notifies nearby enemies that an attack is coming so they can react */
|
||||
void NotifyEnemiesOfIncomingAttack();
|
||||
|
||||
/** Handles damage and knockback events */
|
||||
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
|
||||
|
||||
/** Handles death events */
|
||||
virtual void HandleDeath() override;
|
||||
|
||||
/** Handles healing events */
|
||||
virtual void ApplyHealing(float Healing, AActor* Healer) override;
|
||||
|
||||
/** Allows reaction to incoming attacks */
|
||||
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
|
||||
|
||||
// ~end CombatDamageable interface
|
||||
|
||||
/** Called from the respawn timer to destroy and re-create the character */
|
||||
void RespawnCharacter();
|
||||
|
||||
public:
|
||||
|
||||
/** Overrides the default TakeDamage functionality */
|
||||
virtual float TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
|
||||
|
||||
/** Overrides landing to reset damage ragdoll physics */
|
||||
virtual void Landed(const FHitResult& Hit) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Blueprint handler to play damage dealt effects */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
|
||||
void DealtDamage(float Damage, const FVector& ImpactPoint);
|
||||
|
||||
/** Blueprint handler to play damage received effects */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Combat")
|
||||
void ReceivedDamage(float Damage, const FVector& ImpactPoint, const FVector& DamageDirection);
|
||||
|
||||
protected:
|
||||
|
||||
/** Initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Cleanup */
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/** Handles input bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
/** Handles possessed initialization */
|
||||
virtual void NotifyControllerChanged() override;
|
||||
|
||||
public:
|
||||
|
||||
/** Returns CameraBoom subobject **/
|
||||
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
|
||||
|
||||
/** Returns FollowCamera subobject **/
|
||||
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Combat/CombatGameMode.h"
|
||||
|
||||
ACombatGameMode::ACombatGameMode()
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "CombatGameMode.generated.h"
|
||||
|
||||
/**
|
||||
* Simple GameMode for a third person combat game
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
ACombatGameMode();
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Combat/CombatPlayerController.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerStart.h"
|
||||
#include "CombatCharacter.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "AgrarianGame.h"
|
||||
#include "Widgets/Input/SVirtualJoystick.h"
|
||||
|
||||
void ACombatPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// only spawn touch controls on local player controllers
|
||||
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
||||
{
|
||||
// spawn the mobile controls widget
|
||||
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
|
||||
|
||||
if (MobileControlsWidget)
|
||||
{
|
||||
// add the controls to the player screen
|
||||
MobileControlsWidget->AddToPlayerScreen(0);
|
||||
|
||||
} else {
|
||||
|
||||
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
// only add IMCs for local player controllers
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
// add the input mapping context
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
|
||||
// only add these IMCs if we're not using mobile touch input
|
||||
if (!ShouldUseTouchControls())
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatPlayerController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
// subscribe to the pawn's OnDestroyed delegate
|
||||
InPawn->OnDestroyed.AddDynamic(this, &ACombatPlayerController::OnPawnDestroyed);
|
||||
}
|
||||
|
||||
void ACombatPlayerController::SetRespawnTransform(const FTransform& NewRespawn)
|
||||
{
|
||||
// save the new respawn transform
|
||||
RespawnTransform = NewRespawn;
|
||||
}
|
||||
|
||||
void ACombatPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
|
||||
{
|
||||
// spawn a new character at the respawn transform
|
||||
if (ACombatCharacter* RespawnedCharacter = GetWorld()->SpawnActor<ACombatCharacter>(CharacterClass, RespawnTransform))
|
||||
{
|
||||
// possess the character
|
||||
Possess(RespawnedCharacter);
|
||||
}
|
||||
}
|
||||
|
||||
bool ACombatPlayerController::ShouldUseTouchControls() const
|
||||
{
|
||||
// are we on a mobile platform? Should we force touch?
|
||||
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "CombatPlayerController.generated.h"
|
||||
|
||||
class UInputMappingContext;
|
||||
class ACombatCharacter;
|
||||
|
||||
/**
|
||||
* Simple Player Controller for a third person combat game
|
||||
* Manages input mappings
|
||||
* Respawns the player character at the checkpoint when it's destroyed
|
||||
*/
|
||||
UCLASS(abstract, Config="Game")
|
||||
class ACombatPlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Input mapping context for this player */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> DefaultMappingContexts;
|
||||
|
||||
/** Input Mapping Contexts */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
|
||||
|
||||
/** Mobile controls widget to spawn */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
|
||||
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
|
||||
|
||||
/** Pointer to the mobile controls widget */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UUserWidget> MobileControlsWidget;
|
||||
|
||||
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
|
||||
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
||||
bool bForceTouchControls = false;
|
||||
|
||||
/** Character class to respawn when the possessed pawn is destroyed */
|
||||
UPROPERTY(EditAnywhere, Category="Respawn")
|
||||
TSubclassOf<ACombatCharacter> CharacterClass;
|
||||
|
||||
/** Transform to respawn the character at. Can be set to create checkpoints */
|
||||
FTransform RespawnTransform;
|
||||
|
||||
protected:
|
||||
|
||||
/** Gameplay initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Initialize input bindings */
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
/** Pawn initialization */
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
public:
|
||||
|
||||
/** Updates the character respawn transform */
|
||||
void SetRespawnTransform(const FTransform& NewRespawn);
|
||||
|
||||
protected:
|
||||
|
||||
/** Called if the possessed pawn is destroyed */
|
||||
UFUNCTION()
|
||||
void OnPawnDestroyed(AActor* DestroyedActor);
|
||||
|
||||
/** Returns true if the player should use UMG touch controls */
|
||||
bool ShouldUseTouchControls() const;
|
||||
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatActivationVolume.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "CombatActivatable.h"
|
||||
|
||||
ACombatActivationVolume::ACombatActivationVolume()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the box volume
|
||||
RootComponent = Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
|
||||
check(Box);
|
||||
|
||||
// set the box's extent
|
||||
Box->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
|
||||
|
||||
// set the default collision profile to overlap all dynamic
|
||||
Box->SetCollisionProfileName(FName("OverlapAllDynamic"));
|
||||
|
||||
// bind the begin overlap
|
||||
Box->OnComponentBeginOverlap.AddDynamic(this, &ACombatActivationVolume::OnOverlap);
|
||||
}
|
||||
|
||||
void ACombatActivationVolume::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
// has a Character entered the volume?
|
||||
ACharacter* PlayerCharacter = Cast<ACharacter>(OtherActor);
|
||||
|
||||
if (PlayerCharacter)
|
||||
{
|
||||
// is the Character controlled by a player
|
||||
if (PlayerCharacter->IsPlayerControlled())
|
||||
{
|
||||
// process the actors to activate list
|
||||
for (AActor* CurrentActor : ActorsToActivate)
|
||||
{
|
||||
// is the referenced actor activatable?
|
||||
if(ICombatActivatable* Activatable = Cast<ICombatActivatable>(CurrentActor))
|
||||
{
|
||||
Activatable->ActivateInteraction(PlayerCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CombatActivationVolume.generated.h"
|
||||
|
||||
class UBoxComponent;
|
||||
|
||||
/**
|
||||
* A simple volume that activates a list of actors when the player pawn enters.
|
||||
*/
|
||||
UCLASS()
|
||||
class ACombatActivationVolume : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Collision box volume */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UBoxComponent* Box;
|
||||
|
||||
protected:
|
||||
|
||||
/** List of actors to activate when this volume is entered */
|
||||
UPROPERTY(EditAnywhere, Category="Activation Volume")
|
||||
TArray<AActor*> ActorsToActivate;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatActivationVolume();
|
||||
|
||||
protected:
|
||||
|
||||
/** Handles overlaps with the box volume */
|
||||
UFUNCTION()
|
||||
void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatCheckpointVolume.h"
|
||||
#include "CombatCharacter.h"
|
||||
#include "CombatPlayerController.h"
|
||||
|
||||
ACombatCheckpointVolume::ACombatCheckpointVolume()
|
||||
{
|
||||
// create the box volume
|
||||
RootComponent = Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
|
||||
check(Box);
|
||||
|
||||
// set the box's extent
|
||||
Box->SetBoxExtent(FVector(500.0f, 500.0f, 500.0f));
|
||||
|
||||
// set the default collision profile to overlap all dynamic
|
||||
Box->SetCollisionProfileName(FName("OverlapAllDynamic"));
|
||||
|
||||
// bind the begin overlap
|
||||
Box->OnComponentBeginOverlap.AddDynamic(this, &ACombatCheckpointVolume::OnOverlap);
|
||||
}
|
||||
|
||||
void ACombatCheckpointVolume::OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
// ensure we use this only once
|
||||
if (bCheckpointUsed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// has the player entered this volume?
|
||||
ACombatCharacter* PlayerCharacter = Cast<ACombatCharacter>(OtherActor);
|
||||
|
||||
if (PlayerCharacter)
|
||||
{
|
||||
if (ACombatPlayerController* PC = Cast<ACombatPlayerController>(PlayerCharacter->GetController()))
|
||||
{
|
||||
// raise the checkpoint used flag
|
||||
bCheckpointUsed = true;
|
||||
|
||||
// update the player's respawn checkpoint
|
||||
PC->SetRespawnTransform(PlayerCharacter->GetActorTransform());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "CombatCheckpointVolume.generated.h"
|
||||
|
||||
UCLASS(abstract)
|
||||
class ACombatCheckpointVolume : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Collision box volume */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Components, meta = (AllowPrivateAccess = "true"))
|
||||
UBoxComponent* Box;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatCheckpointVolume();
|
||||
|
||||
protected:
|
||||
|
||||
/** Set to true after use to avoid accidentally resetting the checkpoint */
|
||||
bool bCheckpointUsed = false;
|
||||
|
||||
/** Handles overlaps with the box volume */
|
||||
UFUNCTION()
|
||||
void OnOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatDamageableBox.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
ACombatDamageableBox::ACombatDamageableBox()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the mesh
|
||||
RootComponent = Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
|
||||
// set the collision properties
|
||||
Mesh->SetCollisionProfileName(FName("BlockAllDynamic"));
|
||||
|
||||
// enable physics
|
||||
Mesh->SetSimulatePhysics(true);
|
||||
|
||||
// disable navigation relevance so boxes don't affect NavMesh generation
|
||||
Mesh->bNavigationRelevant = false;
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::RemoveFromLevel()
|
||||
{
|
||||
// destroy this actor
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the death timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(DeathTimer);
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
||||
{
|
||||
// only process damage if we still have HP
|
||||
if (CurrentHP > 0.0f)
|
||||
{
|
||||
// apply the damage
|
||||
CurrentHP -= Damage;
|
||||
|
||||
// are we dead?
|
||||
if (CurrentHP <= 0.0f)
|
||||
{
|
||||
HandleDeath();
|
||||
}
|
||||
|
||||
// apply a physics impulse to the box, ignoring its mass
|
||||
Mesh->AddImpulseAtLocation(DamageImpulse * Mesh->GetMass(), DamageLocation);
|
||||
|
||||
// call the BP handler to play effects, etc.
|
||||
OnBoxDamaged(DamageLocation, DamageImpulse);
|
||||
}
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::HandleDeath()
|
||||
{
|
||||
// change the collision object type to Visibility so we ignore most interactions but still retain physics collisions
|
||||
Mesh->SetCollisionObjectType(ECC_Visibility);
|
||||
|
||||
// call the BP handler to play effects, etc.
|
||||
OnBoxDestroyed();
|
||||
|
||||
// set up the death cleanup timer
|
||||
GetWorld()->GetTimerManager().SetTimer(DeathTimer, this, &ACombatDamageableBox::RemoveFromLevel, DeathDelayTime);
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::ApplyHealing(float Healing, AActor* Healer)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
void ACombatDamageableBox::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
||||
{
|
||||
// stub
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CombatDamageable.h"
|
||||
#include "CombatDamageableBox.generated.h"
|
||||
|
||||
/**
|
||||
* A simple physics box that reacts to damage through the ICombatDamageable interface
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatDamageableBox : public AActor, public ICombatDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Damageable box mesh */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* Mesh;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatDamageableBox();
|
||||
|
||||
protected:
|
||||
|
||||
/** Amount of HP this box starts with. */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
float CurrentHP = 3.0f;
|
||||
|
||||
/** Time to wait before we remove this box from the level. */
|
||||
UPROPERTY(EditAnywhere, Category="Damage", meta = (ClampMin = 0, ClampMax = 10, Units = "s"))
|
||||
float DeathDelayTime = 6.0f;
|
||||
|
||||
/** Timer to defer destruction of this box after its HP are depleted */
|
||||
FTimerHandle DeathTimer;
|
||||
|
||||
/** Blueprint damage handler for effect playback */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
|
||||
void OnBoxDamaged(const FVector& DamageLocation, const FVector& DamageImpulse);
|
||||
|
||||
/** Blueprint destruction handler for effect playback */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Damage")
|
||||
void OnBoxDestroyed();
|
||||
|
||||
/** Timer callback to remove the box from the level after it dies */
|
||||
void RemoveFromLevel();
|
||||
|
||||
public:
|
||||
|
||||
/** EndPlay cleanup */
|
||||
void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
// ~Begin CombatDamageable interface
|
||||
|
||||
/** Handles damage and knockback events */
|
||||
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
|
||||
|
||||
/** Handles death events */
|
||||
virtual void HandleDeath() override;
|
||||
|
||||
/** Handles healing events */
|
||||
virtual void ApplyHealing(float Healing, AActor* Healer) override;
|
||||
|
||||
/** Allows reaction to incoming attacks */
|
||||
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
|
||||
|
||||
// ~End CombatDamageable interface
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatDummy.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "PhysicsEngine/PhysicsConstraintComponent.h"
|
||||
|
||||
ACombatDummy::ACombatDummy()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// create the root
|
||||
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
SetRootComponent(Root);
|
||||
|
||||
// create the base plate
|
||||
BasePlate = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Plate"));
|
||||
BasePlate->SetupAttachment(RootComponent);
|
||||
|
||||
// create the dummy
|
||||
Dummy = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Dummy"));
|
||||
Dummy->SetupAttachment(RootComponent);
|
||||
|
||||
Dummy->SetSimulatePhysics(true);
|
||||
|
||||
// create the physics constraint
|
||||
PhysicsConstraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(TEXT("Physics Constraint"));
|
||||
PhysicsConstraint->SetupAttachment(RootComponent);
|
||||
|
||||
PhysicsConstraint->SetConstrainedComponents(BasePlate, NAME_None, Dummy, NAME_None);
|
||||
}
|
||||
|
||||
void ACombatDummy::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
||||
{
|
||||
// apply impulse to the dummy
|
||||
Dummy->AddImpulseAtLocation(DamageImpulse, DamageLocation);
|
||||
|
||||
// call the BP handler
|
||||
BP_OnDummyDamaged(DamageLocation, DamageImpulse.GetSafeNormal());
|
||||
}
|
||||
|
||||
void ACombatDummy::HandleDeath()
|
||||
{
|
||||
// unused
|
||||
}
|
||||
|
||||
void ACombatDummy::ApplyHealing(float Healing, AActor* Healer)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
|
||||
void ACombatDummy::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CombatDamageable.h"
|
||||
#include "CombatDummy.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
class UPhysicsConstraintComponent;
|
||||
|
||||
/**
|
||||
* A simple invincible combat training dummy
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatDummy : public AActor, public ICombatDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Root component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
USceneComponent* Root;
|
||||
|
||||
/** Static base plate */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* BasePlate;
|
||||
|
||||
/** Physics enabled dummy mesh */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* Dummy;
|
||||
|
||||
/** Physics constraint holding the dummy and base plate together */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components", meta = (AllowPrivateAccess = "true"))
|
||||
UPhysicsConstraintComponent* PhysicsConstraint;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatDummy();
|
||||
|
||||
// ~Begin CombatDamageable interface
|
||||
|
||||
/** Handles damage and knockback events */
|
||||
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) override;
|
||||
|
||||
/** Handles death events */
|
||||
virtual void HandleDeath() override;
|
||||
|
||||
/** Handles healing events */
|
||||
virtual void ApplyHealing(float Healing, AActor* Healer) override;
|
||||
|
||||
/** Allows reaction to incoming attacks */
|
||||
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) override;
|
||||
|
||||
// ~End CombatDamageable interface
|
||||
|
||||
protected:
|
||||
|
||||
/** Blueprint handle to apply damage effects */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Combat", meta = (DisplayName = "On Dummy Damaged"))
|
||||
void BP_OnDummyDamaged(const FVector& Location, const FVector& Direction);
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatLavaFloor.h"
|
||||
#include "CombatDamageable.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
|
||||
ACombatLavaFloor::ACombatLavaFloor()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the mesh
|
||||
RootComponent = Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
|
||||
// bind the hit handler
|
||||
Mesh->OnComponentHit.AddDynamic(this, &ACombatLavaFloor::OnFloorHit);
|
||||
}
|
||||
|
||||
void ACombatLavaFloor::OnFloorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
|
||||
{
|
||||
// check if the hit actor is damageable by casting to the interface
|
||||
if (ICombatDamageable* Damageable = Cast<ICombatDamageable>(OtherActor))
|
||||
{
|
||||
// damage the actor
|
||||
Damageable->ApplyDamage(Damage, this, Hit.ImpactPoint, FVector::ZeroVector);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CombatLavaFloor.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
class UPrimitiveComponent;
|
||||
|
||||
/**
|
||||
* A basic actor that applies damage on contact through the ICombatDamageable interface.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ACombatLavaFloor : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Floor mesh */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* Mesh;
|
||||
|
||||
protected:
|
||||
|
||||
/** Amount of damage to deal on contact */
|
||||
UPROPERTY(EditAnywhere, Category="Damage")
|
||||
float Damage = 10000.0f;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ACombatLavaFloor();
|
||||
|
||||
protected:
|
||||
|
||||
/** Blocking hit handler */
|
||||
UFUNCTION()
|
||||
void OnFloorHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit);
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatActivatable.h"
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "CombatActivatable.generated.h"
|
||||
|
||||
/**
|
||||
* Interactable Interface
|
||||
* Provides a context-agnostic way of activating, deactivating or toggling actors
|
||||
*/
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable)
|
||||
class UCombatActivatable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class ICombatActivatable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Toggles the Interactable Actor */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void ToggleInteraction(AActor* ActivationInstigator) = 0;
|
||||
|
||||
/** Activates the Interactable Actor */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void ActivateInteraction(AActor* ActivationInstigator) = 0;
|
||||
|
||||
/** Deactivates the Interactable Actor */
|
||||
UFUNCTION(BlueprintCallable, Category="Activatable")
|
||||
virtual void DeactivateInteraction(AActor* ActivationInstigator) = 0;
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatAttacker.h"
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "CombatAttacker.generated.h"
|
||||
|
||||
/**
|
||||
* CombatAttacker Interface
|
||||
* Provides common functionality to trigger attack animation events.
|
||||
*/
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable)
|
||||
class UCombatAttacker : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class ICombatAttacker
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Performs an attack's collision check. Usually called from a montage's AnimNotify */
|
||||
UFUNCTION(BlueprintCallable, Category="Attacker")
|
||||
virtual void DoAttackTrace(FName DamageSourceBone) = 0;
|
||||
|
||||
/** Performs a combo attack's check to continue the string. Usually called from a montage's AnimNotify */
|
||||
UFUNCTION(BlueprintCallable, Category="Attacker")
|
||||
virtual void CheckCombo() = 0;
|
||||
|
||||
/** Performs a charged attack's check to loop the charge animation. Usually called from a montage's AnimNotify */
|
||||
UFUNCTION(BlueprintCallable, Category="Attacker")
|
||||
virtual void CheckChargedAttack() = 0;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatDamageable.h"
|
||||
|
||||
// Add default functionality here for any ICombatDamageable functions that are not pure virtual.
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "CombatDamageable.generated.h"
|
||||
|
||||
/**
|
||||
* CombatDamageable interface
|
||||
* Provides functionality to handle damage, healing, knockback and death
|
||||
* Also provides functionality to warn characters of incoming sources of damage
|
||||
*/
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable)
|
||||
class UCombatDamageable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
class ICombatDamageable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Handles damage and knockback events */
|
||||
UFUNCTION(BlueprintCallable, Category="Damageable")
|
||||
virtual void ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse) = 0;
|
||||
|
||||
/** Handles death events */
|
||||
UFUNCTION(BlueprintCallable, Category="Damageable")
|
||||
virtual void HandleDeath() = 0;
|
||||
|
||||
/** Handles healing events */
|
||||
UFUNCTION(BlueprintCallable, Category="Damageable")
|
||||
virtual void ApplyHealing(float Healing, AActor* Healer) = 0;
|
||||
|
||||
/** Notifies the actor of impending danger such as an incoming hit, allowing it to react. */
|
||||
UFUNCTION(BlueprintCallable, Category="Damageable")
|
||||
virtual void NotifyDanger(const FVector& DangerLocation, AActor* DangerSource) = 0;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatLifeBar.h"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CombatLifeBar.generated.h"
|
||||
|
||||
/**
|
||||
* A basic life bar user widget.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class UCombatLifeBar : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Sets the life bar to the provided 0-1 percentage value*/
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Life Bar")
|
||||
void SetLifePercentage(float Percent);
|
||||
|
||||
// Sets the life bar fill color
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Life Bar")
|
||||
void SetBarColor(FLinearColor Color);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "AnimNotify_EndDash.h"
|
||||
#include "PlatformingCharacter.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
|
||||
void UAnimNotify_EndDash::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
|
||||
{
|
||||
// cast the owner to the attacker interface
|
||||
if (APlatformingCharacter* PlatformingCharacter = Cast<APlatformingCharacter>(MeshComp->GetOwner()))
|
||||
{
|
||||
// tell the actor to end the dash
|
||||
PlatformingCharacter->EndDash();
|
||||
}
|
||||
}
|
||||
|
||||
FString UAnimNotify_EndDash::GetNotifyName_Implementation() const
|
||||
{
|
||||
return FString("End Dash");
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Animation/AnimNotifies/AnimNotify.h"
|
||||
#include "AnimNotify_EndDash.generated.h"
|
||||
|
||||
/**
|
||||
* AnimNotify to finish the dash animation and restore player control
|
||||
*/
|
||||
UCLASS()
|
||||
class UAnimNotify_EndDash : public UAnimNotify
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Perform the Anim Notify */
|
||||
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference) override;
|
||||
|
||||
/** Get the notify name */
|
||||
virtual FString GetNotifyName_Implementation() const override;
|
||||
};
|
||||
@@ -1,367 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "PlatformingCharacter.h"
|
||||
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
|
||||
APlatformingCharacter::APlatformingCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// initialize the flags
|
||||
bHasWallJumped = false;
|
||||
bHasDoubleJumped = false;
|
||||
bHasDashed = false;
|
||||
bIsDashing = false;
|
||||
|
||||
// bind the dash montage ended delegate
|
||||
OnDashMontageEnded.BindUObject(this, &APlatformingCharacter::DashMontageEnded);
|
||||
|
||||
// enable press and hold jump
|
||||
JumpMaxHoldTime = 0.4f;
|
||||
|
||||
// set the jump max count to 3 so we can double jump and check for coyote time jumps
|
||||
JumpMaxCount = 3;
|
||||
|
||||
// Set size for collision capsule
|
||||
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
|
||||
|
||||
// don't rotate the mesh when the controller rotates
|
||||
bUseControllerRotationYaw = false;
|
||||
|
||||
// Configure character movement
|
||||
GetCharacterMovement()->GravityScale = 2.5f;
|
||||
GetCharacterMovement()->MaxAcceleration = 1500.0f;
|
||||
GetCharacterMovement()->BrakingFrictionFactor = 1.0f;
|
||||
GetCharacterMovement()->bUseSeparateBrakingFriction = true;
|
||||
|
||||
GetCharacterMovement()->GroundFriction = 4.0f;
|
||||
GetCharacterMovement()->MaxWalkSpeed = 750.0f;
|
||||
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
|
||||
GetCharacterMovement()->BrakingDecelerationWalking = 2500.0f;
|
||||
GetCharacterMovement()->PerchRadiusThreshold = 15.0f;
|
||||
|
||||
GetCharacterMovement()->JumpZVelocity = 350.0f;
|
||||
GetCharacterMovement()->BrakingDecelerationFalling = 750.0f;
|
||||
GetCharacterMovement()->AirControl = 1.0f;
|
||||
|
||||
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
|
||||
GetCharacterMovement()->bOrientRotationToMovement = true;
|
||||
|
||||
GetCharacterMovement()->NavAgentProps.AgentRadius = 42.0;
|
||||
GetCharacterMovement()->NavAgentProps.AgentHeight = 192.0;
|
||||
|
||||
// create the camera boom
|
||||
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
||||
CameraBoom->SetupAttachment(RootComponent);
|
||||
|
||||
CameraBoom->TargetArmLength = 400.0f;
|
||||
CameraBoom->bUsePawnControlRotation = true;
|
||||
CameraBoom->bEnableCameraLag = true;
|
||||
CameraBoom->CameraLagSpeed = 8.0f;
|
||||
CameraBoom->bEnableCameraRotationLag = true;
|
||||
CameraBoom->CameraRotationLagSpeed = 8.0f;
|
||||
|
||||
// create the orbiting camera
|
||||
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||
FollowCamera->bUsePawnControlRotation = false;
|
||||
}
|
||||
|
||||
void APlatformingCharacter::Move(const FInputActionValue& Value)
|
||||
{
|
||||
FVector2D MovementVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoMove(MovementVector.X, MovementVector.Y);
|
||||
}
|
||||
|
||||
void APlatformingCharacter::Look(const FInputActionValue& Value)
|
||||
{
|
||||
FVector2D LookAxisVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
||||
}
|
||||
|
||||
|
||||
void APlatformingCharacter::Dash()
|
||||
{
|
||||
// route the input
|
||||
DoDash();
|
||||
}
|
||||
|
||||
void APlatformingCharacter::MultiJump()
|
||||
{
|
||||
// ignore jumps while dashing
|
||||
if(bIsDashing)
|
||||
return;
|
||||
|
||||
// are we already in the air?
|
||||
if (GetCharacterMovement()->IsFalling())
|
||||
{
|
||||
|
||||
// have we already wall jumped?
|
||||
if (!bHasWallJumped)
|
||||
{
|
||||
// run a sphere sweep to check if we're in front of a wall
|
||||
FHitResult OutHit;
|
||||
|
||||
const FVector TraceStart = GetActorLocation();
|
||||
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * WallJumpTraceDistance);
|
||||
const FCollisionShape TraceShape = FCollisionShape::MakeSphere(WallJumpTraceRadius);
|
||||
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepSingleByChannel(OutHit, TraceStart, TraceEnd, FQuat(), ECollisionChannel::ECC_Visibility, TraceShape, QueryParams))
|
||||
{
|
||||
// rotate the character to face away from the wall, so we're correctly oriented for the next wall jump
|
||||
FRotator WallOrientation = OutHit.ImpactNormal.ToOrientationRotator();
|
||||
WallOrientation.Pitch = 0.0f;
|
||||
WallOrientation.Roll = 0.0f;
|
||||
|
||||
SetActorRotation(WallOrientation);
|
||||
|
||||
// apply a launch impulse to the character to perform the actual wall jump
|
||||
const FVector WallJumpImpulse = (OutHit.ImpactNormal * WallJumpBounceImpulse) + (FVector::UpVector * WallJumpVerticalImpulse);
|
||||
|
||||
LaunchCharacter(WallJumpImpulse, true, true);
|
||||
|
||||
// enable the jump trail
|
||||
SetJumpTrailState(true);
|
||||
|
||||
// raise the wall jump flag to prevent an immediate second wall jump
|
||||
bHasWallJumped = true;
|
||||
|
||||
GetWorld()->GetTimerManager().SetTimer(WallJumpTimer, this, &APlatformingCharacter::ResetWallJump, DelayBetweenWallJumps, false);
|
||||
}
|
||||
// no wall jump, try a double jump next
|
||||
else
|
||||
{
|
||||
// are we still within coyote time frames?
|
||||
if (GetWorld()->GetTimeSeconds() - LastFallTime < MaxCoyoteTime)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Coyote Jump"));
|
||||
|
||||
// use the built-in CMC functionality to do the jump
|
||||
Jump();
|
||||
|
||||
// enable the jump trail
|
||||
SetJumpTrailState(true);
|
||||
|
||||
// no coyote time jump
|
||||
} else {
|
||||
|
||||
// only double jump once while we're in the air
|
||||
if (!bHasDoubleJumped)
|
||||
{
|
||||
bHasDoubleJumped = true;
|
||||
|
||||
// use the built-in CMC functionality to do the double jump
|
||||
Jump();
|
||||
|
||||
// enable the jump trail
|
||||
SetJumpTrailState(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're grounded so just do a regular jump
|
||||
Jump();
|
||||
|
||||
// activate the jump trail
|
||||
SetJumpTrailState(true);
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingCharacter::ResetWallJump()
|
||||
{
|
||||
// reset the wall jump input lock
|
||||
bHasWallJumped = false;
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DoMove(float Right, float Forward)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
{
|
||||
// momentarily disable movement inputs if we've just wall jumped
|
||||
if (!bHasWallJumped)
|
||||
{
|
||||
// find out which way is forward
|
||||
const FRotator Rotation = GetController()->GetControlRotation();
|
||||
const FRotator YawRotation(0, Rotation.Yaw, 0);
|
||||
|
||||
// get forward vector
|
||||
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
|
||||
|
||||
// get right vector
|
||||
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
|
||||
|
||||
// add movement
|
||||
AddMovementInput(ForwardDirection, Forward);
|
||||
AddMovementInput(RightDirection, Right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DoLook(float Yaw, float Pitch)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
{
|
||||
// add yaw and pitch input to controller
|
||||
AddControllerYawInput(Yaw);
|
||||
AddControllerPitchInput(Pitch);
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DoDash()
|
||||
{
|
||||
// ignore the input if we've already dashed and have yet to reset
|
||||
if (bHasDashed)
|
||||
return;
|
||||
|
||||
// raise the dash flags
|
||||
bIsDashing = true;
|
||||
bHasDashed = true;
|
||||
|
||||
// disable gravity while dashing
|
||||
GetCharacterMovement()->GravityScale = 0.0f;
|
||||
|
||||
// reset the character velocity so we don't carry momentum into the dash
|
||||
GetCharacterMovement()->Velocity = FVector::ZeroVector;
|
||||
|
||||
// enable the jump trails
|
||||
SetJumpTrailState(true);
|
||||
|
||||
// play the dash montage
|
||||
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
||||
{
|
||||
const float MontageLength = AnimInstance->Montage_Play(DashMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
||||
|
||||
// has the montage played successfully?
|
||||
if (MontageLength > 0.0f)
|
||||
{
|
||||
AnimInstance->Montage_SetEndDelegate(OnDashMontageEnded, DashMontage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DoJumpStart()
|
||||
{
|
||||
// handle special jump cases
|
||||
MultiJump();
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DoJumpEnd()
|
||||
{
|
||||
// stop jumping
|
||||
StopJumping();
|
||||
}
|
||||
|
||||
void APlatformingCharacter::DashMontageEnded(UAnimMontage* Montage, bool bInterrupted)
|
||||
{
|
||||
// end the dash
|
||||
EndDash();
|
||||
}
|
||||
|
||||
void APlatformingCharacter::EndDash()
|
||||
{
|
||||
// restore gravity
|
||||
GetCharacterMovement()->GravityScale = 2.5f;
|
||||
|
||||
// reset the dashing flag
|
||||
bIsDashing = false;
|
||||
|
||||
// are we grounded after the dash?
|
||||
if (GetCharacterMovement()->IsMovingOnGround())
|
||||
{
|
||||
// reset the dash usage flag, since we won't receive a landed event
|
||||
bHasDashed = false;
|
||||
|
||||
// deactivate the jump trails
|
||||
SetJumpTrailState(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool APlatformingCharacter::HasDoubleJumped() const
|
||||
{
|
||||
return bHasDoubleJumped;
|
||||
}
|
||||
|
||||
bool APlatformingCharacter::HasWallJumped() const
|
||||
{
|
||||
return bHasWallJumped;
|
||||
}
|
||||
|
||||
void APlatformingCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the wall jump reset timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(WallJumpTimer);
|
||||
}
|
||||
|
||||
void APlatformingCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
// Set up action bindings
|
||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
|
||||
{
|
||||
|
||||
// Jumping
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &APlatformingCharacter::DoJumpStart);
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &APlatformingCharacter::DoJumpEnd);
|
||||
|
||||
// Moving
|
||||
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Move);
|
||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Look);
|
||||
|
||||
// Looking
|
||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Look);
|
||||
|
||||
// Dashing
|
||||
EnhancedInputComponent->BindAction(DashAction, ETriggerEvent::Triggered, this, &APlatformingCharacter::Dash);
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingCharacter::Landed(const FHitResult& Hit)
|
||||
{
|
||||
Super::Landed(Hit);
|
||||
|
||||
// reset the double jump and dash flags
|
||||
bHasDoubleJumped = false;
|
||||
bHasDashed = false;
|
||||
|
||||
// deactivate the jump trail
|
||||
SetJumpTrailState(false);
|
||||
}
|
||||
|
||||
void APlatformingCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode /*= 0*/)
|
||||
{
|
||||
Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);
|
||||
|
||||
// are we falling?
|
||||
if (GetCharacterMovement()->MovementMode == EMovementMode::MOVE_Falling)
|
||||
{
|
||||
// save the game time when we started falling, so we can check it later for coyote time jumps
|
||||
LastFallTime = GetWorld()->GetTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Animation/AnimInstance.h"
|
||||
#include "PlatformingCharacter.generated.h"
|
||||
|
||||
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
struct FInputActionValue;
|
||||
class UAnimMontage;
|
||||
|
||||
/**
|
||||
* An enhanced Third Person Character with the following functionality:
|
||||
* - Platforming game character movement physics
|
||||
* - Press and Hold Jump
|
||||
* - Double Jump
|
||||
* - Wall Jump
|
||||
* - Dash
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class APlatformingCharacter : public ACharacter
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Camera boom positioning the camera behind the character */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
USpringArmComponent* CameraBoom;
|
||||
|
||||
/** Follow camera */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UCameraComponent* FollowCamera;
|
||||
|
||||
protected:
|
||||
|
||||
/** Jump Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* JumpAction;
|
||||
|
||||
/** Move Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* MoveAction;
|
||||
|
||||
/** Look Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* LookAction;
|
||||
|
||||
/** Mouse Look Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* MouseLookAction;
|
||||
|
||||
/** Dash Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* DashAction;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
APlatformingCharacter();
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for looking input */
|
||||
void Look(const FInputActionValue& Value);
|
||||
|
||||
/** Called for dash input */
|
||||
void Dash();
|
||||
|
||||
/** Called for jump pressed to check for advanced multi-jump conditions */
|
||||
void MultiJump();
|
||||
|
||||
/** Resets the wall jump input lock */
|
||||
void ResetWallJump();
|
||||
|
||||
public:
|
||||
|
||||
/** Handles move inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoMove(float Right, float Forward);
|
||||
|
||||
/** Handles look inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoLook(float Yaw, float Pitch);
|
||||
|
||||
/** Handles dash inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoDash();
|
||||
|
||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoJumpStart();
|
||||
|
||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoJumpEnd();
|
||||
|
||||
protected:
|
||||
|
||||
/** Called from a delegate when the dash montage ends */
|
||||
void DashMontageEnded(UAnimMontage* Montage, bool bInterrupted);
|
||||
|
||||
/** Passes control to Blueprint to enable or disable jump trails */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Platforming")
|
||||
void SetJumpTrailState(bool bEnabled);
|
||||
|
||||
public:
|
||||
|
||||
/** Ends the dash state */
|
||||
void EndDash();
|
||||
|
||||
public:
|
||||
|
||||
/** Returns true if the character has just double jumped */
|
||||
UFUNCTION(BlueprintPure, Category="Platforming")
|
||||
bool HasDoubleJumped() const;
|
||||
|
||||
/** Returns true if the character has just wall jumped */
|
||||
UFUNCTION(BlueprintPure, Category="Platforming")
|
||||
bool HasWallJumped() const;
|
||||
|
||||
public:
|
||||
|
||||
/** EndPlay cleanup */
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/** Sets up input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
/** Handle landings to reset dash and advanced jump state */
|
||||
virtual void Landed(const FHitResult& Hit) override;
|
||||
|
||||
/** Handle movement mode changes to keep track of coyote time jumps */
|
||||
virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** movement state flag bits, packed into a uint8 for memory efficiency */
|
||||
uint8 bHasWallJumped : 1;
|
||||
uint8 bHasDoubleJumped : 1;
|
||||
uint8 bHasDashed : 1;
|
||||
uint8 bIsDashing : 1;
|
||||
|
||||
/** timer for wall jump input reset */
|
||||
FTimerHandle WallJumpTimer;
|
||||
|
||||
/** Dash montage ended delegate */
|
||||
FOnMontageEnded OnDashMontageEnded;
|
||||
|
||||
/** Distance to trace ahead of the character to look for walls to jump from */
|
||||
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 1000, Units = "cm"))
|
||||
float WallJumpTraceDistance = 50.0f;
|
||||
|
||||
/** Radius of the wall jump sphere trace check */
|
||||
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 100, Units = "cm"))
|
||||
float WallJumpTraceRadius = 25.0f;
|
||||
|
||||
/** Impulse to apply away from the wall when wall jumping */
|
||||
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm/s"))
|
||||
float WallJumpBounceImpulse = 800.0f;
|
||||
|
||||
/** Vertical impulse to apply when wall jumping */
|
||||
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm/s"))
|
||||
float WallJumpVerticalImpulse = 900.0f;
|
||||
|
||||
/** Time to ignore jump inputs after a wall jump */
|
||||
UPROPERTY(EditAnywhere, Category="Wall Jump", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
|
||||
float DelayBetweenWallJumps = 0.1f;
|
||||
|
||||
/** AnimMontage to use for the Dash action */
|
||||
UPROPERTY(EditAnywhere, Category="Dash")
|
||||
UAnimMontage* DashMontage;
|
||||
|
||||
/** Last recorded time when this character started falling */
|
||||
float LastFallTime = 0.0f;
|
||||
|
||||
/** Max amount of time that can pass since we started falling when we allow a regular jump */
|
||||
UPROPERTY(EditAnywhere, Category="Coyote Time", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
|
||||
float MaxCoyoteTime = 0.16f;
|
||||
|
||||
public:
|
||||
/** Returns CameraBoom subobject **/
|
||||
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
|
||||
|
||||
/** Returns FollowCamera subobject **/
|
||||
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
|
||||
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Platforming/PlatformingGameMode.h"
|
||||
|
||||
APlatformingGameMode::APlatformingGameMode()
|
||||
{
|
||||
// stub
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "PlatformingGameMode.generated.h"
|
||||
|
||||
/**
|
||||
* Simple GameMode for a third person platforming game
|
||||
*/
|
||||
UCLASS()
|
||||
class APlatformingGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
APlatformingGameMode();
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Platforming/PlatformingPlayerController.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerStart.h"
|
||||
#include "PlatformingCharacter.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "AgrarianGame.h"
|
||||
#include "Widgets/Input/SVirtualJoystick.h"
|
||||
|
||||
void APlatformingPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// only spawn touch controls on local player controllers
|
||||
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
||||
{
|
||||
// spawn the mobile controls widget
|
||||
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
|
||||
|
||||
if (MobileControlsWidget)
|
||||
{
|
||||
// add the controls to the player screen
|
||||
MobileControlsWidget->AddToPlayerScreen(0);
|
||||
|
||||
} else {
|
||||
|
||||
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
// only add IMCs for local player controllers
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
// add the input mapping context
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
|
||||
// only add these IMCs if we're not using mobile touch input
|
||||
if (!ShouldUseTouchControls())
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APlatformingPlayerController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
// subscribe to the pawn's OnDestroyed delegate
|
||||
InPawn->OnDestroyed.AddDynamic(this, &APlatformingPlayerController::OnPawnDestroyed);
|
||||
}
|
||||
|
||||
void APlatformingPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
|
||||
{
|
||||
// find the player start
|
||||
TArray<AActor*> ActorList;
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList);
|
||||
|
||||
if (ActorList.Num() > 0)
|
||||
{
|
||||
// spawn a character at the player start
|
||||
const FTransform SpawnTransform = ActorList[0]->GetActorTransform();
|
||||
|
||||
if (APlatformingCharacter* RespawnedCharacter = GetWorld()->SpawnActor<APlatformingCharacter>(CharacterClass, SpawnTransform))
|
||||
{
|
||||
// possess the character
|
||||
Possess(RespawnedCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool APlatformingPlayerController::ShouldUseTouchControls() const
|
||||
{
|
||||
// are we on a mobile platform? Should we force touch?
|
||||
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PlatformingPlayerController.generated.h"
|
||||
|
||||
class UInputMappingContext;
|
||||
class APlatformingCharacter;
|
||||
|
||||
/**
|
||||
* Simple Player Controller for a third person platforming game
|
||||
* Manages input mappings
|
||||
* Respawns the player character at the Player Start when it's destroyed
|
||||
*/
|
||||
UCLASS(abstract, Config="Game")
|
||||
class APlatformingPlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Input mapping context for this player */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> DefaultMappingContexts;
|
||||
|
||||
/** Input Mapping Contexts */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
|
||||
|
||||
/** Mobile controls widget to spawn */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
|
||||
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
|
||||
|
||||
/** Pointer to the mobile controls widget */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UUserWidget> MobileControlsWidget;
|
||||
|
||||
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
|
||||
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
||||
bool bForceTouchControls = false;
|
||||
|
||||
/** Character class to respawn when the possessed pawn is destroyed */
|
||||
UPROPERTY(EditAnywhere, Category="Respawn")
|
||||
TSubclassOf<APlatformingCharacter> CharacterClass;
|
||||
|
||||
protected:
|
||||
|
||||
/** Gameplay initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Initialize input bindings */
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
/** Pawn initialization */
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
/** Called if the possessed pawn is destroyed */
|
||||
UFUNCTION()
|
||||
void OnPawnDestroyed(AActor* DestroyedActor);
|
||||
|
||||
/** Returns true if the player should use UMG touch controls */
|
||||
bool ShouldUseTouchControls() const;
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingAIController.h"
|
||||
#include "GameplayStateTreeModule/Public/Components/StateTreeAIComponent.h"
|
||||
|
||||
ASideScrollingAIController::ASideScrollingAIController()
|
||||
{
|
||||
// create the StateTree AI Component
|
||||
StateTreeAI = CreateDefaultSubobject<UStateTreeAIComponent>(TEXT("StateTreeAI"));
|
||||
check(StateTreeAI);
|
||||
|
||||
// ensure we start the StateTree when we possess the pawn
|
||||
bStartAILogicOnPossess = true;
|
||||
|
||||
// ensure we're attached to the possessed character.
|
||||
// this is necessary for EnvQueries to work correctly
|
||||
bAttachToPawn = true;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "AIController.h"
|
||||
#include "SideScrollingAIController.generated.h"
|
||||
|
||||
class UStateTreeAIComponent;
|
||||
|
||||
/**
|
||||
* A basic AI Controller capable of running StateTree
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingAIController : public AAIController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** StateTree Component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI", meta = (AllowPrivateAccess = "true"))
|
||||
UStateTreeAIComponent* StateTreeAI;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingAIController();
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingNPC.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
ASideScrollingNPC::ASideScrollingNPC()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
GetCharacterMovement()->MaxWalkSpeed = 150.0f;
|
||||
}
|
||||
|
||||
void ASideScrollingNPC::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the deactivation timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(DeactivationTimer);
|
||||
}
|
||||
|
||||
void ASideScrollingNPC::Interaction(AActor* Interactor)
|
||||
{
|
||||
// ignore if this NPC has already been deactivated
|
||||
if (bDeactivated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// reset the deactivation flag
|
||||
bDeactivated = true;
|
||||
|
||||
// stop character movement immediately
|
||||
GetCharacterMovement()->StopMovementImmediately();
|
||||
|
||||
// launch the NPC away from the interactor
|
||||
FVector LaunchVector = Interactor->GetActorForwardVector() * LaunchImpulse;
|
||||
LaunchVector.Y = 0.0f;
|
||||
LaunchVector.Z = LaunchVerticalImpulse;
|
||||
|
||||
LaunchCharacter(LaunchVector, true, true);
|
||||
|
||||
// set up a timer to schedule reactivation
|
||||
GetWorld()->GetTimerManager().SetTimer(DeactivationTimer, this, &ASideScrollingNPC::ResetDeactivation, DeactivationTime, false);
|
||||
}
|
||||
|
||||
void ASideScrollingNPC::ResetDeactivation()
|
||||
{
|
||||
// reset the deactivation flag
|
||||
bDeactivated = false;
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "SideScrollingInteractable.h"
|
||||
#include "SideScrollingNPC.generated.h"
|
||||
|
||||
/**
|
||||
* Simple platforming NPC
|
||||
* Its behaviors will be dictated by a possessing AI Controller
|
||||
* It can be temporarily deactivated through Actor interactions
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingNPC : public ACharacter, public ISideScrollingInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Horizontal impulse to apply to the NPC when it's interacted with */
|
||||
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10000, Units="cm/s"))
|
||||
float LaunchImpulse = 500.0f;
|
||||
|
||||
/** Vertical impulse to apply to the NPC when it's interacted with */
|
||||
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10000, Units="cm/s"))
|
||||
float LaunchVerticalImpulse = 500.0f;
|
||||
|
||||
/** Time that the NPC remains deactivated after being interacted with */
|
||||
UPROPERTY(EditAnywhere, Category="NPC", meta = (ClampMin = 0, ClampMax = 10, Units="s"))
|
||||
float DeactivationTime = 3.0f;
|
||||
|
||||
public:
|
||||
|
||||
/** If true, this NPC is deactivated and will not be interacted with */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="NPC")
|
||||
bool bDeactivated = false;
|
||||
|
||||
/** Timer to reactivate the NPC */
|
||||
FTimerHandle DeactivationTimer;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingNPC();
|
||||
|
||||
public:
|
||||
|
||||
/** Cleanup */
|
||||
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
public:
|
||||
|
||||
// ~begin IInteractable interface
|
||||
|
||||
/** Performs an interaction triggered by another actor */
|
||||
virtual void Interaction(AActor* Interactor) override;
|
||||
|
||||
// ~end IInteractable interface
|
||||
|
||||
/** Reactivates the NPC */
|
||||
void ResetDeactivation();
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingStateTreeUtility.h"
|
||||
#include "StateTreeExecutionContext.h"
|
||||
#include "StateTreeExecutionTypes.h"
|
||||
#include "AIController.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
EStateTreeRunStatus FStateTreeGetPlayerTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// set the player pawn as the target
|
||||
InstanceData.TargetPlayer = UGameplayStatics::GetPlayerPawn(InstanceData.Controller.Get(), 0);
|
||||
|
||||
// are the NPC and target valid?
|
||||
if (IsValid(InstanceData.TargetPlayer) && IsValid(InstanceData.NPC))
|
||||
{
|
||||
InstanceData.bValidTarget = FVector::Distance(InstanceData.NPC->GetActorLocation(), InstanceData.TargetPlayer->GetActorLocation()) < InstanceData.RangeMax;
|
||||
}
|
||||
|
||||
return EStateTreeRunStatus::Running;
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
FText FStateTreeGetPlayerTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const
|
||||
{
|
||||
return FText::FromString("<b>Get Player</b>");
|
||||
}
|
||||
#endif // WITH_EDITOR
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "StateTreeTaskBase.h"
|
||||
|
||||
#include "SideScrollingStateTreeUtility.generated.h"
|
||||
|
||||
class AAIController;
|
||||
|
||||
/**
|
||||
* Instance data for the FStateTreeGetPlayerTask task
|
||||
*/
|
||||
USTRUCT()
|
||||
struct FStateTreeGetPlayerInstanceData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** NPC owning this task */
|
||||
UPROPERTY(VisibleAnywhere, Category="Context")
|
||||
TObjectPtr<APawn> NPC;
|
||||
|
||||
/** Holds the found player pawn */
|
||||
UPROPERTY(VisibleAnywhere, Category="Context")
|
||||
TObjectPtr<AAIController> Controller;
|
||||
|
||||
/** Holds the found player pawn */
|
||||
UPROPERTY(VisibleAnywhere, Category="Output")
|
||||
TObjectPtr<APawn> TargetPlayer;
|
||||
|
||||
/** Is the pawn close enough to be considered a valid target? */
|
||||
UPROPERTY(VisibleAnywhere, Category="Output")
|
||||
bool bValidTarget = false;
|
||||
|
||||
/** Max distance to be considered a valid target */
|
||||
UPROPERTY(EditAnywhere, Category="Parameter", meta = (ClampMin = 0, ClampMax = 10000, Units = "cm"))
|
||||
float RangeMax = 1000.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* StateTree task to get the player-controlled character
|
||||
*/
|
||||
USTRUCT(meta=(DisplayName="Get Player", Category="Side Scrolling"))
|
||||
struct FStateTreeGetPlayerTask : public FStateTreeTaskCommonBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeGetPlayerInstanceData;
|
||||
virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
|
||||
|
||||
/** Runs while the owning state is active */
|
||||
virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual FText GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting = EStateTreeNodeFormatting::Text) const override;
|
||||
#endif // WITH_EDITOR
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingJumpPad.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
|
||||
ASideScrollingJumpPad::ASideScrollingJumpPad()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the root comp
|
||||
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
|
||||
// create the bounding box
|
||||
Box = CreateDefaultSubobject<UBoxComponent>(TEXT("Box"));
|
||||
Box->SetupAttachment(RootComponent);
|
||||
|
||||
// configure the bounding box
|
||||
Box->SetBoxExtent(FVector(115.0f, 90.0f, 20.0f), false);
|
||||
Box->SetRelativeLocation(FVector(0.0f, 0.0f, 16.0f));
|
||||
|
||||
Box->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Box->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
Box->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
|
||||
// add the overlap handler
|
||||
OnActorBeginOverlap.AddDynamic(this, &ASideScrollingJumpPad::BeginOverlap);
|
||||
}
|
||||
|
||||
void ASideScrollingJumpPad::BeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
|
||||
{
|
||||
// were we overlapped by a character?
|
||||
if (ACharacter* OverlappingCharacter = Cast<ACharacter>(OtherActor))
|
||||
{
|
||||
// force the character to jump
|
||||
OverlappingCharacter->Jump();
|
||||
|
||||
// launch the character to override its vertical velocity
|
||||
FVector LaunchVelocity = FVector::UpVector * ZStrength;
|
||||
OverlappingCharacter->LaunchCharacter(LaunchVelocity, false, true);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "SideScrollingJumpPad.generated.h"
|
||||
|
||||
class UBoxComponent;
|
||||
|
||||
/**
|
||||
* A simple jump pad that launches characters into the air
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingJumpPad : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Jump pad bounding box */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UBoxComponent* Box;
|
||||
|
||||
protected:
|
||||
|
||||
/** Vertical velocity to set the character to when they use the jump pad */
|
||||
UPROPERTY(EditAnywhere, Category="Jump Pad", meta = (ClampMin=0, ClampMax=10000, Units="cm/s"))
|
||||
float ZStrength = 1000.0f;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingJumpPad();
|
||||
|
||||
protected:
|
||||
|
||||
UFUNCTION()
|
||||
void BeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
|
||||
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingMovingPlatform.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
|
||||
ASideScrollingMovingPlatform::ASideScrollingMovingPlatform()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the root comp
|
||||
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
}
|
||||
|
||||
void ASideScrollingMovingPlatform::Interaction(AActor* Interactor)
|
||||
{
|
||||
// ignore interactions if we're already moving
|
||||
if (bMoving)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// raise the movement flag
|
||||
bMoving = true;
|
||||
|
||||
// pass control to BP for the actual movement
|
||||
BP_MoveToTarget();
|
||||
}
|
||||
|
||||
void ASideScrollingMovingPlatform::ResetInteraction()
|
||||
{
|
||||
// ignore if this is a one-shot platform
|
||||
if (bOneShot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// reset the movement flag
|
||||
bMoving = false;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "SideScrollingInteractable.h"
|
||||
#include "SideScrollingMovingPlatform.generated.h"
|
||||
|
||||
/**
|
||||
* Simple moving platform that can be triggered through interactions by other actors.
|
||||
* The actual movement is performed by Blueprint code through latent execution nodes.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingMovingPlatform : public AActor, public ISideScrollingInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingMovingPlatform();
|
||||
|
||||
protected:
|
||||
|
||||
/** If this is true, the platform is mid-movement and will ignore further interactions */
|
||||
bool bMoving = false;
|
||||
|
||||
/** Destination of the platform in world space */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Moving Platform")
|
||||
FVector PlatformTarget;
|
||||
|
||||
/** Time for the platform to move to the destination */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Moving Platform", meta = (ClampMin = 0, ClampMax = 10, Units="s"))
|
||||
float MoveDuration = 5.0f;
|
||||
|
||||
/** If this is true, the platform will only move once. */
|
||||
UPROPERTY(EditAnywhere, Category="Moving Platform")
|
||||
bool bOneShot = false;
|
||||
|
||||
public:
|
||||
|
||||
// ~begin IInteractable interface
|
||||
|
||||
/** Performs an interaction triggered by another actor */
|
||||
virtual void Interaction(AActor* Interactor) override;
|
||||
|
||||
// ~end IInteractable interface
|
||||
|
||||
/** Resets the interaction state. Must be called from BP code to reset the platform */
|
||||
UFUNCTION(BlueprintCallable, Category="Moving Platform")
|
||||
virtual void ResetInteraction();
|
||||
|
||||
protected:
|
||||
|
||||
/** Allows Blueprint code to do the actual platform movement */
|
||||
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category="Moving Platform", meta = (DisplayName="Move to Target"))
|
||||
void BP_MoveToTarget();
|
||||
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingPickup.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "SideScrollingGameMode.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
ASideScrollingPickup::ASideScrollingPickup()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// create the root comp
|
||||
RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
|
||||
// create the bounding sphere
|
||||
Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
|
||||
Sphere->SetupAttachment(RootComponent);
|
||||
|
||||
Sphere->SetSphereRadius(100.0f);
|
||||
|
||||
Sphere->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
Sphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
Sphere->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
Sphere->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
|
||||
// add the overlap handler
|
||||
OnActorBeginOverlap.AddDynamic(this, &ASideScrollingPickup::BeginOverlap);
|
||||
}
|
||||
|
||||
void ASideScrollingPickup::BeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
|
||||
{
|
||||
// have we collided against a character?
|
||||
if (ACharacter* OverlappedCharacter = Cast<ACharacter>(OtherActor))
|
||||
{
|
||||
// is this the player character?
|
||||
if (OverlappedCharacter->IsPlayerControlled())
|
||||
{
|
||||
// get the game mode
|
||||
if (ASideScrollingGameMode* GM = Cast<ASideScrollingGameMode>(GetWorld()->GetAuthGameMode()))
|
||||
{
|
||||
// tell the game mode to process a pickup
|
||||
GM->ProcessPickup();
|
||||
|
||||
// disable collision so we don't get picked up again
|
||||
SetActorEnableCollision(false);
|
||||
|
||||
// Call the BP handler. It will be responsible for destroying the pickup
|
||||
BP_OnPickedUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "SideScrollingPickup.generated.h"
|
||||
|
||||
class USphereComponent;
|
||||
|
||||
/**
|
||||
* A simple side scrolling game pickup
|
||||
* Increments a counter on the GameMode
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingPickup : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Pickup bounding sphere */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
|
||||
USphereComponent* Sphere;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingPickup();
|
||||
|
||||
protected:
|
||||
|
||||
/** Handles pickup collision */
|
||||
UFUNCTION()
|
||||
void BeginOverlap(AActor* OverlappedActor, AActor* OtherActor);
|
||||
|
||||
/** Passes control to BP to play effects on pickup */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="Pickup", meta = (DisplayName = "On Picked Up"))
|
||||
void BP_OnPickedUp();
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingSoftPlatform.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "SideScrollingCharacter.h"
|
||||
|
||||
ASideScrollingSoftPlatform::ASideScrollingSoftPlatform()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// create the root component
|
||||
RootComponent = Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
||||
|
||||
// create the mesh
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
Mesh->SetupAttachment(Root);
|
||||
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
Mesh->SetCollisionObjectType(ECC_WorldStatic);
|
||||
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
|
||||
|
||||
// create the collision check box
|
||||
CollisionCheckBox = CreateDefaultSubobject<UBoxComponent>(TEXT("Collision Check Box"));
|
||||
CollisionCheckBox->SetupAttachment(Mesh);
|
||||
|
||||
CollisionCheckBox->SetRelativeLocation(FVector(0.0f, 0.0f, -40.0f));
|
||||
CollisionCheckBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
CollisionCheckBox->SetCollisionObjectType(ECC_WorldDynamic);
|
||||
CollisionCheckBox->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
CollisionCheckBox->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
||||
|
||||
// subscribe to the overlap events
|
||||
CollisionCheckBox->OnComponentBeginOverlap.AddDynamic(this, &ASideScrollingSoftPlatform::OnSoftCollisionOverlap);
|
||||
}
|
||||
|
||||
void ASideScrollingSoftPlatform::OnSoftCollisionOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
|
||||
{
|
||||
// have we overlapped a character?
|
||||
if (ASideScrollingCharacter* Char = Cast<ASideScrollingCharacter>(OtherActor))
|
||||
{
|
||||
// disable the soft collision channel
|
||||
Char->SetSoftCollision(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingSoftPlatform::NotifyActorEndOverlap(AActor* OtherActor)
|
||||
{
|
||||
Super::NotifyActorEndOverlap(OtherActor);
|
||||
|
||||
// have we overlapped a character?
|
||||
if (ASideScrollingCharacter* Char = Cast<ASideScrollingCharacter>(OtherActor))
|
||||
{
|
||||
// enable the soft collision channel
|
||||
Char->SetSoftCollision(false);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "SideScrollingSoftPlatform.generated.h"
|
||||
|
||||
class USceneComponent;
|
||||
class UStaticMeshComponent;
|
||||
class UBoxComponent;
|
||||
|
||||
/**
|
||||
* A side scrolling game platform that the character can jump or drop through.
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingSoftPlatform : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Root component */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
|
||||
USceneComponent* Root;
|
||||
|
||||
/** Platform mesh. The part we collide against and see */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UStaticMeshComponent* Mesh;
|
||||
|
||||
/** Collision volume that toggles soft collision on the character when they're below the platform. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Components", meta = (AllowPrivateAccess = "true"))
|
||||
UBoxComponent* CollisionCheckBox;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingSoftPlatform();
|
||||
|
||||
protected:
|
||||
|
||||
/** Handles soft collision check box overlaps */
|
||||
UFUNCTION()
|
||||
void OnSoftCollisionOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
|
||||
|
||||
/** Restores soft collision state when overlap ends */
|
||||
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingInteractable.h"
|
||||
|
||||
// Add default functionality here for any IInteractable functions that are not pure virtual.
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Interface.h"
|
||||
#include "SideScrollingInteractable.generated.h"
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
UINTERFACE(MinimalAPI, NotBlueprintable)
|
||||
class USideScrollingInteractable : public UInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple interface to allow Actors to interact without having knowledge of their internal implementation.
|
||||
*/
|
||||
class ISideScrollingInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Triggers an interaction by the provided Actor */
|
||||
UFUNCTION(BlueprintCallable, Category="Interactable")
|
||||
virtual void Interaction(AActor* Interactor) = 0;
|
||||
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingCameraManager.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
void ASideScrollingCameraManager::UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime)
|
||||
{
|
||||
// ensure the view target is a pawn
|
||||
APawn* TargetPawn = Cast<APawn>(OutVT.Target);
|
||||
|
||||
// is our target valid?
|
||||
if (IsValid(TargetPawn))
|
||||
{
|
||||
// set the view target FOV and rotation
|
||||
OutVT.POV.Rotation = FRotator(0.0f, -90.0f, 0.0f);
|
||||
OutVT.POV.FOV = 65.0f;
|
||||
|
||||
// cache the current location
|
||||
FVector CurrentActorLocation = OutVT.Target->GetActorLocation();
|
||||
|
||||
// copy the current camera location
|
||||
FVector CurrentCameraLocation = GetCameraLocation();
|
||||
|
||||
// calculate the "zoom distance" - in reality the distance we want to keep to the target
|
||||
float CurrentY = CurrentZoom + CurrentActorLocation.Y;
|
||||
|
||||
// do first-time setup
|
||||
if (bSetup)
|
||||
{
|
||||
// lower the setup flag
|
||||
bSetup = false;
|
||||
|
||||
// initialize the camera viewpoint and return
|
||||
OutVT.POV.Location.X = CurrentActorLocation.X;
|
||||
OutVT.POV.Location.Y = CurrentY;
|
||||
OutVT.POV.Location.Z = CurrentActorLocation.Z + CameraZOffset;
|
||||
|
||||
// save the current camera height
|
||||
CurrentZ = OutVT.POV.Location.Z;
|
||||
|
||||
// skip the rest of the calculations
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the camera needs to update its height
|
||||
bool bZUpdate = false;
|
||||
|
||||
// is the character moving vertically?
|
||||
if (FMath::IsNearlyZero(TargetPawn->GetVelocity().Z))
|
||||
{
|
||||
// determine if we need to do a height update
|
||||
bZUpdate = FMath::IsNearlyEqual(CurrentZ, CurrentCameraLocation.Z, 25.0f);
|
||||
|
||||
} else {
|
||||
|
||||
// run a trace below the character to determine if we need to do a height update
|
||||
FHitResult OutHit;
|
||||
|
||||
const FVector End = CurrentActorLocation + FVector(0.0f, 0.0f, -1000.0f);
|
||||
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(TargetPawn);
|
||||
|
||||
// only update height if we're not about to hit ground
|
||||
bZUpdate = !GetWorld()->LineTraceSingleByChannel(OutHit, CurrentActorLocation, End, ECC_Visibility, QueryParams);
|
||||
|
||||
}
|
||||
|
||||
// do we need to do a height update?
|
||||
if (bZUpdate)
|
||||
{
|
||||
|
||||
// set the height goal from the actor location
|
||||
CurrentZ = CurrentActorLocation.Z;
|
||||
|
||||
} else {
|
||||
|
||||
// are we close enough to the target height?
|
||||
if (FMath::IsNearlyEqual(CurrentZ, CurrentActorLocation.Z, 100.0f))
|
||||
{
|
||||
// set the height goal from the actor location
|
||||
CurrentZ = CurrentActorLocation.Z;
|
||||
|
||||
} else {
|
||||
|
||||
// blend the height towards the actor location
|
||||
CurrentZ = FMath::FInterpTo(CurrentZ, CurrentActorLocation.Z, DeltaTime, 2.0f);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// clamp the X axis to the min and max camera bounds
|
||||
float CurrentX = FMath::Clamp(CurrentActorLocation.X, CameraXMinBounds, CameraXMaxBounds);
|
||||
|
||||
// blend towards the new camera location and update the output
|
||||
FVector TargetCameraLocation(CurrentX, CurrentY, CurrentZ);
|
||||
|
||||
OutVT.POV.Location = FMath::VInterpTo(CurrentCameraLocation, TargetCameraLocation, DeltaTime, 2.0f);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Camera/PlayerCameraManager.h"
|
||||
#include "SideScrollingCameraManager.generated.h"
|
||||
|
||||
/**
|
||||
* Simple side scrolling camera with smooth scrolling and horizontal bounds
|
||||
*/
|
||||
UCLASS()
|
||||
class ASideScrollingCameraManager : public APlayerCameraManager
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Overrides the default camera view target calculation */
|
||||
virtual void UpdateViewTarget(FTViewTarget& OutVT, float DeltaTime) override;
|
||||
|
||||
public:
|
||||
|
||||
/** How close we want to stay to the view target */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=0, ClampMax=10000, Units="cm"))
|
||||
float CurrentZoom = 1000.0f;
|
||||
|
||||
/** How far above the target do we want the camera to focus */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=0, ClampMax=10000, Units="cm"))
|
||||
float CameraZOffset = 100.0f;
|
||||
|
||||
/** Minimum camera scrolling bounds in world space */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=-100000, ClampMax=100000, Units="cm"))
|
||||
float CameraXMinBounds = -400.0f;
|
||||
|
||||
/** Maximum camera scrolling bounds in world space */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling Camera", meta=(ClampMin=-100000, ClampMax=100000, Units="cm"))
|
||||
float CameraXMaxBounds = 10000.0f;
|
||||
|
||||
protected:
|
||||
|
||||
/** Last cached camera vertical location. The camera only adjusts its height if necessary. */
|
||||
float CurrentZ = 0.0f;
|
||||
|
||||
/** First-time update camera setup flag */
|
||||
bool bSetup = true;
|
||||
};
|
||||
@@ -1,350 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingCharacter.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/InputComponent.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "InputAction.h"
|
||||
#include "Engine/World.h"
|
||||
#include "SideScrollingInteractable.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
ASideScrollingCharacter::ASideScrollingCharacter()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// create the camera component
|
||||
Camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
|
||||
Camera->SetupAttachment(RootComponent);
|
||||
|
||||
Camera->SetRelativeLocationAndRotation(FVector(0.0f, 300.0f, 0.0f), FRotator(0.0f, -90.0f, 0.0f));
|
||||
|
||||
// configure the collision capsule
|
||||
GetCapsuleComponent()->SetCapsuleSize(35.0f, 90.0f);
|
||||
|
||||
// configure the Pawn properties
|
||||
bUseControllerRotationYaw = false;
|
||||
|
||||
// configure the character movement component
|
||||
GetCharacterMovement()->GravityScale = 1.75f;
|
||||
GetCharacterMovement()->MaxAcceleration = 1500.0f;
|
||||
GetCharacterMovement()->BrakingFrictionFactor = 1.0f;
|
||||
GetCharacterMovement()->bUseSeparateBrakingFriction = true;
|
||||
GetCharacterMovement()->Mass = 500.0f;
|
||||
|
||||
GetCharacterMovement()->SetWalkableFloorAngle(75.0f);
|
||||
GetCharacterMovement()->MaxWalkSpeed = 500.0f;
|
||||
GetCharacterMovement()->MinAnalogWalkSpeed = 20.0f;
|
||||
GetCharacterMovement()->BrakingDecelerationWalking = 2000.0f;
|
||||
GetCharacterMovement()->bIgnoreBaseRotation = true;
|
||||
|
||||
GetCharacterMovement()->PerchRadiusThreshold = 15.0f;
|
||||
GetCharacterMovement()->LedgeCheckThreshold = 6.0f;
|
||||
|
||||
GetCharacterMovement()->JumpZVelocity = 750.0f;
|
||||
GetCharacterMovement()->AirControl = 1.0f;
|
||||
|
||||
GetCharacterMovement()->RotationRate = FRotator(0.0f, 750.0f, 0.0f);
|
||||
GetCharacterMovement()->bOrientRotationToMovement = true;
|
||||
|
||||
GetCharacterMovement()->SetPlaneConstraintNormal(FVector(0.0f, 1.0f, 0.0f));
|
||||
GetCharacterMovement()->bConstrainToPlane = true;
|
||||
|
||||
// enable double jump and coyote time
|
||||
JumpMaxCount = 3;
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::EndPlay(EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
Super::EndPlay(EndPlayReason);
|
||||
|
||||
// clear the wall jump timer
|
||||
GetWorld()->GetTimerManager().ClearTimer(WallJumpTimer);
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
||||
|
||||
// Set up action bindings
|
||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
|
||||
{
|
||||
// Jumping
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ASideScrollingCharacter::DoJumpStart);
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ASideScrollingCharacter::DoJumpEnd);
|
||||
|
||||
// Interacting
|
||||
EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::DoInteract);
|
||||
|
||||
// Moving
|
||||
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::Move);
|
||||
|
||||
// Dropping from platform
|
||||
EnhancedInputComponent->BindAction(DropAction, ETriggerEvent::Triggered, this, &ASideScrollingCharacter::Drop);
|
||||
EnhancedInputComponent->BindAction(DropAction, ETriggerEvent::Completed, this, &ASideScrollingCharacter::DropReleased);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
|
||||
{
|
||||
Super::NotifyHit(MyComp, Other, OtherComp, bSelfMoved, HitLocation, HitNormal, NormalImpulse, Hit);
|
||||
|
||||
// only apply push impulse if we're falling
|
||||
if (!GetCharacterMovement()->IsFalling())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure the colliding component is valid
|
||||
if (OtherComp)
|
||||
{
|
||||
// ensure the component is movable and simulating physics
|
||||
if (OtherComp->Mobility == EComponentMobility::Movable && OtherComp->IsSimulatingPhysics())
|
||||
{
|
||||
const FVector PushDir = FVector(ActionValueY > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f);
|
||||
|
||||
// push the component away
|
||||
OtherComp->AddImpulse(PushDir * JumpPushImpulse, NAME_None, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::Landed(const FHitResult& Hit)
|
||||
{
|
||||
// reset the double jump
|
||||
bHasDoubleJumped = false;
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode /*= 0*/)
|
||||
{
|
||||
Super::OnMovementModeChanged(PrevMovementMode, PreviousCustomMode);
|
||||
|
||||
// are we falling?
|
||||
if (GetCharacterMovement()->MovementMode == EMovementMode::MOVE_Falling)
|
||||
{
|
||||
// save the game time when we started falling, so we can check it later for coyote time jumps
|
||||
LastFallTime = GetWorld()->GetTimeSeconds();
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::Move(const FInputActionValue& Value)
|
||||
{
|
||||
FVector2D MoveVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoMove(MoveVector.Y);
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::Drop(const FInputActionValue& Value)
|
||||
{
|
||||
// route the input
|
||||
DoDrop(Value.Get<float>());
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DropReleased(const FInputActionValue& Value)
|
||||
{
|
||||
// reset the input
|
||||
DoDrop(0.0f);
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DoMove(float Forward)
|
||||
{
|
||||
// is movement temporarily disabled after wall jumping?
|
||||
if (!bHasWallJumped)
|
||||
{
|
||||
// save the movement values
|
||||
ActionValueY = Forward;
|
||||
|
||||
// figure out the movement direction
|
||||
const FVector MoveDir = FVector(1.0f, Forward > 0.0f ? 0.1f : -0.1f, 0.0f);
|
||||
|
||||
// apply the movement input
|
||||
AddMovementInput(MoveDir, Forward);
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DoDrop(float Value)
|
||||
{
|
||||
// save the movement value
|
||||
DropValue = Value;
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DoJumpStart()
|
||||
{
|
||||
// handle advanced jump behaviors
|
||||
MultiJump();
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DoJumpEnd()
|
||||
{
|
||||
StopJumping();
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::DoInteract()
|
||||
{
|
||||
// do a sphere trace to look for interactive objects
|
||||
FHitResult OutHit;
|
||||
|
||||
const FVector Start = GetActorLocation();
|
||||
const FVector End = Start + FVector(100.0f, 0.0f, 0.0f);
|
||||
|
||||
FCollisionShape ColSphere;
|
||||
ColSphere.SetSphere(InteractionRadius);
|
||||
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
||||
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
|
||||
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
if (GetWorld()->SweepSingleByObjectType(OutHit, Start, End, FQuat::Identity, ObjectParams, ColSphere, QueryParams))
|
||||
{
|
||||
// have we hit an interactable?
|
||||
if (ISideScrollingInteractable* Interactable = Cast<ISideScrollingInteractable>(OutHit.GetActor()))
|
||||
{
|
||||
// interact
|
||||
Interactable->Interaction(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::MultiJump()
|
||||
{
|
||||
// does the user want to drop to a lower platform?
|
||||
if (DropValue > 0.0f)
|
||||
{
|
||||
CheckForSoftCollision();
|
||||
return;
|
||||
}
|
||||
|
||||
// reset the drop value
|
||||
DropValue = 0.0f;
|
||||
|
||||
// if we're grounded, disregard advanced jump logic
|
||||
if (!GetCharacterMovement()->IsFalling())
|
||||
{
|
||||
Jump();
|
||||
return;
|
||||
}
|
||||
|
||||
// if we have a horizontal input, try for wall jump first
|
||||
if (!bHasWallJumped && !FMath::IsNearlyZero(ActionValueY))
|
||||
{
|
||||
// trace ahead of the character for walls
|
||||
FHitResult OutHit;
|
||||
|
||||
const FVector Start = GetActorLocation();
|
||||
const FVector End = Start + (FVector(ActionValueY > 0.0f ? 1.0f : -1.0f, 0.0f, 0.0f) * WallJumpTraceDistance);
|
||||
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams);
|
||||
|
||||
if (OutHit.bBlockingHit)
|
||||
{
|
||||
// rotate to the bounce direction
|
||||
const FRotator BounceRot = UKismetMathLibrary::MakeRotFromX(OutHit.ImpactNormal);
|
||||
SetActorRotation(FRotator(0.0f, BounceRot.Yaw, 0.0f));
|
||||
|
||||
// calculate the impulse vector
|
||||
FVector WallJumpImpulse = OutHit.ImpactNormal * WallJumpHorizontalImpulse;
|
||||
WallJumpImpulse.Z = GetCharacterMovement()->JumpZVelocity * WallJumpVerticalMultiplier;
|
||||
|
||||
// launch the character away from the wall
|
||||
LaunchCharacter(WallJumpImpulse, true, true);
|
||||
|
||||
// enable wall jump lockout for a bit
|
||||
bHasWallJumped = true;
|
||||
|
||||
// schedule wall jump lockout reset
|
||||
GetWorld()->GetTimerManager().SetTimer(WallJumpTimer, this, &ASideScrollingCharacter::ResetWallJump, DelayBetweenWallJumps, false);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// test for double jump only if we haven't already tested for wall jump
|
||||
if (!bHasWallJumped)
|
||||
{
|
||||
// are we still within coyote time frames?
|
||||
if (GetWorld()->GetTimeSeconds() - LastFallTime < MaxCoyoteTime)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Coyote Jump"));
|
||||
|
||||
// use the built-in CMC functionality to do the jump
|
||||
Jump();
|
||||
|
||||
// no coyote time jump
|
||||
} else {
|
||||
|
||||
// The movement component handles double jump but we still need to manage the flag for animation
|
||||
if (!bHasDoubleJumped)
|
||||
{
|
||||
// raise the double jump flag
|
||||
bHasDoubleJumped = true;
|
||||
|
||||
// let the CMC handle jump
|
||||
Jump();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::CheckForSoftCollision()
|
||||
{
|
||||
// reset the drop value
|
||||
DropValue = 0.0f;
|
||||
|
||||
// trace down
|
||||
FHitResult OutHit;
|
||||
|
||||
const FVector Start = GetActorLocation();
|
||||
const FVector End = Start + (FVector::DownVector * SoftCollisionTraceDistance);
|
||||
|
||||
FCollisionObjectQueryParams ObjectParams;
|
||||
ObjectParams.AddObjectTypesToQuery(SoftCollisionObjectType);
|
||||
|
||||
FCollisionQueryParams QueryParams;
|
||||
QueryParams.AddIgnoredActor(this);
|
||||
|
||||
GetWorld()->LineTraceSingleByObjectType(OutHit, Start, End, ObjectParams, QueryParams);
|
||||
|
||||
// did we hit a soft floor?
|
||||
if (OutHit.GetActor())
|
||||
{
|
||||
// drop through the floor
|
||||
SetSoftCollision(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::ResetWallJump()
|
||||
{
|
||||
// reset the wall jump flag
|
||||
bHasWallJumped = false;
|
||||
}
|
||||
|
||||
void ASideScrollingCharacter::SetSoftCollision(bool bEnabled)
|
||||
{
|
||||
// enable or disable collision response to the soft collision channel
|
||||
GetCapsuleComponent()->SetCollisionResponseToChannel(SoftCollisionObjectType, bEnabled ? ECR_Ignore : ECR_Block);
|
||||
}
|
||||
|
||||
bool ASideScrollingCharacter::HasDoubleJumped() const
|
||||
{
|
||||
return bHasDoubleJumped;
|
||||
}
|
||||
|
||||
bool ASideScrollingCharacter::HasWallJumped() const
|
||||
{
|
||||
return bHasWallJumped;
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "SideScrollingCharacter.generated.h"
|
||||
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
struct FInputActionValue;
|
||||
|
||||
/**
|
||||
* A player-controllable character side scrolling game
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingCharacter : public ACharacter
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Player camera */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category ="Camera", meta = (AllowPrivateAccess = "true"))
|
||||
UCameraComponent* Camera;
|
||||
|
||||
protected:
|
||||
|
||||
/** Move Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* MoveAction;
|
||||
|
||||
/** Jump Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* JumpAction;
|
||||
|
||||
/** Drop from Platform Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* DropAction;
|
||||
|
||||
/** Interact Input Action */
|
||||
UPROPERTY(EditAnywhere, Category="Input")
|
||||
UInputAction* InteractAction;
|
||||
|
||||
/** Impulse to manually push physics objects while we're in midair */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Jump")
|
||||
float JumpPushImpulse = 600.0f;
|
||||
|
||||
/** Max distance that interactive objects can be triggered */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Interaction")
|
||||
float InteractionRadius = 200.0f;
|
||||
|
||||
/** Time to disable input after a wall jump to preserve momentum */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
|
||||
float DelayBetweenWallJumps = 0.3f;
|
||||
|
||||
/** Distance to trace ahead of the character for wall jumps */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
|
||||
float WallJumpTraceDistance = 50.0f;
|
||||
|
||||
/** Horizontal impulse to apply to the character during wall jumps */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
|
||||
float WallJumpHorizontalImpulse = 500.0f;
|
||||
|
||||
/** Multiplies the jump Z velocity for wall jumps. */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Wall Jump")
|
||||
float WallJumpVerticalMultiplier = 1.4f;
|
||||
|
||||
/** Collision object type to use for soft collision traces (dropping down floors) */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Soft Platforms")
|
||||
TEnumAsByte<ECollisionChannel> SoftCollisionObjectType;
|
||||
|
||||
/** Distance to trace down during soft collision checks */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Soft Platforms")
|
||||
float SoftCollisionTraceDistance = 1000.0f;
|
||||
|
||||
/** Last recorded time when this character started falling */
|
||||
float LastFallTime = 0.0f;
|
||||
|
||||
/** Max amount of time that can pass since we started falling when we allow a regular jump */
|
||||
UPROPERTY(EditAnywhere, Category="Side Scrolling|Coyote Time", meta = (ClampMin = 0, ClampMax = 5, Units = "s"))
|
||||
float MaxCoyoteTime = 0.16f;
|
||||
|
||||
/** Wall jump lockout timer */
|
||||
FTimerHandle WallJumpTimer;
|
||||
|
||||
/** Last captured horizontal movement input value */
|
||||
float ActionValueY = 0.0f;
|
||||
|
||||
/** Last captured platform drop axis value */
|
||||
float DropValue = 0.0f;
|
||||
|
||||
/** If true, this character has already wall jumped */
|
||||
bool bHasWallJumped = false;
|
||||
|
||||
/** If true, this character has already double jumped */
|
||||
bool bHasDoubleJumped = false;
|
||||
|
||||
/** If true, this character is moving along the side scrolling axis */
|
||||
bool bMovingHorizontally = false;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
ASideScrollingCharacter();
|
||||
|
||||
protected:
|
||||
|
||||
/** Gameplay cleanup */
|
||||
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/** Initialize input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
/** Collision handling */
|
||||
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
|
||||
|
||||
/** Landing handling */
|
||||
virtual void Landed(const FHitResult& Hit) override;
|
||||
|
||||
/** Handle movement mode changes to keep track of coyote time jumps */
|
||||
virtual void OnMovementModeChanged(EMovementMode PrevMovementMode, uint8 PreviousCustomMode = 0) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for drop from platform input */
|
||||
void Drop(const FInputActionValue& Value);
|
||||
|
||||
/** Called for drop from platform input release */
|
||||
void DropReleased(const FInputActionValue& Value);
|
||||
|
||||
public:
|
||||
|
||||
/** Handles move inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoMove(float Forward);
|
||||
|
||||
/** Handles drop inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoDrop(float Value);
|
||||
|
||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoJumpStart();
|
||||
|
||||
/** Handles jump pressed inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoJumpEnd();
|
||||
|
||||
/** Handles interact inputs from either controls or UI interfaces */
|
||||
UFUNCTION(BlueprintCallable, Category="Input")
|
||||
virtual void DoInteract();
|
||||
|
||||
protected:
|
||||
|
||||
/** Handles advanced jump logic */
|
||||
void MultiJump();
|
||||
|
||||
/** Checks for soft collision with platforms */
|
||||
void CheckForSoftCollision();
|
||||
|
||||
/** Resets wall jump lockout. Called from timer after a wall jump */
|
||||
void ResetWallJump();
|
||||
|
||||
public:
|
||||
|
||||
/** Sets the soft collision response. True passes, False blocks */
|
||||
void SetSoftCollision(bool bEnabled);
|
||||
|
||||
public:
|
||||
|
||||
/** Returns true if the character has just double jumped */
|
||||
UFUNCTION(BlueprintPure, Category="Side Scrolling")
|
||||
bool HasDoubleJumped() const;
|
||||
|
||||
/** Returns true if the character has just wall jumped */
|
||||
UFUNCTION(BlueprintPure, Category="Side Scrolling")
|
||||
bool HasWallJumped() const;
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingGameMode.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "SideScrollingUI.h"
|
||||
#include "SideScrollingPickup.h"
|
||||
|
||||
void ASideScrollingGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// create the game UI
|
||||
APlayerController* OwningPlayer = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
|
||||
UserInterface = CreateWidget<USideScrollingUI>(OwningPlayer, UserInterfaceClass);
|
||||
|
||||
check(UserInterface);
|
||||
}
|
||||
|
||||
void ASideScrollingGameMode::ProcessPickup()
|
||||
{
|
||||
// increment the pickups counter
|
||||
++PickupsCollected;
|
||||
|
||||
// if this is the first pickup we collect, show the UI
|
||||
if (PickupsCollected == 1)
|
||||
{
|
||||
UserInterface->AddToViewport(0);
|
||||
}
|
||||
|
||||
// update the pickups counter on the UI
|
||||
UserInterface->UpdatePickups(PickupsCollected);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "SideScrollingGameMode.generated.h"
|
||||
|
||||
class USideScrollingUI;
|
||||
|
||||
/**
|
||||
* Simple Side Scrolling Game Mode
|
||||
* Spawns and manages the game UI
|
||||
* Counts pickups collected by the player
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class ASideScrollingGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Class of UI widget to spawn when the game starts */
|
||||
UPROPERTY(EditAnywhere, Category="UI")
|
||||
TSubclassOf<USideScrollingUI> UserInterfaceClass;
|
||||
|
||||
/** User interface widget for the game */
|
||||
UPROPERTY(BlueprintReadOnly, Category="UI")
|
||||
TObjectPtr<USideScrollingUI> UserInterface;
|
||||
|
||||
/** Number of pickups collected by the player */
|
||||
UPROPERTY(BlueprintReadOnly, Category="Pickups")
|
||||
int32 PickupsCollected = 0;
|
||||
|
||||
protected:
|
||||
|
||||
/** Initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
public:
|
||||
|
||||
/** Receives an interaction event from another actor */
|
||||
virtual void ProcessPickup();
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingPlayerController.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerStart.h"
|
||||
#include "SideScrollingCharacter.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "AgrarianGame.h"
|
||||
#include "Widgets/Input/SVirtualJoystick.h"
|
||||
|
||||
void ASideScrollingPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// only spawn touch controls on local player controllers
|
||||
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
||||
{
|
||||
// spawn the mobile controls widget
|
||||
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
|
||||
|
||||
if (MobileControlsWidget)
|
||||
{
|
||||
// add the controls to the player screen
|
||||
MobileControlsWidget->AddToPlayerScreen(0);
|
||||
|
||||
} else {
|
||||
|
||||
UE_LOG(LogAgrarianGame, Error, TEXT("Could not spawn mobile controls widget."));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
// only add IMCs for local player controllers
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
// add the input mapping context
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : DefaultMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
|
||||
// only add these IMCs if we're not using mobile touch input
|
||||
if (!ShouldUseTouchControls())
|
||||
{
|
||||
for (UInputMappingContext* CurrentContext : MobileExcludedMappingContexts)
|
||||
{
|
||||
Subsystem->AddMappingContext(CurrentContext, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ASideScrollingPlayerController::OnPossess(APawn* InPawn)
|
||||
{
|
||||
Super::OnPossess(InPawn);
|
||||
|
||||
// subscribe to the pawn's OnDestroyed delegate
|
||||
InPawn->OnDestroyed.AddDynamic(this, &ASideScrollingPlayerController::OnPawnDestroyed);
|
||||
}
|
||||
|
||||
void ASideScrollingPlayerController::OnPawnDestroyed(AActor* DestroyedActor)
|
||||
{
|
||||
// find the player start
|
||||
TArray<AActor*> ActorList;
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActorList);
|
||||
|
||||
if (ActorList.Num() > 0)
|
||||
{
|
||||
// spawn a character at the player start
|
||||
const FTransform SpawnTransform = ActorList[0]->GetActorTransform();
|
||||
|
||||
if (ASideScrollingCharacter* RespawnedCharacter = GetWorld()->SpawnActor<ASideScrollingCharacter>(CharacterClass, SpawnTransform))
|
||||
{
|
||||
// possess the character
|
||||
Possess(RespawnedCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ASideScrollingPlayerController::ShouldUseTouchControls() const
|
||||
{
|
||||
// are we on a mobile platform? Should we force touch?
|
||||
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "EnhancedInput/Public/InputAction.h"
|
||||
#include "SideScrollingPlayerController.generated.h"
|
||||
|
||||
class ASideScrollingCharacter;
|
||||
class UInputMappingContext;
|
||||
|
||||
/**
|
||||
* A simple Side Scrolling Player Controller
|
||||
* Manages input mappings
|
||||
* Respawns the player pawn at the player start if it is destroyed
|
||||
*/
|
||||
UCLASS(abstract, Config="Game")
|
||||
class ASideScrollingPlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Input mapping context for this player */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> DefaultMappingContexts;
|
||||
|
||||
/** Input Mapping Contexts */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Input Mappings")
|
||||
TArray<UInputMappingContext*> MobileExcludedMappingContexts;
|
||||
|
||||
/** Mobile controls widget to spawn */
|
||||
UPROPERTY(EditAnywhere, Category="Input|Touch Controls")
|
||||
TSubclassOf<UUserWidget> MobileControlsWidgetClass;
|
||||
|
||||
/** Pointer to the mobile controls widget */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UUserWidget> MobileControlsWidget;
|
||||
|
||||
/** If true, the player will use UMG touch controls even if not playing on mobile platforms */
|
||||
UPROPERTY(EditAnywhere, Config, Category = "Input|Touch Controls")
|
||||
bool bForceTouchControls = false;
|
||||
|
||||
/** Character class to respawn when the possessed pawn is destroyed */
|
||||
UPROPERTY(EditAnywhere, Category="Respawn")
|
||||
TSubclassOf<ASideScrollingCharacter> CharacterClass;
|
||||
|
||||
protected:
|
||||
|
||||
/** Gameplay initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Initialize input bindings */
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
/** Pawn initialization */
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
/** Called if the possessed pawn is destroyed */
|
||||
UFUNCTION()
|
||||
void OnPawnDestroyed(AActor* DestroyedActor);
|
||||
|
||||
/** Returns true if the player should use UMG touch controls */
|
||||
bool ShouldUseTouchControls() const;
|
||||
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingUI.h"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "SideScrollingUI.generated.h"
|
||||
|
||||
/**
|
||||
* Simple Side Scrolling game UI
|
||||
* Displays and manages a pickup counter
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class USideScrollingUI : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Update the widget's pickup counter */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category="UI")
|
||||
void UpdatePickups(int32 Amount);
|
||||
};
|
||||
Reference in New Issue
Block a user