,

Wiz-Duels

This project was intended to digitally replicate a simple fictional children’s board game. The original premise was dreamt up in less than 5 minutes and remained almost entirely unchanged through playtesting. However, the implementation of the concept in code was trickier than I anticipated, and development wound up being put to the side as I had to focus on college work.

Concept/Rules

The game is straightforward. Two players sit on opposite sides of a play board, each with 5 dice and a set of health trackers. The dice can have any even number of sides, all that matters is that the result is binary. For my design, I chose 4-sided dice.

The play board has 10 spaces on either side. 5 ‘attack’ spaces are positioned close to the player, and 5 ‘defense’ spaces are positioned away from the player. For my design, attack spaces were red, and defense spaces were blue. There is a dividing board which prevents the players from seeing the other’s spaces.

On a turn, each player rolls their dice and determines one of two outcomes for each die rolled based on which face it landed on, either attack or defense. Die with the attack result can be placed on any attack space on the board, and the same goes for defense results. Once both players had placed their dice down, the dividing board is removed and the result is determined.

For each attack die on the board, an attack is attempted against the other player. If the other player has a defense die positioned in the same column as the attack die, the attack is blocked. If not, the other player removes one health tracker. Then the board is replaced and a new turn begins. This continues until at least one player has lost all their health trackers.

Dice Interaction

The dice were quite fun to get rolling properly. Firstly, the dice are set up with trigger detectors on each side to figure out the orientation.

Second, a spawner creates five copies of the die at the same time, and the collision solver forces the overlapping colliders to spread out and bounce off invisible walls. Once the dice are stationary, a check is made on which trigger is active. If there is no active trigger, a bit of force sends the die tumbling again.

Once a die has a definite outcome, it can be clicked on and dragged onto the board.

Code Snippet

The die runs through all of the attached triggers, and the one that is being triggered determines which side is up, then locks the rotation.

switch (CheckSide())
{
     case 0:
     case 1:
     {
         isSettled = true;
         Debug.Log("Die result: Blue");
         upSide = false;
         rb.constraints = RigidbodyConstraints.FreezeRotation;
         break;
     }
     case 2:
     case 3:
     {
         isSettled = true;
         Debug.Log("Die result: Red");
         upSide = true;
         rb.constraints = RigidbodyConstraints.FreezeRotation;
         break;
     }
     default: Jostle(); break;
}
  

    //Goes through the list of triggers, checking which of them are in contact.
    //When it first encounters an active trigger, it registers the side and continues
    //If it encounters a second active trigger, it returns from the function with a value of -1
private int CheckSide()
{
    int side = -1;
    int position = 0;
    foreach (TriggerNotifier t in triggerList)
    {
        if (t.isTouching && side == -1)
        {
            side = position;
        }
        else if (t.isTouching && side != -1)
        {
            return -1;
        }
        position++;
    }
    return side;
}

    //Adds force to the rigidbody
private void Jostle()
{
    rb.AddRelativeForce(Vector3.up * 300);
}
Game Logic

The logic is very simple. A board manager script runs through each of the attack spaces, checking if they’ve been activated (by having a die placed on them). If true, it checks the opposite defense spot. If there’s no defender, it ticks down the opponent’s health. Else, nothing happens.

Code Snippet

To make the board manager able to run through the spots, I needed a custom Enumerator.

[Serializable]
    public class Row : IEnumerator, IEnumerable
    {
        public int position = -1;
        [SerializeField]
        public List<DiceSpot> diceSpots;

        public IEnumerator GetEnumerator()
        {
            return (IEnumerator)this;
        }

        public bool MoveNext()
        {
            position++;
            if (position >= diceSpots.Count) { Reset(); return false; }
            return (position < diceSpots.Count);
        }

        public void Reset()
        {
            position = -1;
        }

        public object Current
        {
            get { return diceSpots[position]; }
        }

        public DiceSpot GetSpot(int index)
        {
            return diceSpots[index];
        }
    }

    [Header("Player 1")]
    public int p1Health;
    [SerializeField]
    public List<Row> p1Rows;

    [Header("Player 2")]
    public int p2Health;
    [SerializeField]
    public List<Row> p2Rows;

int position = 0;
        DiceSpot temp;
        foreach (DiceSpot spot in p1Rows[0])
        {
            if (spot.isActive)
            {
                temp = p2Rows[1].GetSpot(4 - position);
                if (!temp.isActive)
                {
                    Debug.Log("Player 2 took damage from " + spot + " because " + temp + "was inactive");
                    spot.gameObject.GetComponent<DieSpotMarker>().AnimateOffense(16f, 3f);
                    p2Health--;
                }
                else
                {
                    spot.gameObject.GetComponent<DieSpotMarker>().AnimateOffense(12f, 3f);
                    Debug.Log("Player 2 was defended by" + temp); 
                }
            }
            else
            {
                Debug.Log(spot + " is not active");
            }
            position++;
        }

Leave a comment