Curse of the Blacksmith

C#
Unity
⛋ 2D
Uni
11/18

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

1 / 5
2 / 5
3 / 5
4 / 5
5 / 5

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.