Step 1: the Salty project, two modules, tests and the gym
Salty.uproject (UE 5.8) from the Third Person template with class redirects, the SaltyCore and Salty modules, the three build targets, USaltyAssetManager calling InitGlobalData, Config/Tags with the 23 root namespaces, Scripts/run-tests.sh, build.sh and Authoring/create_gym.py, L_Gym, Git LFS attributes and the placeholder test. Template variants kept as reference (D-41). The four Fab packs stay out of the repository for now (~10 GB). The editor serves the engine MCP plugin on 127.0.0.1:8000 through DefaultEditorPerProjectUserSettings.ini. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0e61a77346
commit
4f2c55cd2a
@@ -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,59 @@
|
||||
// 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::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
|
||||
{
|
||||
// get the instance data
|
||||
FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
|
||||
|
||||
// reset the selected target
|
||||
APawn* SelectedTarget = nullptr;
|
||||
float ClosestDistance = 1000000000000000000.0f;
|
||||
|
||||
// iterate through each local player
|
||||
const int32 NumPlayers = UGameplayStatics::GetNumLocalPlayerControllers(InstanceData.Controller.Get());
|
||||
|
||||
for (int32 i = 0; i < NumPlayers; ++i)
|
||||
{
|
||||
if (APawn* Current = UGameplayStatics::GetPlayerPawn(InstanceData.Controller.Get(), i))
|
||||
{
|
||||
// compute the distance to the target
|
||||
const float TargetDist = (Current->GetActorLocation() - InstanceData.NPC->GetActorLocation()).Size();
|
||||
|
||||
// if we haven't selected a target, or this one is closer to the current one, choose this pawn as target
|
||||
if (!SelectedTarget || TargetDist < ClosestDistance)
|
||||
{
|
||||
SelectedTarget = Current;
|
||||
ClosestDistance = TargetDist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the selected target, assume out of range by default
|
||||
InstanceData.TargetPlayer = SelectedTarget;
|
||||
InstanceData.bValidTarget = false;
|
||||
|
||||
if (SelectedTarget)
|
||||
{
|
||||
// consider this a valid target if it's within range
|
||||
const float TargetDist = (SelectedTarget->GetActorLocation() - InstanceData.NPC->GetActorLocation()).Size();
|
||||
|
||||
InstanceData.bValidTarget = TargetDist < InstanceData.RangeMax;
|
||||
}
|
||||
|
||||
// succeed or fail depending on target validity
|
||||
return InstanceData.bValidTarget ? EStateTreeRunStatus::Succeeded : EStateTreeRunStatus::Failed;
|
||||
}
|
||||
|
||||
#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,68 @@
|
||||
// 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()
|
||||
|
||||
FStateTreeGetPlayerTask()
|
||||
{
|
||||
// disable tick
|
||||
bShouldCallTick = false;
|
||||
|
||||
// skip state change events if this is sustained
|
||||
bShouldStateChangeOnReselect = false;
|
||||
}
|
||||
|
||||
/* Ensure we're using the correct instance data struct */
|
||||
using FInstanceDataType = FStateTreeGetPlayerInstanceData;
|
||||
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
|
||||
};
|
||||
@@ -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,77 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
|
||||
#include "SideScrollingGameMode.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "SideScrollingUI.h"
|
||||
#include "SideScrollingPickup.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/PlayerStart.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
void ASideScrollingGameMode::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// create the game UI
|
||||
APlayerController* OwningPlayer = UGameplayStatics::GetPlayerController(GetWorld(), 0);
|
||||
|
||||
UserInterface = CreateWidget<USideScrollingUI>(OwningPlayer, UserInterfaceClass);
|
||||
|
||||
// create each additional local player.
|
||||
// Player 0 will be created automatically as part of regular game init
|
||||
for (int32 i = 2; i <= NumberOfLocalPlayers; ++i)
|
||||
{
|
||||
UGameplayStatics::CreatePlayer(GetWorld(), -1, true);
|
||||
}
|
||||
}
|
||||
|
||||
AActor* ASideScrollingGameMode::ChoosePlayerStart_Implementation(AController* Player)
|
||||
{
|
||||
// build the current player tag
|
||||
FName PlayerTag = FName(*FString::Printf(TEXT("Player%d"), CurrentPlayerStartAssignment));
|
||||
|
||||
// find all player starts with the matching player tag
|
||||
TArray<AActor*> PlayerStarts;
|
||||
|
||||
UGameplayStatics::GetAllActorsOfClassWithTag(GetWorld(), APlayerStart::StaticClass(), PlayerTag, PlayerStarts);
|
||||
|
||||
// increment the player start assignment index
|
||||
++CurrentPlayerStartAssignment;
|
||||
|
||||
// if no PlayerStarts were found, default to all PlayerStarts instead
|
||||
if (PlayerStarts.IsEmpty())
|
||||
{
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), PlayerStarts);
|
||||
}
|
||||
|
||||
// have we found at least one PlayerStart?
|
||||
if (!PlayerStarts.IsEmpty())
|
||||
{
|
||||
return PlayerStarts[ FMath::RandRange(0, PlayerStarts.Num() - 1) ];
|
||||
}
|
||||
|
||||
// no PlayerStarts in the level
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ASideScrollingGameMode::ProcessPickup()
|
||||
{
|
||||
// increment the pickups counter
|
||||
++PickupsCollected;
|
||||
|
||||
if (UserInterface)
|
||||
{
|
||||
// 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,56 @@
|
||||
// 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;
|
||||
|
||||
/** Assigns a PlayerStart to a specific player */
|
||||
virtual AActor* ChoosePlayerStart_Implementation(AController* Player) override;
|
||||
|
||||
protected:
|
||||
|
||||
/** Determines how many local players should be spawned on game start */
|
||||
UPROPERTY(EditDefaultsOnly, Category="Local Multiplayer", meta = (ClampMin = 1, ClampMax = 4))
|
||||
int32 NumberOfLocalPlayers = 1;
|
||||
|
||||
/** Used to assign players to different PlayerStarts in the level */
|
||||
int32 CurrentPlayerStartAssignment = 0;
|
||||
|
||||
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 "Salty.h"
|
||||
#include "Widgets/Input/SVirtualJoystick.h"
|
||||
|
||||
void ASideScrollingPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only spawn touch controls on local player controllers
|
||||
if (IsLocalPlayerController() && ShouldUseTouchControls())
|
||||
{
|
||||
// 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(LogSalty, Error, TEXT("Could not spawn mobile controls widget."));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
Reference in New Issue
Block a user