Made for: ARU 2nd Year – Semester 1
Duration: 3 Months
Engine: Unreal Engine 4.19
Language: C++/Blueprint
Position: Solo
Brief (Excerpts):
“Students must develop a games artefact utilizing the Unreal engine.
The game needs to be:
- First person character
- Character/player must hold a gun of some description
- Students may use Blueprints or C++ inside the engine as ways to develop the game.
- Game must consist of one level
- Built upon the first person Blueprint/C++ template that comes with Unreal Engine 4
- The player character must remain within the “arena” boundary walls of the first person template
- The mechanics themselves can vary but be sure to discuss any ideas with your lecturers to ensure the project is contained.”
“Further to this, students are asked to use the Maya 3D modelling software to create a detailed gun/ranged weapon which will then be implemented into their artifact.”
“Finally students are asked to document their development process using Blogger.com.”
While the brief was quite restrictive, I saw an opportunity to try an idea I had a while ago. The crux of it is being able to draw shapes on surfaces and pull them out or “extrude” them. The final result can be seen below:

Using this concept I created a first person puzzle game. A blog of the development progress, as requested in the brief, can be found here:
http://getsystemsmodelling1626470.blogspot.com
Drawing shapes
The player is able to trace a path on select surfaces, creating a shape once they are finished. The drawn path consists of spline mesh components following points created as the cursor travels across the surface.
The core of the game relies on this shape creation. It hinges on Unreals procedural mesh generation system by turning a set of points into a scale-able object with its own collision. I created this system in C++, the first time I had used the language.
For the front face I used an ear clipping method by testing line intersections to detect valid polygons, eventually reducing the points down to 3 and producing the shape. The “wrap” around the edge is just a second set of the same points offset towards the extrusion surface and joined by alternating triangles. The creation process can be expensive if done with a large number of points so I calculate the information first then limit created sections per frame and spread the cost over time.
Normals, tangents and UVs are also created appropriately using the same data. All this data is then used to create two more meshes for visual feedback.

Since creating complex collision shapes in Unreal with the procedural mesh system gets expensive and splitting it up into smaller shapes causes more issues, I generate an optimized mesh for this purpose. Iterating through the set of points and leaving out the least important ones, determined by cumulative angle change, creates a more suitable collision shape.

The entire mesh creation function is displayed below:
// Creates a mesh if possible out of the points provided
bool AProceduralMeshTemplate::EntireMeshUpdate(AEXSurface* SourceSurface, TArray<FVector> Points) {
// Apply Material
if (ProcMeshMaterial) {
UMaterialInstanceDynamic* MaterialInstance = UMaterialInstanceDynamic::Create(ProcMeshMaterial, this);
ProcMeshMaterialInstance = MaterialInstance;
}
else { GEngine->AddOnScreenDebugMessage(-1, 300.f, FColor::Red, FString::Printf(TEXT("PROC MESH MATERIAL MISSING"))); return false; }
// No triangles can be made
if (Points.Num() < 3) {
return false;
}
bool InvertWinding;
int NoOfPoints = Points.Num();
float TotalArea = 0.f;
float SurfaceSize = 100.f;
// Make the points world positions, rotated to match the surface
for (int i = 0; i < NoOfPoints; i++) {
Points[i] = (GetActorRotation().GetInverse().RotateVector(Points[i]));
Points[i] = GetActorLocation() + Points[i];
}
// Find the center of the provided points
FVector Center = FVector::ZeroVector;
for (int i = 0; i < NoOfPoints; i++) {
Center = Center + Points[i];
}
Center = Center / NoOfPoints;
// Find Forward, Right and Up vectors based on the inverse rotation of the actor
FVector RotF = (GetActorRotation().GetInverse().RotateVector(GetActorForwardVector()));
FVector RotR = (GetActorRotation().GetInverse().RotateVector(-GetActorRightVector()));
FVector RotU = (GetActorRotation().GetInverse().RotateVector(GetActorUpVector()));
// Poject the center onto the surface
FVector2D ProjectedCenter = UMeshGen::ProjectOntoPlane(Center, RotF, GetActorLocation(), RotR, GetActorUpVector());
TArray<FVector2D> ProjectedPoints;
TArray<FVector> RelativePoints;
TArray<FVector2D> AdjustedRelativePoints2D;
TArray<FVector> AdjustedRelativePoints;
TArray<FVector2D> UVPoints;
TArray<FVector2D> OptimizedProjectedPoints;
TArray<FVector> OptimizedRelativePoints;
// Add projected points, relative points and UVs to lists
for (int i = 0; i < NoOfPoints; i++) {
ProjectedPoints.Add(UMeshGen::ProjectOntoPlane(Points[i], RotF, GetActorLocation(), RotR, RotU));
RelativePoints.Add(ProjectedPoints[i].X * RotR + ProjectedPoints[i].Y * RotU);
UVPoints.Add((ProjectedPoints[i] + (FVector2D::UnitVector * SurfaceSize / 2.f) + ProjectedCenter) / SurfaceSize);
UVPoints[i] = FVector2D(-UVPoints[i].Y, -UVPoints[i].X);
}
// Optimise the projected points for the collision mesh, then add projected versions
OptimizedProjectedPoints = UMeshGen::OptimisePoints(ProjectedPoints);
for (int i = 0; i < OptimizedProjectedPoints.Num(); i++) {
OptimizedRelativePoints.Add(OptimizedProjectedPoints[i].X * RotR + OptimizedProjectedPoints[i].Y * RotU);
}
// Determine the surface area of the polygon created by the shape
for (int i = 0; i < NoOfPoints - 1; i++) {
TotalArea += (ProjectedPoints[i + 1].X - ProjectedPoints[i].X)*
(ProjectedPoints[i + 1].Y + ProjectedPoints[i].Y);
}
TotalArea += (ProjectedPoints[0].X - ProjectedPoints[NoOfPoints - 1].X)*
(ProjectedPoints[0].Y + ProjectedPoints[NoOfPoints - 1].Y);
// Face has no surface area
if (FMath::Abs(TotalArea) < 10) {
return false;
}
// Change the winding direction based on the surface area and add adjusted points for outline effect
if (TotalArea < 0) {
InvertWinding = false;
AdjustedRelativePoints2D = UMeshGen::MovePointsAlongNormals(0.1f, ProjectedPoints);
}
else {
InvertWinding = true;
AdjustedRelativePoints2D = UMeshGen::MovePointsAlongNormals(0.1f, ProjectedPoints, true);
}
for (int i = 0; i < NoOfPoints; i++) {
AdjustedRelativePoints.Add(AdjustedRelativePoints2D[i].X * RotR + AdjustedRelativePoints2D[i].Y * RotU);
}
FVector RotatedForward = GetActorRotation().GetInverse().RotateVector(GetActorForwardVector());
// Setup the appropriate lists for mesh section creation in update
CurrentVerts = FaceVerts = RelativePoints;
CurrentTriangles = FaceTriangles = UMeshGen::PolygonTrianglesFromPoints(ProjectedPoints, InvertWinding);
// No triangles to make
if (CurrentTriangles.Num() == 0) {
return false;
}
CurrentUVs = FaceUVs = UVPoints;
CurrentMesh = ProcMesh;
CurrentMaterial = ProcMeshMaterialInstance;
CurrentWrapVerts = UMeshGen::CreatePrismPoints(RelativePoints, SpawnDepth, RotatedForward);
CurrentWrapTriangles = UMeshGen::PrismTrianglesFromPoints(ProjectedPoints, !InvertWinding);
CurrentWrapUVs = UMeshGen::CreatePrismUVs(RelativePoints);
CurrentEffectVerts = UMeshGen::CreatePrismPoints(AdjustedRelativePoints, 100.f, RotatedForward);
FaceIndex = 0;
CurrentSectionIndex = 0;
GenState = EGenState::Face;
CollisionProcMesh->bUseComplexAsSimpleCollision = false;
RotatedForward = GetActorRotation().GetInverse().RotateVector(GetActorForwardVector());
// Create the two section collision mesh, based on winding direction
if (TotalArea < 0) {
UMeshGen::CreateMesh(CollisionProcMesh, OptimizedRelativePoints, UMeshGen::PolygonTrianglesFromPoints(OptimizedProjectedPoints), UVPoints, 0, true);
UMeshGen::CreateMesh(CollisionProcMesh, UMeshGen::CreatePrismPoints(OptimizedRelativePoints, 1000.f, RotatedForward), UMeshGen::PrismTrianglesFromPoints(OptimizedProjectedPoints, true), TArray<FVector2D>(), 1, true);
CollisionProcMesh->SetCollisionConvexMeshes({ OptimizedRelativePoints, UMeshGen::CreatePrismPoints(OptimizedRelativePoints, 1000.f, RotatedForward) });
}
else {
UMeshGen::CreateMesh(CollisionProcMesh, OptimizedRelativePoints, UMeshGen::PolygonTrianglesFromPoints(OptimizedProjectedPoints, true), UVPoints, 0, true);
UMeshGen::CreateMesh(CollisionProcMesh, UMeshGen::CreatePrismPoints(OptimizedRelativePoints, 1000.f, RotatedForward), UMeshGen::PrismTrianglesFromPoints(OptimizedProjectedPoints), TArray<FVector2D>(), 1, true);
CollisionProcMesh->SetCollisionConvexMeshes({ OptimizedRelativePoints, UMeshGen::CreatePrismPoints(OptimizedRelativePoints, 1000.f, RotatedForward) });
}
// Move mesh to 0 distance from surface
MoveActorAlongLine(0);
this->SourceSurface = SourceSurface;
SourceNormal = SourceSurface->GetActorForwardVector();
IgnorePlayerInSweep = SourceSurface->IgnorePlayerInSweep;
SourceSurface->AddProcMesh(this);
return true;
}
Even before the player can produce objects on their own, pre-defined shapes need to be used to teach them the importance of what they can draw. Since the system takes a set of points, these can be fed an array from the level blueprint instead.

Other actions
Destroying and selecting extrusions to move is done by firing the appropriate projectiles with other mouse buttons. Moving a selected extrusion can then be done using the scroll wheel.

Puzzles

As mentioned earlier not all the abilities are given to the player from the beginning. The game teaches the player how to move and destroy first, in that order.
Since we were constrained to a relatively small area I had to move objects in and out of the level to fully explore the mechanics that I had created. This process was tedious but the amount of content as a result made it worthwhile.

Starting off, I aimed to teach the player about choosing shapes. After learning about using an extrusion as a platform, they are given choices between shapes to move with the correct ones allowing them to progress.

The next area features a wall with holes in it, some with extrusion surfaces behind them. To ascend this wall the player needs to push and pull the right extrusions, giving them line of sight to new surfaces and exposing the correct path.

To teach the player destruction I made them hunt for specific objects that re-spawn, moving them into goal zones and opening subsequent areas.

I then give the player the ability to trace with surfaces to create platforms on. Only certain shapes will work for each surface so the player has to put thought into the shapes drawn.

For the final area I split the puzzles into three, each with re-spawning objects and surfaces. The player must guide one of the objects per wall to the floor to complete the game.

The gun
Alongside the game I also had to produce a model of a weapon in Maya that the character would be using. Since it had to be based on an existing object I picked the P.E.P.S gun from Deus Ex: Human Revolution. As with C++, Maya was not something I had used before.
The end result has UVs, simple textures and basic animations. I also set up lights, particles and emissive maps to communicate its current functionality to the player.
Additional work
I decided to keep the visual presentation very simple since I didn’t have the time or experience to make any environment assets.
The gun felt very static relative to the camera so I added some animations via blueprint timelines for drawing, walking, jumping, recoil and weapon sway.


Since I was more well versed with particle effects, I incorporated some into the game to guide the player through their objectives.
The “finale” towards the end gave me an opportunity to go to town and create something more interesting too.


This project took a long time to get right. Most of the time I spent early on was focused on fixing problems with the procedural mesh system. Even then the best solution I came up with couldn’t push other objects which would have opened up many more opportunities for puzzles.
The restrictive specification also meant I had to spend time moving things around in the small area. Unconstrained by this I may have come up with better puzzles and spent more time on the visuals of the environment.
Overall I am happy with the result of this project. Getting the mesh generation working felt like quite an accomplishment due to my inexperience with C++ and I was able to spend time doing interesting things with it as a result.





