Tag

tutorial

Unreal C++ – Puzzle Mechanics – Pressure Plates and Doors by moving Static Meshes & using Interfaces

By | Props, Puzzle, Tutorial, Unreal | No Comments

Hey guys,

This new set of tutorials is meant to tackle re-creating puzzle mechanics from our favorite games, starting with pressure plates and doors.

The project files for this video & article can be found on our GitHub page.

To get started we need to create four elements that will comprise our puzzle scene:

  • First we need to extend a Static Mesh Component so we can make it move around.
  • We need a pressure plate object that will handle the triggering of other elements in the level.
  • We need elements in the level that will be affected by the trigger ie) doors / windows
  • And lastly we need an interface that will allow for ease of communication between the last two elements.

So let’s start by creating a custom Static Mesh Component. Below is the header definition.

UENUM(BlueprintType)
enum class ERotationAxis : uint8
{
  Yaw, Pitch, Roll
};

UENUM(BlueprintType)
enum class ELocationAxis : uint8
{
  X, Y, Z
};

UENUM(BlueprintType)
enum class EMovementType : uint8
{
  Location, Rotation
};

/**
 * 
 */
UCLASS()
class PUZZLE1_API UMovableStaticMeshComponent : public UStaticMeshComponent
{
  GENERATED_BODY()

public:

  UMovableStaticMeshComponent();

  virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
  virtual void BeginPlay() override;

  UFUNCTION(BlueprintCallable)
  void Move(bool bTriggered);

  UFUNCTION()
  void OnMove();

  UFUNCTION()
  void OnMoveFinished();

  bool IsMoving() const { return bIsMoving; }
  bool IsTriggered() const { return bIsTriggered; }

public:
  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Movement")
  UCurveFloat* MoveCurve;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Movement")
  EMovementType MovementType;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Movement", meta=(EditCondition="MovementType == EMovementType::Rotation"))
  ERotationAxis RotateAxis;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Movement", meta=(EditCondition="MovementType == EMovementType::Location"))
  ELocationAxis LocationAxis;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Movement")
  bool bIsReversed;

private:
  void UpdateRotation(float CurveValue);
  void UpdateLocation(float CurveValue);

private:
  bool bIsMoving;
  FTimeline MoveTimeline;

  bool bIsTriggered;
  float PreviousTimelineValue;
};

Then we can define the inner workings of our component

UMovableStaticMeshComponent::UMovableStaticMeshComponent()
{
  PrimaryComponentTick.bCanEverTick = true;
}

void UMovableStaticMeshComponent::BeginPlay()
{
  Super::BeginPlay();

  if(MoveCurve)
  {
    FOnTimelineFloat TimelineCallback;
    FOnTimelineEventStatic TimelineFinishCallback;

    TimelineCallback.BindUFunction(this, FName("OnMove"));
    TimelineFinishCallback.BindUFunction(this, FName("OnMoveFinished"));

    MoveTimeline.AddInterpFloat(MoveCurve, TimelineCallback);
    MoveTimeline.SetTimelineFinishedFunc(TimelineFinishCallback);
  }
}

void UMovableStaticMeshComponent::TickComponent(float DeltaTime, ELevelTick Tick, FActorComponentTickFunction* ThisTickFunction)
{
  Super::TickComponent(DeltaTime, Tick, ThisTickFunction);

  if(bIsMoving)
  {
    MoveTimeline.TickTimeline(DeltaTime);
  }
}

void UMovableStaticMeshComponent::Move(bool bTriggered)
{
  bIsTriggered = bTriggered;

  if(bTriggered)
  {
    if(MoveTimeline.IsReversing())
    {
      MoveTimeline.Play();
    }
    else
    {
      MoveTimeline.PlayFromStart();	
    }
  }
  else
  {
    MoveTimeline.Reverse();
  }

  bIsMoving = true;
}

void UMovableStaticMeshComponent::OnMove()
{
  const float PlaybackPosition = MoveTimeline.GetPlaybackPosition();
  float CurveValue = MoveCurve->GetFloatValue(PlaybackPosition);

  if(bIsReversed)
  {
    CurveValue = -CurveValue;
  }

  const float AdjustedValue = CurveValue - PreviousTimelineValue;

  if(MovementType == EMovementType::Location)
  {
    UpdateLocation(AdjustedValue);
  }
  else if(MovementType == EMovementType::Rotation)
  {
    UpdateRotation(AdjustedValue);
  }

  PreviousTimelineValue = CurveValue;
}

void UMovableStaticMeshComponent::OnMoveFinished()
{
  bIsMoving = false;
}

void UMovableStaticMeshComponent::UpdateRotation(float CurveValue)
{
  FRotator NewRotation = GetRelativeRotation();

  switch(RotateAxis)
  {
    case ERotationAxis::Pitch:
      NewRotation.Pitch += CurveValue;
      break;
    case ERotationAxis::Roll:
      NewRotation.Roll += CurveValue;
      break;
    case ERotationAxis::Yaw:
      NewRotation.Yaw += CurveValue;
      break;
    default:
      break;
  }

  SetRelativeRotation(NewRotation);
}


void UMovableStaticMeshComponent::UpdateLocation(float CurveValue)
{
  FVector NewLocation = GetRelativeLocation();

  switch(LocationAxis)
  {
    case ELocationAxis::X:
      NewLocation.X += CurveValue;
      break;
    case ELocationAxis::Y:
      NewLocation.Y += CurveValue;
      break;
    case ELocationAxis::Z:
      NewLocation.Z += CurveValue;
      break;
    default:
      break;
  }

  SetRelativeLocation(NewLocation);
}

As you can see we pass the majority of the work two the two callback functions our timeline is interfacing with: OnMove and OnMoveFinished.

TimelineCallback.BindUFunction(this, FName("OnMove")); 
TimelineFinishCallback.BindUFunction(this, FName("OnMoveFinished"));

OnMove handles the actual position / rotation of the static mesh relative to it’s parent base mesh object.

By overwriting the components Tick Component function we can start / stop the timeline which in turn affects the behavior of our static mesh.

So let’s see how this component can now be used to create a pressure plate.

Let’s define the header first.

UCLASS()
class PUZZLE1_API APressurePlateTrigger : public AActor
{
  GENERATED_BODY()
  
public:	
  // Sets default values for this actor's properties
  APressurePlateTrigger();

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:	
  // Called every frame
  virtual void Tick(float DeltaTime) override;
  UFUNCTION()
  void OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult);
  UFUNCTION()
  void OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);

private:
  void Interact(bool bIsInteracting);

public:
  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAcess = "true"), Category = "Trigger")
  UStaticMeshComponent* BaseMesh;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAcess = "true"), Category = "Trigger")
  UMovableStaticMeshComponent* MoveableMesh;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta=(AllowPrivateAcess = "true"), Category = "Trigger")
  USphereComponent* OverlapComponent;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Trigger")
  bool bIsDisabled;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Trigger")
  bool bResetTrigger;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Trigger")
  TArray<FName> TargetTags;

private:
  bool bIsTriggered;
};

Based on this definition we can see there are a couple of parts that go into our pressureplate.

We have a Static Mesh that acts as our base and a Movable Static Mesh Component as the piece that will go down as our character steps on it.

Lastly we use the overlap of our Sphere Component to trigger the behavior.

// Sets default values
APressurePlateTrigger::APressurePlateTrigger()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Mesh"));
  SetRootComponent(BaseMesh);

  MoveableMesh = CreateDefaultSubobject<UMovableStaticMeshComponent>(TEXT("Movable Mesh"));
  MoveableMesh->SetupAttachment(BaseMesh);

  OverlapComponent = CreateDefaultSubobject<USphereComponent>(TEXT("Overlap Area"));
  OverlapComponent->SetupAttachment(MoveableMesh);
}

// Called when the game starts or when spawned
void APressurePlateTrigger::BeginPlay()
{
  Super::BeginPlay();

  OverlapComponent->OnComponentBeginOverlap.AddDynamic(this, &ThisClass::OnBeginOverlap);
  OverlapComponent->OnComponentEndOverlap.AddDynamic(this, &ThisClass::OnEndOverlap);
}

// Called every frame
void APressurePlateTrigger::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);

}

void APressurePlateTrigger::Interact(bool bIsInteracting)
{
  TArray<AActor*> InteractableActors;
  UGameplayStatics::GetAllActorsWithInterface(GetWorld(), UInteractableInterface::StaticClass(), InteractableActors);

  for(AActor* Actor : InteractableActors)
  {
    FName InteractTag = IInteractableInterface::Execute_GetInteractTag(Actor);
    for(FName Tag : TargetTags)
    {
      if(InteractTag.IsEqual(Tag, ENameCase::IgnoreCase))
      {
        IInteractableInterface::Execute_Interact(Actor, bIsInteracting);
      }
    }
  }
}

void APressurePlateTrigger::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
  if(!bIsDisabled && !bIsTriggered)
  {
    MoveableMesh->Move(true);
    Interact(true);

    bIsTriggered = true;
  }
}

void APressurePlateTrigger::OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
  if(!bIsDisabled && bResetTrigger)
  {
    MoveableMesh->Move(false);
    Interact(false);

    bIsTriggered = false;
  }
}

Since we offloaded the majority of the work to our Movable Static Mesh Component you will notice that this actor is really not doing much of anything.

It handles the Begin and End overlap states to simply notify the movable component that it needs to perform some work.

The only tricky bit is examining the world for our affected components. This is done by using the TargetTags against any actor that implements a specific interface.

TArray<AActor*> InteractableActors;
UGameplayStatics::GetAllActorsWithInterface(GetWorld(), UInteractableInterface::StaticClass(), InteractableActors);

for(AActor* Actor : InteractableActors)
{
  FName InteractTag = IInteractableInterface::Execute_GetInteractTag(Actor);
  for(FName Tag : TargetTags)
  {
    if(InteractTag.IsEqual(Tag, ENameCase::IgnoreCase))
    {
      IInteractableInterface::Execute_Interact(Actor, bIsInteracting);
    }
  }
}

Using UGameplayStatics::GetAllActorsWithInterface we can find all the appropriate actors and then use the interface calls directly to call the specific methods on t hose actors.

Let’s go ahead and define our interface as we haven’t done that yet.

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UInteractableInterface : public UInterface
{
  GENERATED_BODY()
};

/**
 * 
 */
class PUZZLE1_API IInteractableInterface
{
  GENERATED_BODY()

  // Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interact")
  FName GetInteractTag();

  UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interact")
  void Interact(bool bInteracting);
};

The two methods of GetInteractTag and Interact is what our doors / windows / chests etc will have to implement in order for our pressure plate to work.

We are going to create a basic actor that similar to our pressure plate actor only contains a few meshes but inherits our new interface.

UCLASS()
class PUZZLE1_API AMovingStaticMeshActor : public AActor, public IInteractableInterface
{
  GENERATED_BODY()
  
public:	
  // Sets default values for this actor's properties
  AMovingStaticMeshActor();

  virtual FName GetInteractTag_Implementation() override;
  virtual void Interact_Implementation(bool bInteracting) override;

  bool IsDisabled() const { return bIsDisabled; }

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:	
  // Called every frame
  virtual void Tick(float DeltaTime) override;


public:
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"), Category = "Moving")
  UStaticMeshComponent* BaseMesh;

  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"), Category = "Moving")
  UMovableStaticMeshComponent* MovableMesh;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Moving")
  FName InteractTag;

  UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Moving")
  bool bIsDisabled;
};

The other property we need to ensure is available to our new actor is the InteractTag which our GetInteractTag method returns. This is important as we use those values do to a string match.

Now let’s define the body of our new actor.

// Sets default values
AMovingStaticMeshActor::AMovingStaticMeshActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Base Mesh"));
  SetRootComponent(BaseMesh);

  MovableMesh = CreateDefaultSubobject<UMovableStaticMeshComponent>(TEXT("Movable Mesh"));
  MovableMesh->SetupAttachment(GetRootComponent());
}

// Called when the game starts or when spawned
void AMovingStaticMeshActor::BeginPlay()
{
  Super::BeginPlay();
}

// Called every frame
void AMovingStaticMeshActor::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);
}

FName AMovingStaticMeshActor::GetInteractTag_Implementation()
{
  return InteractTag;
}

void AMovingStaticMeshActor::Interact_Implementation(bool bInteracting)
{
  if(!bIsDisabled)
  {
    MovableMesh->Move(bInteracting);
  }
}

Again very simple logic as all we are doing is triggering the Movable Static Mesh Component when someone interacts with this actor.

This actor can now be used to represent a door, window, trunk of a car, or even an elevator. Basically anything that has a single component that you need to move or rotate this will give you that flexbility.

Please check out the video and the GitHub page for more details.

Additionally find the Blueprint version of this tutorial on our Patreon.

Unreal Engine C++ Fundamentals – Using Inheritance by moving the player & particles along a spline

By | Tutorial, Unreal | No Comments

Hey guys,

Today we are going to continue exploring our last tutorial by looking at inheritance and what it takes to refactor our class.

The project files for this video & article can be found on our GitHub page.

The majority of the inheritance work is by creating a base class that our children can derive from which contains the primary logic that all children are always expected to perform.

To do this we are going to take our previous MovingSplineActor class make some of it’s methods virtual and migrate all of it’s Mesh specific logic to a new MeshMovingSplineActor class.

My making our MovingSplineActor functions virtual we provide the child class to override said functions and implement it’s own logic.

Since we need to have our MeshMovingSplineActor do it’s own work on a static mesh that the parent MovingSplineActor those methods need to be accessible otherwise it’s going to cause conflicts in our logic.

UCLASS()
class UE4FUNDAMENTALS14LIVE_API AMovingSplineActor : public AActor
{
  GENERATED_BODY()
  
public:	
  // Sets default values for this actor's properties
  AMovingSplineActor();

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:	
  // Called every frame
  virtual void Tick(float DeltaTime) override;

  UFUNCTION()
  virtual void ProcessMovementTimeline(float Value);

  UFUNCTION()
  virtual void OnEndMovementTimeline();

  UFUNCTION()
  virtual void TriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
  UFUNCTION()
  virtual void TriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
}

Additionally we need to ensure that the logic we had in our MovingSplineActor is available to our child class. In this case we need to move the SplineLocation and SplineRotation variables out of the initial processing logic and into their own protected variables.

protected:
  FVector StartingSplineLocation;
  FVector CurrentSplineLocation;
  FRotator CurrentSplineRotation;

We can then modify our CPP file to ensure those variables are populated.

void AMovingSplineActor::ProcessMovementTimeline(float Value)
{
  const float SplineLength = SplineComponent->GetSplineLength();

  CurrentSplineLocation = SplineComponent->GetLocationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);
  CurrentSplineRotation = SplineComponent->GetRotationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);
}

By migrating all of our logic to the MeshMovingSplineActor we need to ensure it inherits from our new parent class. To do this we need to modify the header definitions to change it from AActor.

UCLASS()
class UE4FUNDAMENTALS14LIVE_API AMeshMovingSplineActor : public AMovingSplineActor
{
  GENERATED_BODY()
  
public:	
  // Sets default values for this actor's properties
  AMeshMovingSplineActor();

protected:
  // Called when the game starts or when spawned
  virtual void BeginPlay() override;

public:	
  // Called every frame
  virtual void Tick(float DeltaTime) override;

  virtual void ProcessMovementTimeline(float Value) override;

  UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Spline", meta=(AllowPrivateAccess = "true"))
  UStaticMeshComponent* MeshComponent;
};

With our headers defined we can now migrate the logic of actually moving the spline component into our new child actor.

AMeshMovingSplineActor::AMeshMovingSplineActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh Component"));
  MeshComponent->SetupAttachment(SplineComponent);

  TriggerComponent->SetupAttachment(MeshComponent);
}

// Called when the game starts or when spawned
void AMeshMovingSplineActor::BeginPlay()
{
  Super::BeginPlay();

  MeshComponent->SetWorldLocation(StartingSplineLocation);
}

// Called every frame
void AMeshMovingSplineActor::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);

}

void AMeshMovingSplineActor::ProcessMovementTimeline(float Value)
{
  Super::ProcessMovementTimeline(Value);

  FRotator SplineRotation = CurrentSplineRotation;
  SplineRotation.Pitch = 0.f;
  MeshComponent->SetWorldLocationAndRotation(CurrentSplineLocation, SplineRotation);
}

If you notice, each override method ends up calling “Super::MethodSignature()” in order to execute the logic in the parent. Since our parent is simply moving the timeline around we don’t have to worry about anything other than the mesh in our timeline.

So that’s it, you just did some inhertiance work by shuffling around a few class definitions.

If you would like to see more examples check out the video as well as the GitHub project for examples of child classes that move the player and particles around.

Unreal Engine C++ Fundamentals – Moving Static Meshes along a Spline Component

By | Props, Tutorial, Unreal | No Comments

Hey guys,

Today we are going to take a look how to use Spline Components and modify their various properties to allow us to specify custom materials as well as determine how meshes are attached to the spline.

The project files for this video & article can be found on our GitHub page.

Looking at our previous tutorial we know that splines are not terribly hard to work with but there is a bit of magic to having something move along a spline.

The key ingredient in this is the FTimeline. This allows us to keep track of an execution of values across a specific timeframe.

The FTimeline combined with a UCurveFloat gives us all that flexibility. So let’s look at the code to see how this all breaks down.

FTimeline MovementTimeline;

FOnTimelineFloat ProgressFunction; ProgressFunction.BindUFunction(this, TEXT("ProcessMovementTimeline")); MovementTimeline.AddInterpFloat(MovementCurve, ProgressFunction); FOnTimelineEvent OnTimelineFinishedFunction; OnTimelineFinishedFunction.BindUFunction(this, TEXT("OnEndMovementTimeline")); MovementTimeline.SetTimelineFinishedFunc(OnTimelineFinishedFunction); MovementTimeline.SetTimelineLengthMode(TL_LastKeyFrame); if(bAutoActivate) { MovementTimeline.PlayFromStart(); }

Looking over the code we can see that as part of the FTimeline definition we are specifying a function that will be executed during the runtime of the timeline.

UFUNCTION()
void ProcessMovementTimeline(float Value);

As well as a function at will be executed at the end of our timeline.

UFUNCTION()
void OnEndMovementTimeline();

Additionally we need to ensure that our timeline ticks as part of our object. For this we can examine the Tick method.

// Called every frame
void AMovingSplineActor::Tick(float DeltaTime)
{
  Super::Tick(DeltaTime);

  if(MovementTimeline.IsPlaying())
  {
    MovementTimeline.TickTimeline(DeltaTime);	
  }
}

Now let’s take a look at the magic around moving the mesh across the spline. This all happens within our ProcessMovementTimeline method.

void AMovingSplineActor::ProcessMovementTimeline(float Value)
{
  const float SplineLength = SplineComponent->GetSplineLength();

  const FVector CurrentSplineLocation = SplineComponent->GetLocationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);
  FRotator CurrentSplineRotation = SplineComponent->GetRotationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);

  CurrentSplineRotation.Pitch = 0.f;
  MeshComponent->SetWorldLocationAndRotation(CurrentSplineLocation, CurrentSplineRotation);
}

As you can see the logic is not that scary. We are simply taking the delta time by the lenght of the spline and retrieving that points location and rotation.

This is then applied to our mesh and we prevent the pitch from being corrected so our elevator / platform does not tip as it’s traversing along the spline.

Lastly let’s take a look at the function OnEndMovementTimeline() which is called at the .. you guessed it … end of our timeline.

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spline", meta=(EditCondition = "!bRestartOnEndTimeline"))
bool bReverseOnEndTimeline;

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spline", meta=(EditCondition = "!bReverseOnEndTimeline"))
bool bRestartOnEndTimeline;
void AMovingSplineActor::OnEndMovementTimeline()
{
  if(bReverseOnEndTimeline)
  {
    MovementTimeline.Reverse();
  }
  else if(bRestartOnEndTimeline)
  {
    MovementTimeline.PlayFromStart();
  }
}

This function is a great spot to put in logic for things like restarting the movement or reversing the movement by injecting some simple properties into our new object.

For further reading about these topics take a look at the following links:

Unreal Engine C++ Fundamentals – Using Spline Components

By | Development, Tutorial, Unreal | No Comments

Hey guys,

Today we are going to take a look how to use Spline Components and modify their various properties to allow us to specify custom materials as well as determine how meshes are attached to the spline.

The project files for this video & article can be found on our GitHub page.

Splines splines splines !

So why do we care about splines ? Well splines allow us to construct various objects within our world by providing us with easily extensible and modifiable joined sets of meshes.

Splines can also provide support for navigation by being used as a path for something or someone to move along. In our case though we are going to use it for prop building.

Let’s dive into how to initialize a Spline Component.

#include "Components/SplineComponent.h"

UPROPERTY(VisibleAnywhere, Category = "Spline")
USplineComponent* SplineComponent;

Inside our constructor we simply instantiate it.

// Sets default values
ASplineActor::ASplineActor()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;

  SplineComponent = CreateDefaultSubobject<USplineComponent>("Spline");
  if(SplineComponent)
  {
    SetRootComponent(SplineComponent);	
  }	
}

With our spline component setup we can proceed with attaching Spline Mesh Components by iterating over the available spline points.

To do this we want to override the OnConstruction method in order to allow us to specify the Spline Mesh Components during the process of modifying our spline within the editor.

void ASplineActor::OnConstruction(const FTransform& Transform)
{	
  Super::OnConstruction(Transform);

  if(SplineComponent && SplineMeshMap.Num() > 0)
  {
    // lookup all pertinent values
    FSplineMeshDetails* StartMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::START))
    {
      StartMeshDetails = SplineMeshMap.Find(ESplineMeshType::START);	
    }

    FSplineMeshDetails* EndMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::END))
    {
      EndMeshDetails = SplineMeshMap.Find(ESplineMeshType::END);
    }

    FSplineMeshDetails* DefaultMeshDetails = nullptr;
    if(SplineMeshMap.Contains(ESplineMeshType::DEFAULT))
    {
      DefaultMeshDetails = SplineMeshMap.Find(ESplineMeshType::DEFAULT);	
    }
    else
    {
      // exit if we don't have a default mesh to work with
      return;
    }
    
    const int32 SplinePoints = SplineComponent->GetNumberOfSplinePoints();

    for(int SplineCount = 0; SplineCount < (SplinePoints - 1); SplineCount++)
    {
      USplineMeshComponent *SplineMesh = NewObject<USplineMeshComponent>(this, USplineMeshComponent::StaticClass());

      UStaticMesh* StaticMesh = DefaultMeshDetails->Mesh;
      UMaterialInterface* Material = nullptr;
      ESplineMeshAxis::Type ForwardAxis = DefaultMeshDetails->ForwardAxis;
  
      // start mesh
      if(StartMeshDetails && StartMeshDetails->Mesh && SplineCount == 0)
      {
        StaticMesh = StartMeshDetails->Mesh;
        ForwardAxis = StartMeshDetails->ForwardAxis;
        
        if(StartMeshDetails->DefaultMaterial)
        {
          Material = StartMeshDetails->DefaultMaterial;
        }					
      }
      else if(EndMeshDetails && EndMeshDetails->Mesh && SplinePoints > 2 && SplineCount == (SplinePoints - 2))
      {
        // end mesh
        StaticMesh = EndMeshDetails->Mesh;
        ForwardAxis = EndMeshDetails->ForwardAxis;
        
        if(EndMeshDetails->DefaultMaterial)
        {
          Material = EndMeshDetails->DefaultMaterial;
        }	
      }
      else
      {
        // default assignment - middle mesh
        if(DefaultMeshDetails->AlternativeMaterial && SplineCount > 0 && SplineCount % 2 == 0)
        {
          Material = DefaultMeshDetails->AlternativeMaterial;
        }
        else if(DefaultMeshDetails->DefaultMaterial)
        {
          Material = DefaultMeshDetails->DefaultMaterial;
        }			
      }

      // update mesh details
      SplineMesh->SetStaticMesh(StaticMesh);
      SplineMesh->SetForwardAxis(ForwardAxis, true);
      SplineMesh->SetMaterial(0, Material);
      
  
      // initialize the object
      SplineMesh->RegisterComponentWithWorld(GetWorld());
  
      SplineMesh->CreationMethod = EComponentCreationMethod::UserConstructionScript;
      SplineMesh->SetMobility(EComponentMobility::Movable);
  
      SplineMesh->AttachToComponent(SplineComponent, FAttachmentTransformRules::KeepRelativeTransform);
  
      // define the positions of the points and tangents
      const FVector StartPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
      const FVector StartTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
      const FVector EndPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
      const FVector EndTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
      SplineMesh->SetStartAndEnd(StartPoint, StartTangent, EndPoint, EndTangent, true);
  
      // query physics
      SplineMesh->SetCollisionEnabled(ECollisionEnabled::Type::QueryAndPhysics);
    }
  }
}

WHOA that’s a lot of stuff , slow down !

That was a whole pile of code so let’s break it up bit by bit.

void ASplineActor::OnConstruction(const FTransform& Transform)
{	
  Super::OnConstruction(Transform);
}

Here we end up overwriting the OnConstruction method, ensuring we call the parent’s OnConstruction method before we proceed with any of our code. This method is going to be called when our blueprint is being modified in the editor or manipulated in the level.

To provide greater flexibility in our spline actor we introduce a struct and an enum to let us handle multiple different types of meshes / materials and forward axis definitions for our spline mesh components.

UENUM(BlueprintType)
enum class ESplineMeshType: uint8 {
  DEFAULT		UMETA(DisplayName = "Default Mesh"),
  START		UMETA(DisplayName = "Starting Mesh"),
  END			UMETA(DisplayName = "EndingMesh"),
};

USTRUCT(BlueprintType)
struct FSplineMeshDetails : public FTableRowBase
{
  GENERATED_BODY()
  
  UPROPERTY(EditAnywhere, BlueprintReadOnly)
  UStaticMesh* Mesh;

  UPROPERTY(EditAnywhere, BlueprintReadOnly)
  TEnumAsByte<ESplineMeshAxis::Type> ForwardAxis;
  
  UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
  class UMaterialInterface* DefaultMaterial;

  UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
  class UMaterialInterface* AlternativeMaterial;

  FSplineMeshDetails() : ForwardAxis(ESplineMeshAxis::Type::X)
  {
  }
};

We convert this enum / struct combination into a map that will allow us to configure multiple versions of these definitions.

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Spline")
TMap<ESplineMeshType, FSplineMeshDetails> SplineMeshMap;

From which we can load the appropriate details and make any necessary checks to determine if our map and the contents are valid.

if(SplineComponent && SplineMeshMap.Num() > 0)
{
  // lookup all pertinent values
  FSplineMeshDetails* StartMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::START))
  {
    StartMeshDetails = SplineMeshMap.Find(ESplineMeshType::START);	
  }

  FSplineMeshDetails* EndMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::END))
  {
    EndMeshDetails = SplineMeshMap.Find(ESplineMeshType::END);
  }

  FSplineMeshDetails* DefaultMeshDetails = nullptr;
  if(SplineMeshMap.Contains(ESplineMeshType::DEFAULT))
  {
    DefaultMeshDetails = SplineMeshMap.Find(ESplineMeshType::DEFAULT);	
  }
  else
  {
    // exit if we don't have a default mesh to work with
    return;
  }
}

The spline is made up of spline points that which allow for transformation of the spline component using the level editor but it also provides you with information that we require for our construction script as each spline point can change.

We need to capture these changes by iterating over the spline points and then update your spline component accordingly.

const int32 SplinePoints = SplineComponent->GetNumberOfSplinePoints();

for(int SplineCount = 0; SplineCount < (SplinePoints - 1); SplineCount++)
{
}

In order to render any meshes along our spline we need to create spline mesh components during our OnConstruction call.

USplineMeshComponent *SplineMesh = NewObject<USplineMeshComponent>(this, USplineMeshComponent::StaticClass());

With the object instantiated we can determine which of the meshes we want to assign.

Do we want a mesh at the start of our spline ? Do we want one at the end ? Do we want to apply some in the middle ? This type of logic fits extremely well with assets like pipes, fences, ladders and various other common props that need to be adjusted to fit their environment as well as provide quick work flow.

Here we set some environments to hold our mesh details. We use a UMaterialInterface in order to provide flexibility of using master or instance materials.

UStaticMesh* StaticMesh = DefaultMeshDetails->Mesh;
UMaterialInterface* Material = nullptr;
ESplineMeshAxis::Type ForwardAxis = DefaultMeshDetails->ForwardAxis;

In order for us to determine which part of our spline we are working on we can simply keep count of the spline points and react accordingly.

// start mesh
if(StartMeshDetails && StartMeshDetails->Mesh && SplineCount == 0)
{
  StaticMesh = StartMeshDetails->Mesh;
  ForwardAxis = StartMeshDetails->ForwardAxis;
  
  if(StartMeshDetails->DefaultMaterial)
  {
    Material = StartMeshDetails->DefaultMaterial;
  }					
}
else if(EndMeshDetails && EndMeshDetails->Mesh && SplinePoints > 2 && SplineCount == (SplinePoints - 2))
{
  // end mesh
  StaticMesh = EndMeshDetails->Mesh;
  ForwardAxis = EndMeshDetails->ForwardAxis;
  
  if(EndMeshDetails->DefaultMaterial)
  {
    Material = EndMeshDetails->DefaultMaterial;
  }	
}
else
{
  // default assignment - middle
  if(DefaultMeshDetails->AlternativeMaterial && SplineCount > 0 && SplineCount % 2 == 0)
  {
    Material = DefaultMeshDetails->AlternativeMaterial;
  }
  else if(DefaultMeshDetails->DefaultMaterial)
  {
    Material = DefaultMeshDetails->DefaultMaterial;
  }			
}

With our assignment determined we register which mesh / material and forward axis we are going to be assigning to our spline mesh component.

// update mesh details
SplineMesh->SetStaticMesh(StaticMesh);
SplineMesh->SetForwardAxis(ForwardAxis, true);
SplineMesh->SetMaterial(0, Material);

Before the spline component is available to our editor and blueprint we need to register it with the world it’s going to be present in.

// initialize the object
SplineMesh->RegisterComponentWithWorld(GetWorld());

We also tell this component that it needs to be processed at construction time.

SplineMesh->CreationMethod = EComponentCreationMethod::UserConstructionScript;

We let the component know it can be moved and adjusted. This can also be static for specific needs.

SplineMesh->SetMobility(EComponentMobility::Movable);

Then finally we attach the spline mesh component to our spline component by keeping it’s transformation relative to the spline component.

SplineMesh->AttachToComponent(SplineComponent, FAttachmentTransformRules::KeepRelativeTransform);

Lastly we need to define the starting / ending points as well as the tangents that control the curvature of the spline points along said spline.

This allows the spline to determine the position of our new mesh along that spline so everything appears one after the other as we extend our spline points.

// define the positions of the points and tangents
const FVector StartPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
const FVector StartTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount, ESplineCoordinateSpace::Type::Local);
const FVector EndPoint = SplineComponent->GetLocationAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
const FVector EndTangent = SplineComponent->GetTangentAtSplinePoint(SplineCount + 1, ESplineCoordinateSpace::Type::Local);
SplineMesh->SetStartAndEnd(StartPoint, StartTangent, EndPoint, EndTangent, true);

Lastly we specify the collision response for each mesh component. This allows us to specify and register different overlap / hit events for each part of the spline providing a lot of flexbility.

SplineMesh->SetCollisionEnabled(ECollisionEnabled::Type::QueryAndPhysics);

See not too bad at all and at the end of this little implementation you should have a fairly modular spline actor for various uses.

For further reading about these topics take a look at the following links:

Unreal Engine C++ Fundamentals – Character possession and changing materials

By | Development, Tutorial, Unreal | No Comments

Hey guys,

We are back after a short summer break with more tutorials.

Today we go back to our C++ Fundamentals lessons by looking at possession and how we can control different charactesr in our world. In addition to that we are going to setup a few materials so we can change our characters appearance to showcase the possession a bit better.

As usual you can find the start project on our GitHub page.

Possession ? That sounds scary

Don’t worry it’s not halloween yet, and possession is just a fancy term for taking complete control over another character or actor within your game world.

This allows our player to be able to experience different character behaviors, it allows for playing different stories like in GTA 5 or you can use it for simple mount mechanics. Tons of possibilities.

So let’s take a look at how we possess our characters

// save a copy of our controller
AController* SavedController = GetController();
// unpossess first ( helps with multiplayer )
SavedController->UnPossess();
// disable movement mode
GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
// possess our new actor
SavedController->Possess(Cast<APawn>(ActorToPossess));

Seems pretty straight forward but let’s see what is going on here.

First we end up saving a copy of our current player controller so we can reference it later. This can also be used to save the “AI” brain of a character in the world and put it back when we unpossess said character.

We then go ahead and unpossess the current controller to ensure it’s cleaned up.

We then go ahead and set the movement mode on our player to none so he doesn’t move when we leave his body.

And lastly we possess the actor of our choosing.

So what about these materials ? Are they also haunted ?

As far as we know, they are not, but we only know so much.

Materials are pretty easy to setup and change, you just need to be aware where on the character in question they reside. Let’s take a look at this example

header

UPROPERTY(EditDefaultsOnly, Category = Possession)
class UMaterialInterface* DefaultMaterialBody;
  
UPROPERTY(EditDefaultsOnly, Category = Possession)
class UMaterialInterface* DefaultMaterialChest;

cpp

if(DefaultMaterialBody)
{
  GetMesh()->SetMaterial(0, DefaultMaterialBody);	
}
if(DefaultMaterialChest)
{
  GetMesh()->SetMaterial(1, DefaultMaterialChest);	
}

As you can see we do really just a few things to switch over the materials.

We first define the properties on our blueprint so we can assign material instances to our player.

Then based on our logic we can set the material to the newly defined variables.

The only little gotcha is to ensure you are referencing the right material index on your mesh. In the case of our Unreal mannequin he has two materials, one for the body and the other for the chest piece. They are respectively assigned to index 0 and 1 on the mesh. So just ensure you are setting your material on the correct section of your model.

It still feels like a need an old priest and a young priest

Nope that’s really it friends, unpossess current controller and possess new target. Rinse and repeat.

Here is a much more involved example lifted from our sample project combining both material switching as well as possession

void AUE4Fundamentals11Character::Interact()
{
  FVector Start;
  FVector End;

  FVector PlayerEyesLoc;
  FRotator PlayerEyesRot;

  GetActorEyesViewPoint(PlayerEyesLoc, PlayerEyesRot);

  Start = PlayerEyesLoc;
  End = PlayerEyesLoc + (PlayerEyesRot.Vector() * LineTraceDistance);

  FCollisionQueryParams TraceParams(FName(TEXT("InteractTrace")), true, this);

  FHitResult InteractHit = FHitResult(ForceInit);

  bool bIsHit = GetWorld()->LineTraceSingleByChannel(InteractHit, Start, End, ECC_GameTraceChannel3, TraceParams);

  if(bIsHit && InteractHit.GetActor() != this)
  {
    // Log(ELogLevel::WARNING, InteractHit.Actor->GetName());
    // start to end, green, will lines always stay on, depth priority, thickness of line
    DrawDebugLine(GetWorld(), Start, End, FColor::Green, false, 5.f, ECC_WorldStatic, 1.f);

    // implements interface
    if(InteractHit.GetActor()->GetClass()->ImplementsInterface(UInteractiveActor::StaticClass()))
    {
      IInteractiveActor::Execute_Interact(InteractHit.GetActor());
    }
    else if(InteractHit.GetActor()->IsA(ACharacter::StaticClass()))
    {
      // check to see if we are a possessed entity
      if(bIsCurrentlyPossessed)
      {
        bIsCurrentlyPossessed = false;

        if(DefaultMaterialBody)
        {
          GetMesh()->SetMaterial(0, DefaultMaterialBody);	
        }
        if(DefaultMaterialChest)
        {
          GetMesh()->SetMaterial(1, DefaultMaterialChest);	
        }
      }

      AUE4Fundamentals11Character* PossessableCharacter = Cast<AUE4Fundamentals11Character>(InteractHit.GetActor());
      if(PossessableCharacter)
      {
        if(!PossessableCharacter->bIsCurrentlyPossessed)
        {
          // handle possession
          if(!SavedController)
          {
            // save the controller
            SavedController = GetController();	
          }

          // unpossess first ( helps with multiplayer )
          SavedController->UnPossess();

          // disable current player state management
          bIsKeyboardEnabled = false;					
          bIsRunning = false;
          bIsArmed = false;
          // disable movement mode
          GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
          
          // possess our new actor
          SavedController->Possess(Cast<APawn>(InteractHit.GetActor()));
          // enable movement back on the possessed actor
          PossessableCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
          
          // set the values for our possession materials
          if(PossessableCharacter->PossessMaterialBody)
          {
            PossessableCharacter->GetMesh()->SetMaterial(0, PossessableCharacter->PossessMaterialBody);	
          }

          if(PossessableCharacter->PossessMaterialChest)
          {
            PossessableCharacter->GetMesh()->SetMaterial(1, PossessableCharacter->PossessMaterialChest);	
          }	

          // ensure the new player is correctly marked as possesed and can be interacted with
          PossessableCharacter->bIsKeyboardEnabled = true;
          PossessableCharacter->bIsCurrentlyPossessed = true;
        }
      }
    }
  }
  else
  {
    DrawDebugLine(GetWorld(), Start, End, FColor::Purple, false, 5.f, ECC_WorldStatic, 1.f);
  }
}

As you can see I am doing a few moves to update the material on the target mesh we want to possess and reset the materials on the mesh we are currently occupying.

Additionally there is a bit of logic to update the state of a few variables that control our animation presentation. For example I am turning off the armed animation logic.

But otherwise it’s just a call to Possess / Unpossess as well as SetMaterial

That’s it folks, hope you found this helpful and here are a few links for some light reading: