My Testing Grounds work

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"));
}
3 Likes

Wow, nice work! I like the binding of the lambda delegate.

@DanM I’m trying to implement this myself but I’m getting caught up on a compile error I’m not too familiar with. I’m not sure if this is due to being on the latest version of UE4 or what.

Any input on what this means or how to fix it would be much appreciated.

CompilerResultsLog: Building 3 actions with 16 processes...
CompilerResultsLog:   [1/3] TestingGroundsGameMode.cpp
CompilerResultsLog:    
C:\Users\Documents\05_TestingGrounds\TestingGrounds\Source\TestingGrounds\TestingGroundsGameMode.cpp(32) : error C2280: 'TCircularQueue<ATile *> &TCircularQueue<ATile *>::operator =(const TCircularQueue<ATile *> &)': attempting to reference a deleted function
CompilerResultsLog:   C:\Program Files\Epic Games\UE_4.21\Engine\Source\Runtime\Core\Public\Containers\CircularQueue.h(198): note: compiler has generated 'TCircularQueue<ATile *>::operator =' here
CompilerResultsLog:   C:\Program Files\Epic Games\UE_4.21\Engine\Source\Runtime\Core\Public\Containers\CircularQueue.h(198): note: 'TCircularQueue<ATile *> &TCircularQueue<ATile *>::operator =(const TCircularQueue<ATile *> &)': function was implicitly deleted because a data member invokes a deleted or inaccessible function 'TAtomic<uint32> &TAtomic<uint32>::
operator =(const TAtomic<uint32> &)'

TestingGroundsGameMode.h:

TCircularQueue<class ATile*> Tiles{ DEFAULT_MAX };

TestingGroundsGameMode.cpp:

Tiles = TCircularQueue<ATile*>(MaxTiles);

I also get a Red Squiggly under the “=” sign in the above Tiles = TCircularQueue<ATile*>(MaxTiles); statement. When hovering I see:

TCircularQueue<ATile *> &TCircularQueue<ATile *>::operator=(const TCircularQueue<ATile *> &)

funtion "TCircularQueue<ElementType>::operator=(const TCircularQueue<ATile *> &) [with ElementType=ATile *]" (declared implicitly) cannot be referenced -- it is a deleted function

Once again, any insight at all here would be very helpful to me!
@ben @sampattuzzi

Because it’s no longer valid. You have to specify the size and can’t assign a new one, so you won’t be able to specify it’s size in blueprint anymore.

Gotcha, that helps me understand what’s going on.

@DanM would you mind pulling this topic out into a separate question over in the #unreal:ask section?

I think it’s better this way?

1 Like

Privacy & Terms