Initial Commit - Lesson 3 - Project Creation and Assets
This commit is contained in:
15
Source/InventoryProject.Target.cs
Normal file
15
Source/InventoryProject.Target.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class InventoryProjectTarget : TargetRules
|
||||
{
|
||||
public InventoryProjectTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Game;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
|
||||
ExtraModuleNames.Add("InventoryProject");
|
||||
}
|
||||
}
|
||||
51
Source/InventoryProject/InventoryProject.Build.cs
Normal file
51
Source/InventoryProject/InventoryProject.Build.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class InventoryProject : ModuleRules
|
||||
{
|
||||
public InventoryProject(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] {
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"InputCore",
|
||||
"EnhancedInput",
|
||||
"AIModule",
|
||||
"StateTreeModule",
|
||||
"GameplayStateTreeModule",
|
||||
"UMG",
|
||||
"Slate"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { });
|
||||
|
||||
PublicIncludePaths.AddRange(new string[] {
|
||||
"InventoryProject",
|
||||
"InventoryProject/Variant_Platforming",
|
||||
"InventoryProject/Variant_Platforming/Animation",
|
||||
"InventoryProject/Variant_Combat",
|
||||
"InventoryProject/Variant_Combat/AI",
|
||||
"InventoryProject/Variant_Combat/Animation",
|
||||
"InventoryProject/Variant_Combat/Gameplay",
|
||||
"InventoryProject/Variant_Combat/Interfaces",
|
||||
"InventoryProject/Variant_Combat/UI",
|
||||
"InventoryProject/Variant_SideScrolling",
|
||||
"InventoryProject/Variant_SideScrolling/AI",
|
||||
"InventoryProject/Variant_SideScrolling/Gameplay",
|
||||
"InventoryProject/Variant_SideScrolling/Interfaces",
|
||||
"InventoryProject/Variant_SideScrolling/UI"
|
||||
});
|
||||
|
||||
// Uncomment if you are using Slate UI
|
||||
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
// Uncomment if you are using online features
|
||||
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
|
||||
|
||||
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
|
||||
}
|
||||
}
|
||||
8
Source/InventoryProject/InventoryProject.cpp
Normal file
8
Source/InventoryProject/InventoryProject.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InventoryProject.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, InventoryProject, "InventoryProject" );
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogInventoryProject)
|
||||
8
Source/InventoryProject/InventoryProject.h
Normal file
8
Source/InventoryProject/InventoryProject.h
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
/** Main log category used across the project */
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogInventoryProject, Log, All);
|
||||
133
Source/InventoryProject/InventoryProjectCharacter.cpp
Normal file
133
Source/InventoryProject/InventoryProjectCharacter.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InventoryProjectCharacter.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "GameFramework/CharacterMovementComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "GameFramework/Controller.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputActionValue.h"
|
||||
#include "InventoryProject.h"
|
||||
|
||||
AInventoryProjectCharacter::AInventoryProjectCharacter()
|
||||
{
|
||||
// Set size for collision capsule
|
||||
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
|
||||
|
||||
// Don't rotate when the controller rotates. Let that just affect the camera.
|
||||
bUseControllerRotationPitch = false;
|
||||
bUseControllerRotationYaw = false;
|
||||
bUseControllerRotationRoll = false;
|
||||
|
||||
// Configure character movement
|
||||
GetCharacterMovement()->bOrientRotationToMovement = true;
|
||||
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
|
||||
|
||||
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
|
||||
// instead of recompiling to adjust them
|
||||
GetCharacterMovement()->JumpZVelocity = 500.f;
|
||||
GetCharacterMovement()->AirControl = 0.35f;
|
||||
GetCharacterMovement()->MaxWalkSpeed = 500.f;
|
||||
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
|
||||
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
|
||||
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
|
||||
|
||||
// Create a camera boom (pulls in towards the player if there is a collision)
|
||||
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
||||
CameraBoom->SetupAttachment(RootComponent);
|
||||
CameraBoom->TargetArmLength = 400.0f;
|
||||
CameraBoom->bUsePawnControlRotation = true;
|
||||
|
||||
// Create a follow camera
|
||||
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
||||
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
||||
FollowCamera->bUsePawnControlRotation = false;
|
||||
|
||||
// Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
|
||||
// are set in the derived blueprint asset named ThirdPersonCharacter (to avoid direct content references in C++)
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
||||
{
|
||||
// Set up action bindings
|
||||
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
|
||||
|
||||
// Jumping
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &ACharacter::Jump);
|
||||
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &ACharacter::StopJumping);
|
||||
|
||||
// Moving
|
||||
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AInventoryProjectCharacter::Move);
|
||||
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AInventoryProjectCharacter::Look);
|
||||
|
||||
// Looking
|
||||
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AInventoryProjectCharacter::Look);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogInventoryProject, Error, TEXT("'%s' Failed to find an Enhanced Input component! This template is built to use the Enhanced Input system. If you intend to use the legacy system, then you will need to update this C++ file."), *GetNameSafe(this));
|
||||
}
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::Move(const FInputActionValue& Value)
|
||||
{
|
||||
// input is a Vector2D
|
||||
FVector2D MovementVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoMove(MovementVector.X, MovementVector.Y);
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::Look(const FInputActionValue& Value)
|
||||
{
|
||||
// input is a Vector2D
|
||||
FVector2D LookAxisVector = Value.Get<FVector2D>();
|
||||
|
||||
// route the input
|
||||
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::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 AInventoryProjectCharacter::DoLook(float Yaw, float Pitch)
|
||||
{
|
||||
if (GetController() != nullptr)
|
||||
{
|
||||
// add yaw and pitch input to controller
|
||||
AddControllerYawInput(Yaw);
|
||||
AddControllerPitchInput(Pitch);
|
||||
}
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::DoJumpStart()
|
||||
{
|
||||
// signal the character to jump
|
||||
Jump();
|
||||
}
|
||||
|
||||
void AInventoryProjectCharacter::DoJumpEnd()
|
||||
{
|
||||
// signal the character to stop jumping
|
||||
StopJumping();
|
||||
}
|
||||
96
Source/InventoryProject/InventoryProjectCharacter.h
Normal file
96
Source/InventoryProject/InventoryProjectCharacter.h
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Character.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
#include "InventoryProjectCharacter.generated.h"
|
||||
|
||||
class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
struct FInputActionValue;
|
||||
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogTemplateCharacter, Log, All);
|
||||
|
||||
/**
|
||||
* A simple player-controllable third person character
|
||||
* Implements a controllable orbiting camera
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class AInventoryProjectCharacter : 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;
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
AInventoryProjectCharacter();
|
||||
|
||||
protected:
|
||||
|
||||
/** Initialize input action bindings */
|
||||
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Called for movement input */
|
||||
void Move(const FInputActionValue& Value);
|
||||
|
||||
/** Called for looking input */
|
||||
void Look(const FInputActionValue& Value);
|
||||
|
||||
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 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();
|
||||
|
||||
public:
|
||||
|
||||
/** Returns CameraBoom subobject **/
|
||||
FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
|
||||
|
||||
/** Returns FollowCamera subobject **/
|
||||
FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
|
||||
};
|
||||
|
||||
8
Source/InventoryProject/InventoryProjectGameMode.cpp
Normal file
8
Source/InventoryProject/InventoryProjectGameMode.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "InventoryProjectGameMode.h"
|
||||
|
||||
AInventoryProjectGameMode::AInventoryProjectGameMode()
|
||||
{
|
||||
// stub
|
||||
}
|
||||
24
Source/InventoryProject/InventoryProjectGameMode.h
Normal file
24
Source/InventoryProject/InventoryProjectGameMode.h
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/GameModeBase.h"
|
||||
#include "InventoryProjectGameMode.generated.h"
|
||||
|
||||
/**
|
||||
* Simple GameMode for a third person game
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class AInventoryProjectGameMode : public AGameModeBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
|
||||
/** Constructor */
|
||||
AInventoryProjectGameMode();
|
||||
};
|
||||
|
||||
|
||||
|
||||
67
Source/InventoryProject/InventoryProjectPlayerController.cpp
Normal file
67
Source/InventoryProject/InventoryProjectPlayerController.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "InventoryProjectPlayerController.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InventoryProject.h"
|
||||
#include "Widgets/Input/SVirtualJoystick.h"
|
||||
|
||||
void AInventoryProjectPlayerController::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(LogInventoryProject, Error, TEXT("Could not spawn mobile controls widget."));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AInventoryProjectPlayerController::SetupInputComponent()
|
||||
{
|
||||
Super::SetupInputComponent();
|
||||
|
||||
// only add IMCs for local player controllers
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
// Add Input Mapping Contexts
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool AInventoryProjectPlayerController::ShouldUseTouchControls() const
|
||||
{
|
||||
// are we on a mobile platform? Should we force touch?
|
||||
return SVirtualJoystick::ShouldDisplayTouchInterface() || bForceTouchControls;
|
||||
}
|
||||
52
Source/InventoryProject/InventoryProjectPlayerController.h
Normal file
52
Source/InventoryProject/InventoryProjectPlayerController.h
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "InventoryProjectPlayerController.generated.h"
|
||||
|
||||
class UInputMappingContext;
|
||||
class UUserWidget;
|
||||
|
||||
/**
|
||||
* Basic PlayerController class for a third person game
|
||||
* Manages input mappings
|
||||
*/
|
||||
UCLASS(abstract)
|
||||
class AInventoryProjectPlayerController : public APlayerController
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
|
||||
/** Input Mapping Contexts */
|
||||
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;
|
||||
|
||||
/** Gameplay initialization */
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Input mapping context setup */
|
||||
virtual void SetupInputComponent() override;
|
||||
|
||||
/** Returns true if the player should use UMG touch controls */
|
||||
bool ShouldUseTouchControls() const;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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();
|
||||
};
|
||||
343
Source/InventoryProject/Variant_Combat/AI/CombatEnemy.cpp
Normal file
343
Source/InventoryProject/Variant_Combat/AI/CombatEnemy.cpp
Normal file
@@ -0,0 +1,343 @@
|
||||
// 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);
|
||||
}
|
||||
232
Source/InventoryProject/Variant_Combat/AI/CombatEnemy.h
Normal file
232
Source/InventoryProject/Variant_Combat/AI/CombatEnemy.h
Normal file
@@ -0,0 +1,232 @@
|
||||
// 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;
|
||||
};
|
||||
126
Source/InventoryProject/Variant_Combat/AI/CombatEnemySpawner.cpp
Normal file
126
Source/InventoryProject/Variant_Combat/AI/CombatEnemySpawner.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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
|
||||
}
|
||||
109
Source/InventoryProject/Variant_Combat/AI/CombatEnemySpawner.h
Normal file
109
Source/InventoryProject/Variant_Combat/AI/CombatEnemySpawner.h
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,325 @@
|
||||
// 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
|
||||
@@ -0,0 +1,365 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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 INVENTORYPROJECT_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;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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;
|
||||
};
|
||||
547
Source/InventoryProject/Variant_Combat/CombatCharacter.cpp
Normal file
547
Source/InventoryProject/Variant_Combat/CombatCharacter.cpp
Normal file
@@ -0,0 +1,547 @@
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
||||
334
Source/InventoryProject/Variant_Combat/CombatCharacter.h
Normal file
334
Source/InventoryProject/Variant_Combat/CombatCharacter.h
Normal file
@@ -0,0 +1,334 @@
|
||||
// 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; }
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Combat/CombatGameMode.h"
|
||||
|
||||
ACombatGameMode::ACombatGameMode()
|
||||
{
|
||||
|
||||
}
|
||||
20
Source/InventoryProject/Variant_Combat/CombatGameMode.h
Normal file
20
Source/InventoryProject/Variant_Combat/CombatGameMode.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 "InventoryProject.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(LogInventoryProject, 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;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatActivatable.h"
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatAttacker.h"
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatDamageable.h"
|
||||
|
||||
// Add default functionality here for any ICombatDamageable functions that are not pure virtual.
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "CombatLifeBar.h"
|
||||
|
||||
26
Source/InventoryProject/Variant_Combat/UI/CombatLifeBar.h
Normal file
26
Source/InventoryProject/Variant_Combat/UI/CombatLifeBar.h
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,367 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// 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; }
|
||||
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "Variant_Platforming/PlatformingGameMode.h"
|
||||
|
||||
APlatformingGameMode::APlatformingGameMode()
|
||||
{
|
||||
// stub
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 "InventoryProject.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(LogInventoryProject, 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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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();
|
||||
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingInteractable.h"
|
||||
|
||||
// Add default functionality here for any IInteractable functions that are not pure virtual.
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,350 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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();
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 "InventoryProject.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(LogInventoryProject, 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;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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;
|
||||
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingUI.h"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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);
|
||||
};
|
||||
15
Source/InventoryProjectEditor.Target.cs
Normal file
15
Source/InventoryProjectEditor.Target.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class InventoryProjectEditorTarget : TargetRules
|
||||
{
|
||||
public InventoryProjectEditorTarget(TargetInfo Target) : base(Target)
|
||||
{
|
||||
Type = TargetType.Editor;
|
||||
DefaultBuildSettings = BuildSettingsVersion.V6;
|
||||
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_7;
|
||||
ExtraModuleNames.Add("InventoryProject");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user