About Me | Projects | Contact Me
About Me
Hi. I’m Jack, currently 31, and from Pallet Town the county of Hampshire in England. I’m a long-term fan of Pokémon and have expertise in some of the competitive elements of the games. My never-ending ambition to grow as a competitor in Pokémon games, among other games, such as in the Mario and Zelda franchises, has led me to develop an understanding of the inner-workings of games, as well as a demand to optimise my performance.
13 years ago I began learning to script in a Visual-Basic-like language when I had ambitions to grow my Pokémon community. From that day on, I’ve immersed myself in the coding world from time to time, and slowly built on my knowledge—mainly by trying to master JavaScript, but also by learning some PHP.
Fastforwarding to today, I’ve taken my first steps into the game development world, by making a Unity game in C#. Below, I will demonstrate to you my programming experiences, which I wish to have many more of in the process of attaining a degree.
Final Project: My Color Switch Game
Project Type: University Solo Project
Game Engine: Unity
Language(s): C#
Click below to view explanations of systems:
Attack System

Figure 2: Attack System Demonstration
The attacking system consists of shooting with different coloured bullets that correspond to the player or enemy. Green are the slowest and least damaging, while blue are the fastest and most damaging. The standard shooting comes out of one vertex, while the special shooting, which you can see coming out of the player in the highermost image of the page, comes out of all the vertices of the player. In the code, there’s a fire method that takes multiple parameters and some setup before calling it that happens for both player and enemy.
Fire Method
The fire method takes 7 arguments: side determines whether it’s a player or enemy, and the other 6 are properties related to the bullet that spawns.
bulletSpeed is one of the components that makes up the bullet velocity; the other component (the direction) is a normal vector (transform.right) that is rotated by rotationModifier and a further 90 degrees. rotationModifier similarly helps determine the rotation of the bullet when it’s instantiated, along with positionModifier that helps determine its position.
bulletColor is responsible for recolouring enemy bullets (the player already does this when changing colour). The last two parameters of expiry and scale, are related to the expiry time of the bullet, the scale just being a factor of how long a bullet takes to be destroyed.
Lastly, it checks to see if the player is not white. If it is, it makes the player flicker, which symbolises the firing doesn’t work. It also checks that it’s either not an enemy (is a player) or the waiting time has been reached or exceeded. The wait being either 1-3 seconds, since the waitPoint is set to 3 and it generates a wait between 0 and 2 seconds after every usage.
public void Fire(string side, float bulletSpeed, Color bulletColor, Vector3 positionModifier, Quaternion rotationModifier, float expiry, float scale)
{
if (playerScript.activeColor != playerScript.colorWhite && (side != "Enemy" || waitCount >= waitPoint))
{
// Launch bullet.
Rigidbody2D bulletClone = Instantiate(bullet, transform.position + positionModifier, rotationModifier * Quaternion.Euler(0, 0, 90) * transform.rotation);
bulletClone.velocity = rotationModifier * Quaternion.Euler(0,0,90) * transform.right * bulletSpeed;
bulletClone.GetComponent().color = bulletColor;
// Destroy the bullet after a fixed amount of time.
Destroy(bulletClone.gameObject, expiry*Mathf.Sqrt(scale));
// Reset enemy wait.
waitTime = Random.Range(0f, 2f);
waitCount = 0;
}
else if (side == "Player")
{
StartCoroutine(playerScript.Flicker(playerScript.gameObject));
}
}
Player Fire
The trigger and preparation for the player firing is fairly simple, upon pressing Space (not holding it down), the player will shoot bullets 5 times its current run speed and they will expire in 0.3 seconds. The "Player" argument just specifies it's not an enemy, the colour argument just specifies the bullet's colour (as mentioned above, they are already the correct colour for the player). The rest of the arguments are just identity elements, because no modifiers are needed.
It gets more complex with the special 'E' attack. If the player is blue, the player can fire bullets from all its vertices. Due to the for loop, it fires 5 bullets at a time. The parts to note are that its position formula results in (0, 1) and the AngleAxis function is used to rotate the bullets anti-clockwise by 90 degrees each time compared to the last one.
if (gameObject.CompareTag("Player"))
{
if (Input.GetKeyDown(KeyCode.Space))
{
Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, Vector2.zero, Quaternion.identity, 0.3f, 1);
}
else if (Input.GetKeyDown(KeyCode.E) && playerScript.activeColor == playerScript.colorBlue)
{
for (int i = 0; i < 5; i++)
{
Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, transform.rotation * Vector2.up, Quaternion.identity, 0.5f, 1);
Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, transform.rotation * Vector2.right, Quaternion.AngleAxis(-90, Vector3.forward), 0.5f, 1);
Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, transform.rotation * Vector2.down, Quaternion.AngleAxis(180, Vector3.forward), 0.5f, 1);
Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, transform.rotation * Vector2.left, Quaternion.AngleAxis(90, Vector3.forward), 0.5f, 1);
}
}
}
Enemy Fire
For enemy firing, the bullets are turned more transparent. Because of the dark grey floor, this means they appear darker. The distance an enemy is away from the player is checked; if the enemy is too far, they won't fire. It's scaled so that bigger enemies can fire from longer distances.
Next, the wait time is incremented and the arctangent2 of the direction is calculated, which gives us the angle between the enemy and the origin. That angle is then constrained to the set of π/4 radians increments, corresponding to the octagon's vertices for the enemy. From there, the positionModifier and rotationModifier are calculated, the latter using the LookRotation function.
If the scale is less than 5, i.e. not the final boss, those arguments are passed into the Fire function, if not it is further modified. For the final boss, 5 shots are fired simultaneously and while the boss is blocking, it can still fire, but the speed of the bullets is decreased, so the bullets will have less range before they decay.
if (gameObject.CompareTag("Enemy") && bullet != null)
{
SpriteRenderer rendereb = gameObject.GetComponent();
Color bulletColor = rendereb.color;
bulletColor.a = 0.5f;
// Assigns distance, scale and direction variables.
float distance = Vector2.Distance(transform.position, playerScript.transform.position);
float scale = transform.localScale.x;
Vector2 direction = (playerScript.transform.position) - this.transform.position;
if (distance < scale * 6)
{
// Increments the enemy wait time.
waitTime += Time.deltaTime;
waitCount = Mathf.RoundToInt(waitTime);
// Constrains the angle to pi/4 radians (i.e. to the 8 vertices of an octagon) and positions/rotates the bullets accordingly.
float angle = Mathf.Atan2(direction.y, direction.x);
float constrainedAngle = Mathf.RoundToInt(angle / (Mathf.PI / 4f)) * (Mathf.PI / 4f);
Vector3 position = new Vector3(Mathf.Cos(constrainedAngle), Mathf.Sin(constrainedAngle), 0);
Quaternion rotation = Quaternion.LookRotation(Vector3.forward, position);
if (scale < 5)
{
Fire("Enemy", enemyScript.runSpeed * 5, bulletColor, position, rotation, 0.5f, scale);
}
else
{
int speedModifier;
speedModifier = blockScript.blockOn ? 5 : 10;
for (int i = 0; i < 5; i++)
{
Fire("Enemy", enemyScript.runSpeed * speedModifier, bulletColor, position, rotation, 0.5f, 1);
waitCount = waitPoint;
}
waitTime = Random.Range(0f, 2f);
waitCount = 0;
}
}
}
Health System

Figure 3: Health System Demonstration
The health system for my game consists of 5 hearts; depending on the strength of the enemy, a player can lose 1, 2 or 3 hearts. In the case that they would be left with 1 or less hearts, they are forced into the white mode, with 1 heart, where all firing stops. There also health consumables (denoted H) which restore a number of hearts depending on their size. The 3 main components to the coding of this: are the player receiving damage, the player restoring health, and the updating of the overlay displaying the health.
Receiving Damage
In the player damage function, if the deathCounter, which counts up towards death (0 health), becomes 5 or more, it triggers game over or gives a health warning and a forcing into white mode at 4. This behaviour is executed in the switch statement.
Damage always gets incremented by 1 initially; if there's adequate health left after this (2 or 3), it's boosted further if it's hit by a red or blue bullet (1 or 2 damage).
It should have probably been changed to increment once for red or blue, and then a final time for blue, so that when health is 3 and it gets hits by a blue bullet, health goes down to 1 instead of 2. But since the main blue enemy is the boss battle, the unintended extra protection is probably useful when you first encounter it.
Otherwise, the damage is limited to 5 if it goes beyond 5 and the player flickers when any damage is dealt besides when triggering game over.
void PlayerDamage(GameObject obj)
{
if (!pauseDamage && !blockScript.blockOn)
{
if ((obj.CompareTag("Bullet") && obj.layer == LayerMask.NameToLayer("Enemy Bullet")) || obj.CompareTag("Enemy"))
{
deathCounter++;
SpriteRenderer rendereb = obj.GetComponent();
Color enemyBulletColor = rendereb.color;
if (enemyBulletColor.r == 1 && deathCounter < 4)
{
deathCounter++;
}
if (enemyBulletColor.b == 1 && deathCounter 5)
{
deathCounter = 5;
}
damagedCount++;
UpdateOverlay();
if (obj != null)
{
switch (deathCounter)
{
case 4:
FlickerOrange();
int attempts = 0;
while (activeColor != colorWhite && attempts < 5)
{
ChangeColor();
attempts++;
}
break;
case 5:
// Game over event.
gameObject.SetActive(false);
gameManager.GameOver();
break;
default:
FlickerOrange();
break;
}
}
}
}
}
Restoring Health
Regaining health is very straightforward. If a health consumable collision is detected, and health is not already at the max (deathCounter != 0), it destroys the health consumable and restores health relative to the size of the it. If health restored would exceed 5 (the maximum), it sets it to 5 (0 for the deathCounter).
if ((other.gameObject.name.Substring(0, 6) == "Health" || other.gameObject.name == "Health(Clone)") & deathCounter != 0)
{
Destroy(other.gameObject);
deathCounter -= 1 * Mathf.RoundToInt(other.gameObject.transform.localScale.x);
deathCounter = deathCounter < 0 ? 0 : deathCounter;
UpdateOverlay();
}
Overlay Updating
For updating the overlay, a health variable is set as the complement of the deathCounter. A red heart is added for every integer of health, while a grey heart is added for every missing heart, using standard for loops.
int health = 5 - deathCounter;
for (int i = 0; i < health; i++)
{
healthText += "♥";
}
for (int j = 0; j < deathCounter; j++)
{
healthText += "♥";
}
Teleport System

Figure 4: Teleport System Demonstration
For teleporting, the player can teleport to any circle that matches their current colour if they possess the ability for that colour. They can also teleport to checkpoints that are gradually revealed as they progress through the game, and even teleport their moving bullets.
Teleporting
The teleporter code performs several checks before teleporting the player to the correct location. Upon pressing down T, the game loops through all teleporters, then checks if the player and teleporter colour match and that the player possesses the teleport ability for that colour. The player's active colour is stored, while the teleporters are assigned different layers depending on their colours, and there's a dictionary that stores 6 strings and 6 true or false values representing whether the player has collected the relevant ability. Next, the teleporter is checked if its centre is in camera-view. If the teleporter is visible, it becomes the new co-ordinates to teleport to, and is prepared for being made invisible. If it's the invisible one, it's prepared to become visible. Finally, once that is confirmed, the player teleports and the visibility between the pair of teleporters is switched.
if (Input.GetKeyDown(KeyCode.T))
{
GameObject[] teleporters = GameObject.FindGameObjectsWithTag("Teleporter");
SpriteRenderer spriteRenderer1 = null;
SpriteRenderer spriteRenderer2 = null;
Vector3 teleporterLocation = new Vector3(0,0,0);
foreach (GameObject teleporter in teleporters)
{
bool whiteCheck = player.layer == LayerMask.NameToLayer("IgnorePlayer") && teleporter.layer == LayerMask.NameToLayer("White Teleporter");
bool greenCheck = playerScript.activeColor == playerScript.colorGreen && teleporter.layer == LayerMask.NameToLayer("Green Teleporter") && playerScript.hasAbility["GreenTeleport"];
bool redCheck = playerScript.activeColor == playerScript.colorRed && teleporter.layer == LayerMask.NameToLayer("Red Teleporter") && playerScript.hasAbility["RedTeleport"];
bool blueCheck = playerScript.activeColor == playerScript.colorBlue && teleporter.layer == LayerMask.NameToLayer("Blue Teleporter") && playerScript.hasAbility["BlueTeleport"];
if (whiteCheck || greenCheck || redCheck || blueCheck)
{
Vector3 targetPosition = mainCamera.WorldToViewportPoint(teleporter.transform.position);
if (targetPosition.x >= 0 && targetPosition.x = 0 && targetPosition.y <= 1)
{
SpriteRenderer spriteRenderer = teleporter.GetComponent();
if (spriteRenderer.enabled)
{
teleporterLocation = teleporter.transform.position;
spriteRenderer1 = spriteRenderer;
}
else
{
spriteRenderer2 = spriteRenderer;
}
}
}
}
if (spriteRenderer1 != null && spriteRenderer2 != null && !teleporterLocation.Equals(default))
{
transform.position = teleporterLocation;
spriteRenderer1.enabled = false;
spriteRenderer2.enabled = true;
}
}
Checkpoints
There are 3 checkpoints in this game, one at the start of the red enemy layer, one at the start of the blue enemy layer, and one before the final boss. For this example, I'm just showing the code for the first checkpoint.
At the start, the time is stored. For any frame where less than 1 second has passed, the player can press the alphanumeric 2 key and be teleported to the first checkpoint, holding the abilities corresponding to what is necessary to get there. The teleporters visible behind the player are also swapped, so the visible ones are on the other side to the player, the same as if they'd been used.
P is the restart button, which reloads the scene, erasing any points. The expected usage is you press P and 2 in conjunction and that restarts you at a checkpoint with no points.
void Start()
{
player = GameObject.Find("Player");
whiteTeleporter1 = GameObject.Find("Teleporter 3");
whiteTeleporter2 = GameObject.Find("Teleporter 4");
playerScript = player.GetComponent();
restartTime = Time.time;
}
// Restart button
void Update()
{
if (Input.GetKey(KeyCode.Alpha2) && Time.time < (restartTime + 1f))
{
player.transform.position = new Vector2(-16f, 25f);
playerScript.colorChange = new Dictionary
{
{ playerScript.colorWhite, playerScript.colorGreen },
{ playerScript.colorGreen, playerScript.colorWhite },
};
playerScript.hasAbility["GreenWall"] = true;
playerScript.hasAbility["GreenTeleport"] = true;
playerScript.greenTeleporter1.SetActive(true);
playerScript.greenTeleporter2.SetActive(true);
playerScript.greenTeleporter1.GetComponent().enabled = true;
playerScript.greenTeleporter2.GetComponent().enabled = false;
whiteTeleporter1.GetComponent().enabled = true;
whiteTeleporter2.GetComponent().enabled = false;
playerScript.UpdateOverlay();
}
}
else if (Input.GetKeyDown(KeyCode.P))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
Teleport Shooting
For teleport shooting, the fire method in the attack script is used. The player's position is subtracted from the teleporterLocation, to negate it, since it is added within the sum of the bullet's position in the fire method.
A check for whether space bar is held down is used. If it is, the teleport shooting occurs; if not, the player teleports.
if (Input.GetKey(KeyCode.Space))
{
attackScript.Fire("Player", playerScript.runSpeed * 5, playerScript.activeColor, teleporterLocation - transform.position, Quaternion.identity, 0.3f, 1);
return;
}
else
{
transform.position = teleporterLocation;
}
For other in-depth information on my project, take a look at my devlogs: https://jakilutra.com/category/game-development/?order=asc
Other Projects
And for other examples of my other work, check out my Github Profile here: https://github.com/Jakilutra
Contact Me
E-mail: jakilutra@gmail.com | Discord: Lutra#3902 | YouTube: jakilutra
Message me on any of these platforms.
I have a very good record of replying and responding quickly.