Contributions :
- Developed and maintained a tool to generate interactable flat 2D shadows from 3D models
- Created the mechanic of light exclusion
- Rigged and animated 2D skeletal meshes of enemy sprites
- Worked on some puzzles
The way the shadow script genrally works is it generates shadows of 3D models and place it in the scene, each shadow have a script that determines its size and position based on the distance to the light source and wall casted on.
For a basic flow of sequence :
- Assuming you have a 3D object to generate for, instantiate copy of object in front of sub camera.
- Adjust translation and size to fit the camera lens
- Snap a photo with background removed
- Save the picture as sprite and apply some settings to it
- Create an empty object and place a sprite rendere component on it
- Attach to the original 3D game object and adjust the size and positon of the spirite based on distance to the \"main light source\" and \"main wall\"
Shadow generation showcase :
The main script for shadow generated is as follows :
This section is 'snapshot' phase referenced from a screenshot plugin, while this part maybe heavily referenced, the rest of the code are original.
This code involves the screenshot action and converting into a sprite.
Then store the sprite in a specified folder, and apply texture settings with UNITY_EDITOR, shadow generation is only done once in the editor as this is a mobile game.
To change shadow shape in real-time we had a workaround that we did not have the chance to apply but, the idea is to generate multiple frames of the object at different angles then animate between them.
Note
The rest of the code can be found at the snippet link from the first snippet.
The idea for this way of generating shadows was conceived on week 2 of the project when it was approved by the lecturer, the rest of the time was spent on improving the tool and making it more user friendly.
It was somewhat of a cheese way that doesn't require real time shadow rendering which is a heavy task for mobile devices, but it was a fun experiment. The other reason for this method is normal 3D shadow has a complex shape, which makes it hard to build a 2D level.
Our method however snaps the objects in orthographic mode therefore generating flat sprites, allowing for easy level blocking.
Week 2 Concept Demo :
Shadow/Light exclusion
Another mechanic was planned and added for the second level, but the second level didn't make it to the final build but I'll be explaining it here. This mechanic is to negate shadows, making them passable, it can be enemies or obstacles.
Light disabling enemy specific colliders and reenabling out of range
How it works
The script is a simple one, for context, the enemy has 3 different colliders. Any colliders that intersect with the 'light' will be disabled and enqued into a queue, once the collider is out of range, it will be reenabled based on sequence, hence the queue data structure
There is an Coroutine in this script that checks whether the light is still colliding with the collider through a custom function as the in-built unity one is catered for 3D collisions and doesn't work in our particular case. The coroutine would check every 0.5 seconds, if it is still colliding, add it to the back of the queue, else reenable the component.
Custom bound functions
The following are just a slight rewrite to Unity's default bounds functions and removing the Z axis, the reason for this is because Unity's default bounds functions checks for Z value, which is not needed in our case.
Another notable issue is when calling the .bounds variable from a disabled collider component returns 0 no matter what, hence why there is a need to cahce the extent/bounds of collided components.
Brief
First unreal engine game made for Final Year Project. Still currently being devloped!
The game is mainly a mech simulator where you have to kill a kaiju 10x the size of the player. The mech is equipped with some utilities that could help traverse the map, attack the kaiju and mainly survive.
There is no core mechanic per say, but the one mechanic that could be considered the main is the grapple ability. Player can grapple onto any building within a set distance, reel towards it OR reel halfway and dangle around. You could imagine it being similar to the classic attack on titan tribute game on PC quite a few years back.
Notable Features
- Grapple mechanic, grapple onto structures in a set distance and have the ability to reel towards it. Reeling halfway allows the player to 'hang' around like spiderman. This allows player to jump off building without accumulating too much fall damage and have a way to quickly traverse and scale the landscape.
- Scanning shader, a tool equipped onto the mech to scan more interactables on the map such as cars to destroy and alert the kaiju. A side functionality is to scout the landscape surrounding the player as the map tend to get pretty dark.
- Drone deployment, set a static area on the map whereby if the kaiju enters it, it will notify the player.
C++ in Unreal
As this is my first time dabbling with Unreal, I've attempted to mix blueprint and C++ to get a feel of the system, I managed to create scripts for enemy(the large kaiju) pathfinding with Dijkstra algorithm as I wanted to test implementing a simpler algorithm. This took way longer than expected as there a bunch of hurdles on both the editor and Unreal's own C++ framework, the result wasn't as satisfactory either.
To elaborate, the patfinding code works and always returns a path, the next part where Unreal's behaviour tree system somehow fails to find the next path on random intervals. I've tried to debug this issue for a week and nothing seemed to make sense, I am absolutely certain my code was working as a path is returned everytime the previous one ends, but the behaviour tree clears the path at very random intervals with no notable patterns.
Due to time constraints, I've decided to switch to pure blueprint part for finding the path and focus more on player experience namely the mech. I would tackle C++ later on once I've have a better grasp to the underlying paradigm of Unreal through blueprints.
Dijkstra Pathfind Function
Context on what PathFindingManager is working with :
- A custom object class to store an array of path rerturned by the pathfinder (this is because Uneral's blaackboard component doesn't accept arrays as keys for some reason)
- A custom waypoint actor that is placed on the map to act as path points, each contain data linking to other waypoints
AA_WaypointActor.h Structure
Flow of PathFindingManager :
- Determine kaiju initial location by finding the closest waypoint to it
- Set it as start position and randomly select another waypoint as end point on the map
- Pass both data as parameters to FindPath() function to find the path
- Function returns a path by setting the aforementioned custom array class as key in Unreal's built-in blackboard
- Blackboard checks if the key has changed, executes the path
Contributions :
- Turn-based dialogue battle system for mobs
- Dialogue system that takes input from text files with custom markup
- Modular UI system for collected items that displays on inventory
- Battle / Exploration UI design and art
- Few sprite art in tutorial stage
Battle Manager Script
I won't go into too much details on this as it is somewhat of a bad implementation, but the general idea is that it has 3 states, enemy turn, player turn and no battle state. Partial finite state machine is utilized at the start of each turn but the rest is based entirely in this one script as I was unfamiliar with FSM, the FSM was introduced at first by another programmer but he couldn't make the battle system in time for the first prototype so it was handed over.
Dialogue Manager
A modular dialogue system that takes input from text files with custom markup, that displays text letter by letter as well as lerp the camera to locations to focus on during conversation.
The general flow goes like this :
- Any actor or object that is interactable would have a script called TriggerDialogue.cs where it can reference a txt file contains the conversation, and locations that it would like to focus on duing conversation.
- Another script called InteractablePrompt.cs should also be present that instantiates a collider that allows the dialogue script to be triggered if needed.
- If an interaction is triggered, the TriggerDialigue script would call startConvo() on a global DialogueManager and the conversation will begin, by passing the text file referenced as well as locations to lerp to which is optional.
- The manager would then split the content of the text file into names, the lines as well as which line to act on a camera lerp scenario.
Dialogue Manager Script
Example text file for mark-up format :
name:
is the format for the name to be displayed for the character.
:player
is the format to set the camera to focus on.
Example:
:puzzleGuy
will focus the camera on the puzzle guy on this line.
Rest are the dialogue lines itself.
Displaying/Typing the dialogues :
Brief :
Submission for September Sem Jam 2023, theme was 'Slime'.
Gameplay is a 2D platformer where you play as a slime trying to please your wife by going on a journey to harden yourself. Game mechanic involves sucking on harder items and become harder yourself.
This is also to experiment with 2D softbody physics.
A 2D puzzle/stealth game where the player plays as a blob that can turn into any human being it eats. You came onto the streets outside an apartment feeling hungry, so you decided to take a quick midnight snack in the apartment.
The mechanics are simple, you can consume any human as long as you're in blob form. You can morph back and forth to the latest form you've taken. Being spotted in your blob form or somewhere you shouldn't be in as a human (like a cop in some family home) will trigger suspicion.
A 2D vertical shooter and a fan-parody of the Touhou series. My first completed Unity game.
Second stencyl game, a shoot-em-up with 3 stages each with their own boss battles.
All art are drawn by myself.
My personal favourite game made in Stencyl with 3 difficult stages.
Cool features include gliding, shift-jumping, sliding and blood!
Warning: This game is way too hard. Do give it a try!
My first stencyl game made for assignment, also my first completed game. A modified pong with additional power ups.
// - Creating the texture and capturing
RenderTexture renderTexture = new RenderTexture(horizontalResolution, verticalResolution, captureDepth);
Rect rect = new Rect(0, 0, horizontalResolution, verticalResolution);
Texture2D texture = new Texture2D(horizontalResolution, verticalResolution, TextureFormat.ARGB32, false);
generationCam.targetTexture = renderTexture;
generationCam.Render();
RenderTexture currentRenderTexture = RenderTexture.active;
RenderTexture.active = renderTexture;
texture.ReadPixels(rect, 0, 0);
texture.Apply();
generationCam.targetTexture = null;
RenderTexture.active = currentRenderTexture;
DestroyImmediate(renderTexture);
Sprite sprite = Sprite.Create(texture, rect, Vector2.zero);
if (specifiedSpritePath != null)
{
spritePath = specifiedSpritePath;
}
// - Saving texture as PNG
byte[] itemBGBytes = sprite.texture.EncodeToPNG();
outputfilename = $"{parentName}_ShadowSprite";
textureFailsafeID++;
File.WriteAllBytes($"{spritePath}/{outputfilename}.png", itemBGBytes);
#if UNITY_EDITOR
// - Setting texture settings
UnityEditor.AssetDatabase.Refresh();
TextureImporter importer = (TextureImporter)TextureImporter.GetAtPath($"{spritePath}/{outputfilename}.png");
importer.textureType = TextureImporterType.Sprite;
importer.alphaIsTransparency = true;
importer.filterMode = FilterMode.Point;
importer.spritePixelsPerUnit = (100 * (shadowQuality - 8)) / (shadowSizeOffset / 2);
EditorUtility.SetDirty(importer);
importer.SaveAndReimport();
#endif
// - Applying sprite to shadow
if (specifiedSpritePath != null)
{
tempSR.sprite = Resources.Load($"GeneratedShadowTextures/PermanentSprites/{outputfilename}");
}
else
{
tempSR.sprite = Resources.Load($"GeneratedShadowTextures/{SceneManager.GetActiveScene().name}/{outputfilename}");
}
tempSR.color = shadowColor;
tempSR.material = shadowMaterial;
tempSR.gameObject.AddComponent();
[SerializeField] float extentScaleIncreaseX;
[SerializeField] float extentScaleIncreaseY;
private Collider2D thisBounds; //Exclusion own bounds
Queue> collidedList = new Queue>();
Coroutine crCheck = null;
private void Start(){
thisBounds = GetComponent();
}
void OnTriggerEnter2D(Collider2D collision)
{
//Enqueue collided collider components, and disable them
if (collision.CompareTag("Blight") || collision.CompareTag("ExcludableProp"))
{
collidedList.Enqueue(new KeyValuePair(collision, collision.bounds.extents));
collision.enabled = false;
if (crCheck == null)
{
//Start coroutine that checks if it's in bound every few milliseconds
crCheck = StartCoroutine(ColliderUpdate());
}
}
}
IEnumerator ColliderUpdate()
{
while (collidedList.Count > 0)
{
KeyValuePair item = collidedList.Dequeue();
if (!IntersectCheck(thisBounds.bounds, item.Key.bounds, item.Value))
{
item.Key.enabled = true;
}
else
{
collidedList.Enqueue(item);
}
yield return new WaitForSeconds(0.5f);
}
crCheck = null;
}
//Made own intersect because unity default intersect checks for Z val, will return false even though its 0 <= 0 and think nothing is intersecting at all
//And also to check for cached bounds.extents val instead of the disabled one.
public bool IntersectCheck(Bounds thisB, Bounds collidedB, Vector2 collidedExtentCache)
{
return IMin(thisB, true) <= IMax(collidedB, true, collidedExtentCache) &&
IMax(thisB, true) >= IMin(collidedB, true, collidedExtentCache) &&
IMin(thisB, false) <= IMax(collidedB, false, collidedExtentCache) &&
IMax(thisB, false) >= IMin(collidedB, false, collidedExtentCache);
}
public float IMin(Bounds boundStruct, bool isX, Vector2? cache = null)
{
if (cache != null)
{
return isX ? boundStruct.center.x - (cache.ConvertTo().x + extentScaleIncreaseX) : boundStruct.center.y - (cache.ConvertTo().y + extentScaleIncreaseY);
}
else
{
return isX ? boundStruct.center.x - boundStruct.extents.x : boundStruct.center.y - boundStruct.extents.y;
}
}
public float IMax(Bounds boundStruct, bool isX, Vector2? cache = null)
{
if (cache != null)
{
return isX ? boundStruct.center.x + (cache.ConvertTo().x + extentScaleIncreaseX) : boundStruct.center.y + (cache.ConvertTo().y + extentScaleIncreaseY);
}
else
{
return isX ? boundStruct.center.x + boundStruct.extents.x : boundStruct.center.y + boundStruct.extents.y;
}
}
TArray AA_PathfindingManager::FindPath(AA_WaypointActor* StartWaypoint, AA_WaypointActor* TargetWaypoint){
TArray resultPath;
TMap DistanceMap
TMap ParentMap;
for (AA_WaypointActor* thisWp : AllWaypoints){
DistanceMap.Add(thisWp, 999999);
ParentMap.Add(thisWp, nullptr);
}
if(AllWaypoints.Num() == 0){
GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Purple, TEXT("NO WAYPOINTS WTF"));
return TArray();
}
if(DistanceMap.Num() == 0){
return TArray();
}
DistanceMap[StartWaypoint] = 0;
// Create an empty set to keep record all visited waypoints
TSet VisitedWaypoints;
// Main loop for Dijkstra's algorithm
while (VisitedWaypoints.Num() < AllWaypoints.Num()){
// Find the waypoint with the smallest distance that has not been visited
AA_WaypointActor* CurrentWaypoint = nullptr;
float MinDistance = 999999;//From big to small, reminder to self at 3am
for (AA_WaypointActor* thisWaypoint : AllWaypoints){
if (!VisitedWaypoints.Contains(thisWaypoint) && DistanceMap[thisWaypoint] < MinDistance){
CurrentWaypoint = thisWaypoint;
MinDistance = DistanceMap[thisWaypoint];
}
}
if (!CurrentWaypoint){
GEngine->AddOnScreenDebugMessage(10, 5.f, FColor::White, TEXT("Cant seem to find path or smtg broke"));
break;
}
// Mark the current waypoint as visited
VisitedWaypoints.Add(CurrentWaypoint);
// Update distances and parents for adjacent waypoints
for (FWaypointConnection connectionStruct : CurrentWaypoint->ConnectedWaypoints){
AA_WaypointActor* theConnectedWaypoint = connectionStruct.ConnectedWaypoint;//Singular
float NewDistance = DistanceMap[CurrentWaypoint] + 1.0f; // Assume all connections have the same weight (1.0f)
if (NewDistance < DistanceMap[theConnectedWaypoint]){
DistanceMap[theConnectedWaypoint] = NewDistance;
ParentMap[theConnectedWaypoint] = CurrentWaypoint;
}
}
}
// Get parent->parent->parent of defined path from the target
AA_WaypointActor* tPoint = TargetWaypoint;
while (tPoint != nullptr){
resultPath.Insert(tPoint, 0);//First goes all the way back
tPoint = ParentMap[tPoint];
}
return resultPath;
}
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/BillboardComponent.h"
#include "A_WaypointActor.generated.h"
USTRUCT(BlueprintType)
struct FWaypointConnection//Struct for each connection to waypoints with state to see if is blocked
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
AA_WaypointActor* ConnectedWaypoint;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
bool bIsRoadBlocked = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
int distanceWeighted = 1;//Default 1 for now
};
UCLASS()
class KAIJUGAMETEST_API AA_WaypointActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AA_WaypointActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
int nodeID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
TArray ConnectedWaypoints;//Waypoints connected to this specific node
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding")
FString pointName = "default_name";
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pathfinding")
class USceneComponent* DefaultSceneRoot; // Declaration of DefaultSceneRoot component
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Pathfinding")
class UBillboardComponent* SpriteComponent;
};
//Conversation functions that are called once to initiate by TriggerDialigue.cs
public void startConversation(TextAsset targetFile)
{
currentText = "";
canInput = true;
dialogueUI.SetActive(true);
dialogueActive = true;
dialogueCooldown = true;
curLineNum = 0;
//Dialogue lines, split by line breaks in text file
dls = targetFile.text.Split('\n');
dlsSize = dls.Length;
Time.timeScale = 0;
displayCurrentDialogue();
}
//Overloaded funciton to start conversations with locations to lerp camera to mid convo
public void startConversation(TextAsset targetFile, List trList)//Function override
{
canInput = true;
currentText = "";
gotoDC.Clear();
gotoDC.Add("player", playerPos);
if (trList.Count > 0)
{
foreach (TransformList x in trList)
{
gotoDC.Add(x.locationName, x.transformReference);
}
}
//*/
dialogueUI.SetActive(true);
dialogueActive = true;
dialogueCooldown = true;
curLineNum = 0;
dls = targetFile.text.Split('\n');
dlsSize = dls.Length;
Time.timeScale = 0;
displayCurrentDialogue();
}
Alex:Um do you know where is this?
???:Not really
???:Why don't you try asking that guy over there..:puzzleGuy
???:Dudes been rambling about some kind of puzzle the whole time
???:Might be useful you know hehe..
Alex:Uh.. ok thanks?...:player
void displayCurrentDialogue()
{
StartCoroutine(enableInput());
//Spliting for format
string[] thisLine = dls[curLineNum].Split(':');
// name : character line format is split into a tiny array
talkerName.text = thisLine[0];
typeD = typeDialogue(thisLine[1]);
//Set pointer to start on first line
currentText = thisLine[1];
//Lerp camera to specified target if needed
if (thisLine.Length>2)
{
string sanitizedKey;
if(curLineNum == dlsSize-1)
{
sanitizedKey = thisLine[2];//Last line doesnt have a weird extra character at the end so no need to sanitize
}
else
{
sanitizedKey = thisLine[2].Substring(0, thisLine[2].Length - 1);
}
camState.isCutScene = true;
Vector3 targetPosition = new Vector3(gotoDC[sanitizedKey].position.x, gotoDC[sanitizedKey].position.y, camPos.position.z);
Vector3 defaultPosition = new Vector3(camPos.position.x, camPos.position.y, camPos.position.z);
StartCoroutine(lerpToTarget(defaultPosition, targetPosition));
}
//Start the typing letter by letter coroutine
StartCoroutine(typeD);
curLineNum++;
}