It’s been a long time since I’ve done game development. I’m going to try out Unreal Engine.
I have a firm belief that my game should have a table. I’m fond of voxel art, so I will use MagicaVoxel to make my table.
Now that I have a beautiful table, I can import it into Unreal Engine and drag it into a scene.
Well that’s not quite right
Unreal Engine and MagicaVoxel use different coordinate systems and units. For now, I’ll just rotate the model by 90 degrees and scale up by 10.
Much better!
I’m very happy with my table. It’s time to start building my game level!
I want a lot of tables.
This is getting tedious…
Placing tables manually is slow and prone to error. I should be able to create tables through code. I think I want a SpawnableObject class that contains information about its model.
// SpawnableObject.h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SpawnableObject.generated.h"
UCLASS()
class DETSIM_API ASpawnableObject : public AActor
{
GENERATED_BODY()
public:
ASpawnableObject();
public:
void SetMesh(const TCHAR* MeshPath);
void SetScale(FVector scale);
UStaticMeshComponent* MeshComponent;
};
//SpawnableObject.cpp
#include "SpawnableObject.h"
// Sets default values
ASpawnableObject::ASpawnableObject()
{
PrimaryActorTick.bCanEverTick = false;
SpawnCollisionHandlingMethod = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("RootComponent"));
this->SetRootComponent(MeshComponent);
MeshComponent->SetRelativeLocation(FVector::ZeroVector);
MeshComponent->SetWorldScale3D(FVector(1.0f));
MeshComponent->SetMobility(EComponentMobility::Stationary);
}
void ASpawnableObject::SetMesh(const TCHAR* MeshPath) {
auto ObjectMesh = LoadObject<UStaticMesh>(this, MeshPath);
MeshComponent->SetStaticMesh(ObjectMesh);
}
void ASpawnableObject::SetScale(FVector scale) {
MeshComponent->SetWorldScale3D(scale);
}
And now to create my new SpawnableObject
FActorSpawnParameters spawnParams;
spawnParams.Owner = this;
spawnParams.Instigator = GetInstigator();
spawnParams.ObjectFlags |= RF_Transient;
spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
auto position = FVector(0,0,0);
auto rotation = FRotator(0.0, 0.0, 90.0);
auto tableActor = Cast<ASpawnableObject>(GetWorld()->SpawnActor(ASpawnableObject::StaticClass(), &position, &rotation, spawnParams));
tableActor->SetMesh(TEXT("/Game/Models/table.table"));
tableActor->SetScale(FVector(10.0));
I promise I’m not reusing screenshots.
Isn’t she lovely? Now I can spawn as many tables as I want, wherever I want. But a world filled with only tables is rather boring. I want to put stuff on those tables.
I want to use XML files to introduce dynamic objects to my code. First, I need a class to load object info from XML.
//SpawnableObjectInfo.h
class SpawnableObjectInfo
{
public:
SpawnableObjectInfo(FString XmlPath);
~SpawnableObjectInfo();
FVector Scale;
FRotator Rotation;
FString ModelSrc;
TArray<FVector> SpawnPoints;
};
//SpawnableObjectInfo.cpp
SpawnableObjectInfo::SpawnableObjectInfo(FString XmlPath)
{
FString xmlPath = FPaths::ProjectContentDir() + XmlPath;
FString xmlData;
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
FFileHelper::LoadFileToString(xmlData, &PlatformFile, *xmlPath, FFileHelper::EHashOptions::None, 0);
FXmlFile* xml = new FXmlFile(xmlData, EConstructMethod::ConstructFromBuffer);
FXmlNode* RootNode = xml->GetRootNode();
auto src = RootNode->GetAttribute(TEXT("src"));
ModelSrc = src;
auto options = RootNode->FindChildNode(TEXT("options"));
double voxelScale = FCString::Atod(*options->FindChildNode(TEXT("voxelScale"))->GetContent());
Scale = FVector(voxelScale);
auto rotationOpts = options->FindChildNode(TEXT("rotation"));
double rotX = FCString::Atod(*rotationOpts->GetAttribute(TEXT("x")));
double rotY = FCString::Atod(*rotationOpts->GetAttribute(TEXT("y")));
double rotZ = FCString::Atod(*rotationOpts->GetAttribute(TEXT("z")));
Rotation = FRotator(rotY, rotZ, rotX);
auto children = RootNode->GetChildrenNodes();
for (auto child : children) {
if (child->GetTag() == "spawnPoint") {
auto attr = child->GetAttributes();
double spawnX = FCString::Atod(*child->GetAttribute(TEXT("x")));
double spawnY = FCString::Atod(*child->GetAttribute(TEXT("y")));
double spawnZ = FCString::Atod(*child->GetAttribute(TEXT("z")));
SpawnPoints.Add(FVector(spawnX, spawnY, spawnZ));
}
}
}
With this class, I can load info about a model and then spawn it with the correct parameters. Then I can tell my code where to place other objects, such as a notebook, by defining spawn points.
<model id="table" src="/Game/Models/table.table">
<options>
<voxelScale>10</voxelScale>
<rotation x="90" y="0" z="0"/>
</options>
<spawnPoint x="3" y="3" z="22"/>
<spawnPoint x="16" y="16" z="22"/>
</model>
auto tableInfo = new SpawnableObjectInfo("/Definitions/table.xml");
auto notebookInfo = new SpawnableObjectInfo("/Definitions/notebook.xml");
FActorSpawnParameters spawnParams;
spawnParams.Owner = this;
spawnParams.Instigator = GetInstigator();
spawnParams.ObjectFlags |= RF_Transient;
spawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
auto position = FVector(0,0,0);
auto tableActor = Cast<ASpawnableObject>(GetWorld()->SpawnActor(ASpawnableObject::StaticClass(), &position, &tableInfo->Rotation, spawnParams));
tableActor->SetMesh(*tableInfo->ModelSrc);
tableActor->SetScale(tableInfo->Scale);
for (FVector spawnPoint : tableInfo->SpawnPoints) {
auto parentOrigin = tablePosition;
auto spawnPosition = parentOrigin + spawnPoint;
auto spawnActor = Cast<ASpawnableObject>(GetWorld()->SpawnActor(ASpawnableObject::StaticClass(), &spawnPosition, ¬ebookInfo->Rotation, spawnParams));
spawnActor->SetMesh(*nbInfo->ModelSrc);
spawnActor->SetScale(nbInfo->Scale);
}
I love my notebooks.
I’m quite happy with my new outdoor office. Next time I’ll try filling it with random objects and maybe add some new furniture.