Initial Commit

This commit is contained in:
2026-08-27 21:42:15 +02:00
parent b3bc22499d
commit 78730ce939
353 changed files with 7863 additions and 72 deletions
@@ -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 "KingshearthLegacy.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(LogKingshearthLegacy, 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);
};