Curse of the Blacksmith
The result of a University project in the 2nd half of 2018, Curse of the Blacksmith is a couch co-op where players are tied together as different sentient weapons and must defeat hordes of enemies to return to normal, humanoid form. Watch out as you must master each role within the game!
Four friends - a navigator, a mage, an archer and a front-line defender - have been constantly arguing non-stop, unable to get along. Eventually the blacksmith, a being of many hidden talents, gets sick of them and curses them to forever be stuck in their respective weapons, with only the navigator maintaining his form, and stuck together in a tight radius.
As part of their punishment, their souls will rotate through the weapons so they understand each other's positions. The only way to free themselves from the curse is to take on a dungeon full of waves of enemies. Get through alive, and they may return to their original forms...
Team
Liam Dunstan - AI Programmer
Josh Hardy - Programmer, Tech Artist
Michael Nuttall - Tech Artist, UI
Trent Kosi - Artist
Burju Kocoglu - Team Manager, Producer
Code Snippets
/// <summary>
/// Movement method
/// </summary>
/// <param name="spd">Speed to move at</param>
protected void Move(float spd)
{
if (Time.time > lastRepath + repathRate && seeker.IsDone())
{
lastRepath = Time.time;
// start a new path to the player's position, call the the OnPathComplete function
// when the path has been calculated
seeker.StartPath(transform.position, player.position, OnPathComplete);
}
// check in a loop if we are close enough to the current waypoint to switch to the next one
if (path != null)
{
reachedEndOfPath = false;
// The distance to the next waypoint in the path
float distanceToWaypoint;
while (true)
{
distanceToWaypoint = Vector3.Distance(transform.position, path.vectorPath[currentWaypoint]);
if (distanceToWaypoint < nextWaypointDistance)
{
// check if there is another waypoint or if we have reached the end of the path
if (currentWaypoint + 1 < path.vectorPath.Count)
{
currentWaypoint++;
}
else
{
// set a status variable to indicate that the agent has reached the end of the path
reachedEndOfPath = true;
break;
}
}
else
{
break;
}
}
// apply current waypoint target to obtain direction
Vector3 dir = (path.vectorPath[currentWaypoint] - transform.position).normalized;
rigid.velocity = dir * spd;
}
}
Excerpt from Enemy.cs
An external package was required in order to manage pathfinding, and this is the method for using it. After a certain amount of time has passed, the AI would re-route to make the players current position its destination.
After checking if it can re-route, it then checks its own position vs the waypoint it is aiming for, and either continues towards that waypoint or changes to the next waypoint.
Finally, it uses the current waypoint target and its current position to determine direction and applies it to the RigidBody
's velocity.
void Start()
{
...
List<int> playNo = new List<int>();
// randomise player numbers
for (int h = 0; h < 4; h++)
{
int num;
// loop random range until a number that isn't currently in list is reached
do
{
num = Random.Range(1, 5);
} while (playNo.Contains(num));
playNo.Add(num);
}
// assign player numbers to non-destroying object to carry into main game
playNum.playNum = playNo;
// assign correct colours and text for each player
for (int h = 0; h < players.Length; h++)
{
PlayerMovement pm = players[h].GetComponent<PlayerMovement>();
Color tutCollColor;
pm.playerNumber = playNo[h];
tutCollColor = playerColors[playNo[h]];
switch (h)
{
case 0:
player1Text.color = tutCollColor;
player1ReadyText.text = "Player " + playNo[h] + " Is Not Ready";
break;
case 1:
player2Text.color = tutCollColor;
player2ReadyText.text = "Player " + playNo[h] + " Is Not Ready";
break;
case 2:
player3Text.color = tutCollColor;
player3ReadyText.text = "Player " + playNo[h] + " Is Not Ready";
break;
case 3:
player4Text.color = tutCollColor;
player4ReadyText.text = "Player " + playNo[h] + " Is Not Ready";
break;
}
tutCollColor.a = 0.1960784f;
//Makes sure the sprite is enabled
players[h].GetComponent<SpriteRenderer>().enabled = true;
tutColliders[h].GetComponent<SpriteRenderer>().color = tutCollColor;
players[h].GetComponent<PlayerMovement>().CorrectSpriteColours();
}
}
Excerpt from TutorialScene.cs
Despite having never done AI before, this was actually the hardest thing I did. I did this once I had finished working on the enemy AI and after we had recieved feedback that some randomisation would be nice.
The goal was to change the experience with each playthrough, since players wouldn't naturally change controllers etc. It also added a extra small layer of difficulty since players didn't always rotate the same way in each playthrough.
This snippet highlights the process of randomising the numbers and assigning them to the players. It also manages the colours and text so it all displayed correctly.
The difficult part was sending the numbers over to the main game (since this all occurrs in the tutorial) and then ensuring the colours cycled correctly, since the first implementation of the cycle simply cycled in numerical order
void Update()
{
...
// Squirm
if (burning)
{
rigid.velocity = Random.insideUnitCircle.normalized * burningScatterSpeed;
}
// Pause
else if (Random.value < pauseChance && stopCooldown <= 0 && speed == normalSpeed)
{
StartCoroutine(RandomStop(Random.Range(pauseMinTime, pauseMaxTime)));
}
// Pounce
else if (!pouncing && pouCool <= 0 && Random.value < pounceChance && Vector2.Distance(transform.position, player.position) < distance && speed == normalSpeed)
{
StartCoroutine(Pounce(Random.Range(minPounceSpeed, maxPounceSpeed), Random.Range(pauseMinTime, pauseMaxTime)));
}
// Regular movement
else if (Vector2.Distance(transform.position, player.position) > 0.5 && GetComponent<Rigidbody2D>().velocity.magnitude < speed)
{
Move(speed);
}
...
}
Excerpt from Zorbling.cs
A small example of how the Zorblings (little bug enemies) did their thing.
It'd first make sure it wasn't burning before anything else happens, then it uses randomisation to determine whether it should pause briefly, pounce towards the player, or just move regularly.