Based off comments here: No need for a collision volume
So I created the C++ versions of Tile and InfiniteGameMode and used TCircularQueue for what’s talked about in this thread. I also decided to give the Barrier a dynamic material so that it would blend between the two
Tile.h
#pragma once
#include "GameFramework/Actor.h"
#include "Tile.generated.h"
//To bind in GameMode
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPlayerEnter);
UCLASS()
class TESTINGGROUNDS_API ATile : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ATile();
FTransform GetAttachLocation() const;
FPlayerEnter PlayerEntered;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere)
UBoxComponent* BoundsVolume;
UPROPERTY(EditAnywhere)
UArrowComponent* AttachPoint;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Barrier")
UStaticMeshComponent* Barrier;
UPROPERTY(EditAnywhere)
UMaterialInterface* BarrierMaterial;
UMaterialInstanceDynamic* DynamicMaterial;
UFUNCTION(BlueprintImplementableEvent, Category = "Tile")
void OnEnter();
UFUNCTION(BlueprintImplementableEvent, Category = "Tile")
void OnExit();
UFUNCTION(BlueprintCallable, Category = "Tile")
void UpdateBarrierMaterial(float Blend, float Opacity);
UFUNCTION()
void OnBoundsBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);
UFUNCTION()
void OnBoundsEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);
private:
USceneComponent* SharedRoot;
};
Tile.cpp
#include "TestingGrounds.h"
#include "Tile.h"
#include "Components/ArrowComponent.h"
// Sets default values
ATile::ATile()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
SharedRoot = CreateDefaultSubobject<USceneComponent>(TEXT("Root Component"));
BoundsVolume = CreateDefaultSubobject<UBoxComponent>(TEXT("Bounds Volume"));
AttachPoint = CreateDefaultSubobject<UArrowComponent>(TEXT("Arrow Component"));
RootComponent = SharedRoot;
BoundsVolume->SetupAttachment(RootComponent);
AttachPoint->SetupAttachment(RootComponent);
BoundsVolume->SetCollisionProfileName(TEXT("Trigger"));
PlayerEntered.AddDynamic(this, &ATile::OnEnter);
}
// Called when the game starts or when spawned
void ATile::BeginPlay()
{
check(Barrier)
check(BarrierMaterial)
Super::BeginPlay();
DynamicMaterial = Barrier->CreateAndSetMaterialInstanceDynamicFromMaterial(0, BarrierMaterial);
BoundsVolume->OnComponentBeginOverlap.AddDynamic(this, &ATile::OnBoundsBeginOverlap);
BoundsVolume->OnComponentEndOverlap.AddDynamic(this, &ATile::OnBoundsEndOverlap);
}
FTransform ATile::GetAttachLocation() const
{
return AttachPoint->ComponentToWorld;
}
void ATile::OnBoundsBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
AActor* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
if (OtherActor == Player)
{
Barrier->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PlayerEntered.Broadcast();
}
}
void ATile::OnBoundsEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
AActor* Player = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);
if (OtherActor == Player)
{
Barrier->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
OnExit();
}
}
void ATile::UpdateBarrierMaterial(float Blend, float Opacity)
{
DynamicMaterial->SetScalarParameterValue(TEXT("Blend"), FMath::Clamp(Blend, 0.f, 1.f));
DynamicMaterial->SetScalarParameterValue(TEXT("Opacity"), FMath::Clamp(Opacity, .5f, 1.f));
}
InfiniteGameMode.h (Didn’t actually create a new class just used the existing one)
#pragma once
#include "GameFramework/GameMode.h"
#include "TestingGroundsGameMode.generated.h"
#define DEFAULT_MAX 3
UCLASS(minimalapi)
class ATestingGroundsGameMode : public AGameMode
{
GENERATED_BODY()
public:
ATestingGroundsGameMode();
protected:
virtual void BeginPlay() override;
UFUNCTION()
virtual void SpawnTile();
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = "3", UIMin = "3")) //Clamp default value to min 3.
uint8 MaxTiles = DEFAULT_MAX;
private:
FTransform AttachLocation;
TCircularQueue<class ATile*> Tiles{ DEFAULT_MAX };
UPROPERTY(EditDefaultsOnly)
TSubclassOf<class ATile> TileClass;
};
InfiniteGameMode.cpp
#include "TestingGrounds.h"
#include "TestingGroundsGameMode.h"
#include "TestingGroundsHUD.h"
#include "FirstPersonCharacter.h"
#include "Tile.h"
ATestingGroundsGameMode::ATestingGroundsGameMode()
: Super()
{
FVector Location{ -2000.0, 0, 120 };
AttachLocation = FTransform(FRotator(0),Location);
// set default pawn class to our Blueprinted character
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnClassFinder(TEXT("/Game/Dynamic/Character/BP_TPCharacter"));
DefaultPawnClass = PlayerPawnClassFinder.Class;
// use our custom HUD class
HUDClass = ATestingGroundsHUD::StaticClass();
}
void ATestingGroundsGameMode::BeginPlay()
{
check(TileClass)
//If MaxTiles overidden in BP
if (MaxTiles != DEFAULT_MAX)
{
Tiles = TCircularQueue<ATile*>(MaxTiles);
}
SpawnTile();
SpawnTile();
//Only call BP BeginPlay after initial setup
Super::BeginPlay();
}
void ATestingGroundsGameMode::SpawnTile()
{
if (GetWorld())
{
ATile* NewTile = GetWorld()->SpawnActor<ATile>(TileClass, AttachLocation);
if (NewTile)
{
//Dequeue if at MaxTiles
if (Tiles.Count() >= MaxTiles)
{
ATile* OldTile;
if (Tiles.Dequeue(OldTile))
{
FTimerHandle Handle;
FTimerDelegate RemoveTileDelegate;
//Delay Destruction of Tile for material to transition
RemoveTileDelegate.BindLambda([OldTile] { OldTile->Destroy(); });
GetWorld()->GetTimerManager().SetTimer(Handle, RemoveTileDelegate, 0.3f, false);
}
}
//Bind NewTile's BoundsVolume OnOverlap with SpawnTile
NewTile->PlayerEntered.AddDynamic(this, &ATestingGroundsGameMode::SpawnTile);
//Get new AttachLocation then Queue new Tile
AttachLocation = NewTile->GetAttachLocation();
Tiles.Enqueue(NewTile);
}
}
}
And the BP event graph for the tile because Timelines in C++ require referencing a CurveFloat asset so I figured I may as well just create it in BP.
And the material and making sure “Use Material Atrributes” is checked in the details pannel
@sampattuzzi feel free to use this as a reference… And that screenshot reminded me I need to name my timelines…
A different way to implement the GrassComponent
I went a different route to implementating this with only having the SpawnCount exposed to the editor.
In order to achieve this I created the Floor staticmesh and the GrassComponents as default subobjects in the Tile C++ class
//////////ATile.h///////////
UPROPERTY(VisibleDefaultsOnly)
UStaticMeshComponent* Floor;
UPROPERTY(VisibleDefaultsOnly)
class UGrassComponent* Grass01;
UPROPERTY(VisibleDefaultsOnly)
class UGrassComponent* Grass02;
UPROPERTY(VisibleDefaultsOnly)
class UGrassComponent* Grass03;
////////////////////////////
//////////ATile.cpp/////////
//In the constructor
Floor = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Floor"));
Grass01 = CreateDefaultSubobject<UGrassComponent>(TEXT("Grass01"));
Grass02 = CreateDefaultSubobject<UGrassComponent>(TEXT("Grass02"));
Grass03 = CreateDefaultSubobject<UGrassComponent>(TEXT("Grass03"));
//Block Ground traces (since it _is_ the ground)
Floor->SetCollisionResponseToChannel(ECC_Ground, ECR_Block);
//Ignore Spawn traces
Floor->SetCollisionResponseToChannel(ECC_Spawn, ECR_Ignore);
Floor->SetupAttachment(RootComponent);
Grass01->SetupAttachment(Floor);
Grass02->SetupAttachment(Floor);
Grass03->SetupAttachment(Floor);
/////In BeginPlay/////
FVector Min;
FVector Max;
//Get the bounds of the FloorMesh
Floor->GetLocalBounds(Min, Max);
//Raise the Min Z value to the Max so the box is effectively a plane
Min.Z = Max.Z;
ActorToWorld().TransformPosition(Min);
ActorToWorld().TransformPosition(Max);
FBox Bounds(Min, Max);
Grass01->SpawnGrass(Bounds);
Grass02->SpawnGrass(Bounds);
Grass03->SpawnGrass(Bounds);
Also I added a default constructor for the GrassComponent because I thought Grass suited No Collision better.
UGrassComponent::UGrassComponent()
{
SetCollisionProfileName(TEXT("NoCollision"));
}