UX/UI/Game Designer
Play the game as the faun of the forest, who wakes up by the sunbeams from the morning light. But this morning, something feels odd about the light; something's not quite right. Anxiety starts to spread through the fauns closer to the window. But that is not the only thing spreading; something else is also starting to take over, spreading fast.
- Lead level designer
- Lead environmental mechanic developer
- Technical game designer
- Engine: Unity
- Language: C#
- Platform: PC
- Development time: 8 weeks
- Team size: 8
- Designed 6/8 levels
- Narrative writer
- Balanced the game
- Perforce guideline manager
- Custom tool developer
I collaborated with the team members and created mechanics to increase the player’s agency. My main goal for this game was to make it more enjoyable and intuitive, not only moving left and right but forcing the player to go back and forth when they played each level. Examples are the elevator, destroy platform, and the ability to destroy boxes to solve puzzles within the levels. Which then were implemented by me.
In Corruption our main goal was to let the player move in each axis. The team wanted to have more depth in the game than mainly X-axis movement; the elevator has this function and also the functionality to enhance the feeling of a fantasy game as the player needs to stand on the correct crystal to move the Corruption our main goal was to let the player move on each axis. We wanted more depth in the game than mainly X-axis movement; the elevator has this function and enhances the feeling of a fantasy game, as the player needs to stand on the correct crystal to move the elevator to activate its magic. The elevator enables the player to move across the Y-axis into interesting mine shafts, see the vast environment, including big trees, and see how the environment is changing concerning the spreading Corruption.
Communication between the graphical artists was ongoing during the implementation of the elevator, and communication between the graphical artists and me was ongoing through the implementation of the elevator to get the correct feeling of the elevator. There was also communication among the design team about the elevator’s speed, how responsive it should be, etc. Also, the different elevators in the game had to have different settings and how they would behave when no player was occupying a button. It had to reset to its default position on some levels, and in some situations, it did not. Mainly to counteract some potential hard stuff for the player when they load the game from certain positions.
using System.Collections;
using UnityEngine;
public class movingPlatform : MonoBehaviour
{
//Dependencies: insertPlayerOntrigger - send data
//This components job is to move a platform up or down i regards to settings of this component.
[SerializeField] private float speed;
private Transform topPoint;
private Transform bottomPoint;
private float topPointPos;
private float bottomPointPos;
enum MoveState { moveUp, dontMove, moveDown }
MoveState moves = MoveState.dontMove;
[SerializeField] private movingPlatformUp[] movingPlatformButtonss = new movingPlatformUp[2];
public bool isDefaultPointUp;
public bool isDefaultPointDown;
[SerializeField] private bool dontMoveOnButtonNotOccupied;
[SerializeField] private float timer;
private float savedTimer;
private void Start()
{
savedTimer = timer;
topPoint = transform.Find("UpPoint");
bottomPoint = transform.Find("DownPoint");
topPointPos = topPoint.position.y;
bottomPointPos = bottomPoint.position.y;
}
private void FixedUpdate()
{
if (!movingPlatformButtonss[0].moveUpOccupied && !movingPlatformButtonss[1].moveDownOccupied && dontMoveOnButtonNotOccupied) //If no button is occupied and dontMoveOnButtonNotOccupied is inserted in the inspector dontMove state is initiated
{
moves = MoveState.dontMove;
}
else if (movingPlatformButtonss[0].moveUpOccupied) //if first button is occupied moveUp state is initiated
{
moves = MoveState.moveUp;
}
else if (movingPlatformButtonss[1].moveDownOccupied) //If second button is occupied moveDown state is initiated
{
moves = MoveState.moveDown;
}
else if (!movingPlatformButtonss[1].moveDownOccupied && !movingPlatformButtonss[0].moveDownOccupied && isDefaultPointDown && !dontMoveOnButtonNotOccupied) //when no button is occuptied, down is default point moveDown state is initiated
{
moves = MoveState.moveDown;
}
else if (!movingPlatformButtonss[0].moveDownOccupied && !movingPlatformButtonss[1].moveDownOccupied && isDefaultPointUp && !dontMoveOnButtonNotOccupied) //When no button is occupied and up is default point. moveUp state is initiated
{
moves = MoveState.moveUp;
}
else
{
return;
}
switch (moves) //Cases for different move states
{
case MoveState.moveUp:
StartCoroutine((StartMove(topPointPos, speed, true, 1)));
timer = savedTimer;
break;
case MoveState.dontMove:
if (timer >= -0.1)
{
timer -= Time.deltaTime;
}
if (timer < 0)
{
if (isDefaultPointDown && !dontMoveOnButtonNotOccupied)
StartMove(bottomPointPos, speed, false);
else if (isDefaultPointUp && !dontMoveOnButtonNotOccupied)
StartMove(topPointPos, speed, false);
else if (dontMoveOnButtonNotOccupied)
{
return;
}
}
break;
case MoveState.moveDown:
StartCoroutine(StartMove(bottomPointPos, speed, true, 1));
timer = savedTimer;
break;
default:
break;
}
}
private IEnumerator StartMove(float targetedPosition, float speedForce, bool movePlayer, int seconds) //IEnumerator to move platform to targeted position
{
yield return new WaitForSeconds(seconds);
Vector3 desiredPosition = new Vector3((transform.position.x), targetedPosition, 0);
Vector3 smoothedPosition = Vector3.MoveTowards(transform.position, desiredPosition, speedForce * Time.fixedDeltaTime);
transform.position = smoothedPosition;
}
private void StartMove(float targetedPosition, float speedForce, bool movePlayer) //Translates to targetedPosition when this method is called
{
transform.Translate((Vector2)transform.position + new Vector2(transform.position.x, targetedPosition) * speedForce * Time.fixedDeltaTime);
}
}
Interesting mechanics were needed to create puzzles and game patterns for the player. The destroy platform feature lets the player destroy platforms by standing on them or holding a specific object while jumping. The mechanic was made to increase the player’s agency; these objects are hidden within the level behind puzzles.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class hangingPlatformScript : MonoBehaviour
{
private DistanceJoint2D joint;
public bool playerIsInside;
private GameObject playerObject;
public bool enemyisInside;
public bool respawningPlatform;
[SerializeField] public bool Respawn;
[SerializeField] private Vector2 respawnPoint;
[SerializeField] private Rigidbody2D leftLogPhysics;
[SerializeField] private Rigidbody2D rightLogPhysics;
[SerializeField] private int velocityBreak = 13;
private Float2 leftLogScale;
private Float2 rightLogScale;
private float distanceJoint;
public Rigidbody2D boulderInsideObject;
public bool boulderInside;
private Vector2 LeftLogForce { get; set; } = new Vector2(-30, -30);
private Vector2 RightLogForce { get; set; } = new Vector2(20, -20);
private float LeftTorque { get; set; } = -1.5f;
private float RightTorque { get; set; } = 1f;
private Rigidbody2D playerBody;
[SerializeField] private int WaitTimer;
public bool storeEnemy;
public bool storeBoulder;
public bool activateOnBoulderRollingOver;
public bool storeAboxAndStoreEnemy;
public bool storeAbox;
public bool boxIsInside;
public bool softLockCheck;
private void Start() //Sets parameters
{
if (boulderInsideObject == null)
{
boulderInsideObject = leftLogPhysics;
}
playerObject = GameObject.FindWithTag("Player");
playerBody = playerObject.GetComponent<´.Rigidbody2D>();
joint = GetComponent<.DistanceJoint2D>();
respawnPoint = transform.parent.transform.position;
setBreakingLogParam();
leftLogScale = new Float2(leftLogPhysics.transform.localScale.x, leftLogPhysics.transform.localScale.y);
rightLogScale = new Float2(rightLogPhysics.transform.localScale.x, rightLogPhysics.transform.localScale.y);
distanceJoint = gameObject.GetComponent<.DistanceJoint2D>().distance;
}
private void setBreakingLogParam() //Sets references to spriteobjects.
{
for (int i = 0; i < transform.childCount; i++)
{
if (transform.GetChild(i).GetComponent<.BreakingLog>())
{
if (transform.GetChild(i).GetComponent<.BreakingLog>().dir == "Left")
{
leftLogPhysics = transform.GetChild(i).GetComponent<.Rigidbody2D>();
}
else if (transform.GetChild(i).GetComponent<.BreakingLog>().dir == "Right")
{
rightLogPhysics = transform.GetChild(i).GetComponent<.Rigidbody2D>();
}
}
}
}
private void FixedUpdate()
{
if(softLockCheck) //Sets breakforce to int max value when this boolean is true to avoid softlocks in the game
{
joint.breakForce = int.MaxValue;
}
if (boulderInsideObject == null)
{
boulderInsideObject = leftLogPhysics;
}
if (playerIsInside && playerBody.velocity.y <= -velocityBreak && joint != null && !storeAbox && !softLockCheck)
{
breakPlatform();
}
if (storeAbox && playerIsInside && playerBody.velocity.y <= -velocityBreak && joint != null && boxIsInside && !softLockCheck)
{
breakPlatform();
StartCoroutine(DisableColliders(WaitTimer));
}
if (boxIsInside && enemyisInside && joint != null && storeAboxAndStoreEnemy && !softLockCheck)
{
StartCoroutine(BreakPlatform(WaitTimer));
if (Respawn && !respawningPlatform)
{
StartCoroutine(Destroyplatform());
}
StartCoroutine(DisableColliders(WaitTimer));
}
if (enemyisInside && joint != null && storeEnemy &&!softLockCheck)
{
StartCoroutine(BreakPlatform(WaitTimer));
if (Respawn && !respawningPlatform)
{
StartCoroutine(Destroyplatform());
}
StartCoroutine(DisableColliders(WaitTimer));
}
if(storeBoulder && joint != null && boulderInside && boulderInsideObject.velocity.y <= -velocityBreak && !softLockCheck)
{
breakPlatform();
StartCoroutine(DisableColliders(WaitTimer));
}
if(activateOnBoulderRollingOver && storeBoulder && joint != null && boulderInside && boulderInsideObject.velocity.x <= -velocityBreak && !softLockCheck)
{
breakPlatform();
}
}
private void breakPlatform() //method for the visual effect of the breaking platform.
{
joint.breakForce = 300;
LogBreak(leftLogPhysics, LeftLogForce, LeftTorque);
LogBreak(rightLogPhysics, RightLogForce, RightTorque);
Physics2D.IgnoreCollision(GetComponent<.Collider2D>(), playerObject.GetComponent<.Collider2D>());
if (Respawn && !respawningPlatform)
{
StartCoroutine(Destroyplatform());
}
StartCoroutine(DisableColliders(WaitTimer));
}
private void LogBreak(Rigidbody2D rigidbody2D, Vector2 force, float torque) // used by breakPlatform to apply phyics to the objects.
{
rigidbody2D.gameObject.transform.parent = null;
rigidbody2D.AddTorque(torque);
rigidbody2D.AddForce(force);
rigidbody2D.gravityScale = 1;
}
IEnumerator BreakPlatform(int seconds) // Ienumerator to delay breaking
{
yield return new WaitForSeconds(seconds);
if (joint != null)
{
joint.breakForce = 0;
}
}
IEnumerator Destroyplatform() // destroys the gameobject after it has spawned, only being done if the platform is set to respawn.
{
respawningPlatform = true;
Float2 scale = new Float2(transform.localScale.x, transform.localScale.y);
Float2 parentScale = new Float2(transform.parent.localScale.x, transform.parent.localScale.y);
yield return StartCoroutine(RespawnPlatform(scale, leftLogScale, rightLogScale, parentScale, distanceJoint, storeAbox, velocityBreak));
yield return new WaitForSeconds(2);
Destroy(this.transform.parent.gameObject);
Destroy(leftLogPhysics.transform.gameObject);
Destroy(rightLogPhysics.transform.gameObject);
}
IEnumerator RespawnPlatform(Float2 scriptscale, Float2 leftLogScale, Float2 rightLogScale, Float2 parentScale, float distanceJointSize, bool storeBox, int platformVelocityBreak) // Respawns the platforms with the same transform as in the scene.
{
yield return new WaitForSeconds(3);
var objectToSpawn = Resources.Load("hangingPlatform_Corr") as GameObject;
GameObject gO = Instantiate(objectToSpawn as GameObject, respawnPoint, Quaternion.identity);
yield return new WaitForEndOfFrame();
gO.GetComponentInChildren<.hangingPlatformScript>().Respawn = true;
gO.transform.localScale = new Vector3(parentScale.floatOne, parentScale.floatTwo, 1);
gO.transform.GetComponentInChildren<.hangingPlatformScript>().gameObject.transform.localScale = new Vector3(scriptscale.floatOne, scriptscale.floatTwo, 1);
gO.GetComponentInChildren<.hangingPlatformScript>().leftLogPhysics.gameObject.transform.localScale = new Vector3(leftLogScale.floatOne, leftLogScale.floatTwo, 1);
gO.GetComponentInChildren<.hangingPlatformScript>().rightLogPhysics.gameObject.transform.localScale = new Vector3(rightLogScale.floatOne, rightLogScale.floatTwo, 1);
gO.GetComponentInChildren<.DistanceJoint2D>().distance = distanceJointSize;
gO.GetComponentInChildren<.hangingPlatformScript>().storeAbox = storeBox;
gO.GetComponentInChildren<.hangingPlatformScript>().velocityBreak = platformVelocityBreak;
yield return new WaitForEndOfFrame();
gO.GetComponentInChildren<.hangingPlatformScript>().leftLogPhysics.transform.GetComponent<.SpriteRenderer>().sortingOrder = 3;
gO.GetComponentInChildren<.hangingPlatformScript>().rightLogPhysics.transform.GetComponent<.SpriteRenderer>().sortingOrder = 3;
respawningPlatform = false;
}
IEnumerator DisableColliders(int sec) // removes colliders so it doesn't interfere with the player.
{
yield return new WaitForSeconds(sec);
gameObject.GetComponent<.BoxCollider2D>().enabled = false;
}
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[RequireComponent(typeof(BoxCollider2D))]
public class VFXRandomizer : MonoBehaviour
{
//Julius Sellgren
//Dependencies: Bezier - Gets data from bezier
private int randomNumber;
[SerializeField] float waitTime = 3;
private float startTime;
private bool playerIsInside = false;
[HideInInspector]
public List<.GameObject> vfx;
private BoxCollider2D box;
[SerializeField] private bool dontWantToRandomize;
[SerializeField] [Range(0, 0.3f)] private float pointOffset;
public float _pointOffset
{
get { return pointOffset; }
}
[SerializeField] [Range(1, 100)] private int particlesToSpawn;
[HideInInspector]
public Vector2[] bezierPoint;
public Vector2[] _bezierPoint
{
get { return bezierPoint; }
}
public string vfxName;
private string attemptedToLoad;
public string _attemptedToLoad
{
get { return attemptedToLoad; }
}
private void Start()
{
box = GetComponent<.BoxCollider2D>();
startTime = waitTime;
}
void GetRandomNumber() //Gets random number
{
if (waitTime <= 0 && playerIsInside && !dontWantToRandomize)
{
randomNumber = UnityEngine.Random.Range(0, vfx.Count);
VFX_Start(randomNumber);
waitTime = startTime;
}
else
waitTime -= Time.deltaTime;
}
private void Update() //Exectutes randomizing and play animations
{
if(!dontWantToRandomize)
{
GetRandomNumber();
}
else if(playerIsInside && dontWantToRandomize)
{
for (int i = 0; i < vfx.Count; i++)
{
vfx[i].GetComponent<.ParticleSystem>().Play();
}
}
else if(!playerIsInside && dontWantToRandomize)
{
for (int i = 0; i < vfx.Count; i++)
{
vfx[i].GetComponent<.ParticleSystem>().Stop();
}
}
}
private void OnTriggerEnter2D(Collider2D collision) //When player moves into trigger area
{
if (collision.CompareTag("Player"))
{
playerIsInside = true;
}
}
private void OnTriggerExit2D(Collider2D other) //When player moves out of trigger area
{
if (other.CompareTag("Player"))
{
playerIsInside = false;
}
}
private void VFX_Start(int chosen) //Plays the randomized effect
{
vfx[chosen].GetComponent<.ParticleSystem>().Play();
}
public void SpawnVFX() //Method for attached editor script.
{
vfx = new List<.GameObject>();
int count = 0;
for (int i = transform.childCount - 1; i >= 0; i--)
{
DestroyImmediate(transform.GetChild(i).gameObject);
}
var VFX = Resources.Load(vfxName) as GameObject;
float stepSize = particlesToSpawn * 1;
if (particlesToSpawn <= 0 || VFX == null)
{
return;
}
else
{
stepSize = 1f / (stepSize - 1);
}
for (int p = 0, f = 0; f < particlesToSpawn; f++)
{
count++;
for (int i = 0; i < 1; i++, p++)
{
GameObject item = Instantiate(VFX) as GameObject;
item.name = vfxName + count;
item.tag = "VFX";
item.transform.localScale = new Vector3(transform.localScale.x * item.transform.localScale.x, transform.localScale.y * item.transform.localScale.y, 1);
vfx.Add(item.gameObject);
Vector2 position = GetPoint(p * stepSize);
item.transform.localPosition = position;
item.transform.parent = transform;
}
}
box = GetComponent<.BoxCollider2D>();
box.offset = new Vector2(box.size.x / 2, -box.size.y / 2);
}
public Vector2 GetPoint(float t) //Get bezier point for editor handle
{
return transform.TransformPoint(Bezier.GetPoint(bezierPoint[0], bezierPoint[1], bezierPoint[2], bezierPoint[3], t));
}
public Vector2 GetVelocity(float t) //Get bezier first derivative
{
return transform.TransformPoint(Bezier.GetFirstDerivative(bezierPoint[0], bezierPoint[1], bezierPoint[2], bezierPoint[3], t)) - transform.position;
}
public Vector2 GetDirection(float t) //Gets the normalized direction of this handle
{
return GetVelocity(t).normalized;
}
public void ResetPoints() //Resets and deletes all child objects attached to this object
{
vfx = new List<.GameObject>();
for (int i = transform.childCount - 1; i >= 0; i--)
{
DestroyImmediate(transform.GetChild(i).gameObject);
}
bezierPoint = new Vector2[] {
new Vector2(0f, -pointOffset),
new Vector2(0.25f, -pointOffset),
new Vector2(0.75f, -pointOffset),
new Vector2(1f, -pointOffset)
};
}
public void UpdateOffset() //Updates the offset of the bezier curve
{
try
{
bezierPoint[0] = new Vector2(0f, -pointOffset);
bezierPoint[1] = new Vector2(bezierPoint[1].x, bezierPoint[1].y);
bezierPoint[2] = new Vector2(bezierPoint[2].x, bezierPoint[2].y);
bezierPoint[3] = new Vector2(1f, -pointOffset);
}
catch (IndexOutOfRangeException)
{
}
}
public void DeleteFX() //Deletes all child objects of this gameobject
{
vfx = new List<.GameObject>();
for (int i = transform.childCount - 1; i >= 0; i--)
{
DestroyImmediate(transform.GetChild(i).gameObject);
}
}
private void OnValidate() //When variables has changed this method is called
{
var attemptToload = Resources.Load(vfxName) as GameObject;
if (attemptToload != null)
{
attemptedToLoad = "Yes";
}
else if (attemptToload == null)
{
attemptedToLoad = "No";
}
int tempValdate = particlesToSpawn;
if (tempValdate > gameObject.transform.localScale.x)
{
particlesToSpawn = (int)gameObject.transform.localScale.x;
}
else
{
particlesToSpawn = tempValdate;
}
UpdateOffset();
}
}
using UnityEngine;
public static class Bezier
{
public static Vector2 GetPoint(Vector2 pointOne, Vector2 pointTwo, Vector2 pointThree, float t) //Get the position of the points
{
t = Mathf.Clamp01(t);
float oneMinusT = 1f - t;
return
oneMinusT * oneMinusT * pointOne +
2f * oneMinusT * t * pointTwo +
t * t * pointThree;
}
public static Vector2 GetFirstDerivative(Vector2 pointOne, Vector2 pointTwo, Vector2 pointThree, float t) //Takes the primitive function of the other derivative
{
return
2f * (1f - t) * (pointTwo - pointOne) +
2f * t * (pointThree - pointTwo);
}
public static Vector2 GetPoint(Vector2 pointOne, Vector2 pointTwo, Vector2 pointThree, Vector2 pointFour, float t) //If called with five overloads
{
t = Mathf.Clamp01(t);
float OneMinusT = 1f - t;
return
OneMinusT * OneMinusT * OneMinusT * pointOne +
3f * OneMinusT * OneMinusT * t * pointTwo +
3f * OneMinusT * t * t * pointThree +
t * t * t * pointFour;
}
public static Vector2 GetFirstDerivative(Vector2 pointOne, Vector2 pointTwo, Vector2 pointThree, Vector2 pointFour, float t) //Takes the primitive function of the other derivative if we have another overload
{
t = Mathf.Clamp01(t);
float oneMinusT = 1f - t;
return
3f * oneMinusT * oneMinusT * (pointTwo - pointOne) +
6f * oneMinusT * t * (pointThree - pointTwo) +
3f * t * t * (pointFour - pointThree);
}
}
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(VFXRandomizer))]
public class VFXEditor : Editor
{
private bool showButtons;
public override void OnInspectorGUI()
{
DrawDefaultInspector();
VFXRandomizer myScript = (VFXRandomizer)target;
EditorGUILayout.LabelField("Does the file exist? " + myScript._attemptedToLoad);
if(showButtons)
{
if (GUILayout.Button("Spawn VFX"))
{
EditorUtility.SetDirty(myScript);
myScript.SpawnVFX();
}
if (GUILayout.Button("Remove VFX"))
{
EditorUtility.SetDirty(myScript);
myScript.DeleteFX();
}
if (GUILayout.Button("Reset BezierCurve"))
{
EditorUtility.SetDirty(myScript);
myScript.ResetPoints();
}
if (GUILayout.Button("Collapse all functions"))
{
EditorUtility.SetDirty(myScript);
showButtons = false;
}
}
else
{
if (GUILayout.Button("Expand all functions"))
{
EditorUtility.SetDirty(myScript);
showButtons = true;
}
}
}
private const int lineSteps = 10;
private const float directionScale = 0.5f;
private VFXRandomizer bezierCurve;
private Transform handleTransform;
private Quaternion handleRotation;
private void OnSceneGUI()
{
bezierCurve = target as VFXRandomizer;
handleTransform = bezierCurve.transform;
handleRotation = Tools.pivotRotation == PivotRotation.Local ? handleTransform.rotation : Quaternion.identity;
Vector2 pointOne = ShowPoint(0);
Vector2 pointTwo = ShowPoint(1);
Vector2 pointThree = ShowPoint(2);
Vector2 pointFour = ShowPoint(3);
Handles.color = Color.red;
Handles.DrawLine(pointOne, pointTwo);
Handles.DrawLine(pointThree, pointFour);
ShowDirections();
Handles.DrawBezier(pointOne, pointFour, pointTwo, pointThree, Color.white, null, 2f);
}
private void ShowDirections()
{
Handles.color = Color.red;
Vector2 point = bezierCurve.GetPoint(0f);
Handles.DrawLine(point, point + bezierCurve.GetDirection(0f) * directionScale);
for (int i = 1; i <= lineSteps; i++)
{
point = bezierCurve.GetPoint(i / (float)lineSteps);
Handles.DrawLine(point, point + bezierCurve.GetDirection(i / (float)lineSteps) * directionScale);
}
}
private Vector3 ShowPoint(int index)
{
Vector3 point = handleTransform.TransformPoint(bezierCurve._bezierPoint[index]);
EditorGUI.BeginChangeCheck();
point = Handles.DoPositionHandle(point, handleRotation);
if (EditorGUI.EndChangeCheck())
{
EditorUtility.SetDirty(bezierCurve);
bezierCurve._bezierPoint[index] = handleTransform.InverseTransformPoint(point);
bezierCurve._bezierPoint[0] = new Vector2(0, -bezierCurve._pointOffset);
bezierCurve._bezierPoint[3] = new Vector2(1, -bezierCurve._pointOffset);
}
return point;
}
#endif
}
In this project, I learned and experienced how it is to have a lead role in a more significant game project, that communication is essential, and any miscommunication can lead to lost time and multiple people implementing or developing the same feature. I also learned that with a lead role, you are expected to be responsible for the project; when something isn’t functional, when the ordinary workday is over, you have to work until it’s acceptable. I learned a lot about what problems could happen within a larger project in regards to social issues within the group, people getting sick, etcetera. As I had the lead role, I had to step up in these situations and do more than what I, in the first place, was responsible for. I also learned a lot about how essential it is to have both in-house and external game testing throughout the game development to understand game patterns and sort out bugs and other game-related issues.
Game PageCorruption Orb
Dark Forest Level
Death VFX and Death Screen
Double Jump
Dust Fall VFX and Elevator Crystal VFX
Breakbox VFX
Lifeorb
Mountain Level
Pause Menu
Pickup Box
Treetop Level
Camera Panning to Treebeard