Weapon System Breakdown

Overview

The weapon system for my game utilises a third-party behaviour tree system developed by TheKiwiCoder. To build upon this system, I created many custom nodes in C# to expand the general functionality. I also created some custom nodes specifically for the weapon system.

Structure

The functionality of each weapon is defined by its behaviour tree. These trees have a clear structure, starting at the root and executing nodes top to bottom. The different node types are as follows:

  • Root node: the start point / first node to run, it simply runs its child node.
  • Sequencer node: has multiple child nodes and runs them from left to right until one of them fails.
  • Selector node: has multiple child nodes and runs them from left to right until one of them succeeds.
  • Repeat node: runs their single child node once per frame forever or until it fails or succeeds depending on its configuration.
  • Action node (green): has no child nodes and usually perform some simple task. They can also be used as predicates to check conditions and succeed or fail accordingly.
  • Subtree node: an abstraction used to declutter the diagrams - the behaviour trees can get quite large for more complex behaviours.
As an example, we will look at the game's revolver weapon. Below is a diagram illustrating the overall structure for this weapon's behaviour tree, with some subtrees abstracted for conciseness.

The tree starts off with some setup which consists of initialising variables and event listeners. After this, the repeat node runs its subtree every frame. In this case, it waits for the input button to be pressed so that it can start charging an attack. When the input button is released it stops charging and either performs one of two attacks (quick/charged) based on how long it was charged for.

Revolver Behaviour Tree Structure Diagram

High-level structure of the revolver behaviour tree.

To better illustrate how these trees function and how they are structured, below is a diagram of the subtree which handles the charged attack. This subtree mainly consists of action nodes in sequence so that each part is executed one by one, handling hit detection, sound effects, visual effects, a cooldown, etc.

Charged Shot Subtree Diagram

Detailed structure of the charged shot subtree.

Custom Scripts & Nodes

The original behaviour tree system was quite basic, and I often required functionality that it lacked. Since then, it seems the creator has made updates to it, but by that time I had already extended the functionality myself. For example, each behaviour tree has a blackboard which can be used to store variables of any type, associating them with a string key which can be used to reference them. Although the provided code includes a blackboard, it doesn't fully support reading and writing these variables at runtime. I added this functionality along with custom nodes to allow for reading and writing these variables inside behaviour trees.

Blackboard
Stores variables for behaviour trees to read from and write to.
using System;
using System.Collections.Generic;

namespace TheKiwiCoder
{
    [System.Serializable]
    public class Blackboard
    {
        private Dictionary<string, object> blackboardData = new Dictionary<string, object>();

        //Generic method for setting a value of any type in the blackboard
        public void SetValue<T>(string key, T value)
        {
            if (key == null) throw new ArgumentNullException(nameof(key));

            blackboardData[key] = value;
        }

        //Generic method for retrieving a value of any type from the blackboard
        public bool TryGetValue<T>(string key, out T value)
        {
            if (key == null) throw new ArgumentNullException(nameof(key));

            bool foundValue = blackboardData.TryGetValue(key, out object objectValue);
            if (foundValue && objectValue is T)
            {
                value = (T)objectValue;
                return true;
            }
            else
            {
                value = default;
                return false;
            }
        }

        public bool ContainsKeyValueOfType<T>(string key)
        {
            if (key == null) throw new ArgumentNullException(nameof(key));

            bool foundValue = blackboardData.TryGetValue(key, out object objectValue);
            return foundValue && objectValue is T;
        }
    }
}

Timers are frequently used throughout the weapon system for tasks such as cooldowns and charging attacks, as previously shown. Below are some of the scripts that I created to implement timers in behaviour trees.

Timer Scripts
A collection of scripts related to timers in behaviour trees.
TreeTimer
The main timer class used by the previously listed timer-related nodes.
public class TreeTimer
{
    private double endTime = 0.0;
    private double timeRemaining = 0.0;
    private bool isPaused = false;
    private float duration = 0.0f;

    public void Restart(float newDuration)
    {
        duration = newDuration;
        endTime = GameManager.instance.unadjustedRoundTime + (double)duration;
    }

    public bool IsComplete()
    {
        return GameManager.instance.unadjustedRoundTime >= endTime && !isPaused;
    }

    public void SetPaused(bool pause)
    {
        if (pause == isPaused) return;

        if (pause)
        {
            isPaused = true;
            timeRemaining = endTime - GameManager.instance.unadjustedRoundTime;
        }
        else
        {
            isPaused = false;
            endTime = GameManager.instance.unadjustedRoundTime + timeRemaining;
        }
    }

    public float GetRemainingTime(bool normalise = false, bool reverse = false)
    {
        float unnormalisedResult = (float)(isPaused ? timeRemaining : endTime - GameManager.instance.unadjustedRoundTime);
        if (reverse) unnormalisedResult = duration - unnormalisedResult;
        return normalise ? (unnormalisedResult / duration) : unnormalisedResult;
    }
}
RestartTimer
A behaviour tree node that starts a timer with a given duration.
using UnityEngine;
using TheKiwiCoder;
using static VariableValueStructs;

[NodeMenuAttribute("Timer")]
public class RestartTimer : ActionNode
{
    [SerializeField] private string timerName;
    [SerializeField] private FloatValue duration;

    protected override void OnStart() {} //Unused

    protected override void OnStop() {} //Unused

    protected override State OnUpdate()
    {
        float durationValue;
        durationValue = duration.TryGetValue(varName => (blackboard.TryGetValue(varName, out float floatValue), floatValue));

        TreeTimer timer = new TreeTimer();
        blackboard.SetValue(timerName, timer); //Just overwrites previous timer object if one existed
        timer.Restart(durationValue);

        return State.Success;
    }
} 
CheckTimer
A behaviour tree node that succeeds or fails based on whether a given timer has completed yet.
using UnityEngine;
using TheKiwiCoder;

[NodeMenuAttribute("Timer")]
public class CheckTimer : ActionNode
{
    [SerializeField] private string timerName;
    [Tooltip("Whether this node should succeed if the timer has completed (instead of succeeding if it has not yet completed)")]
    [SerializeField] private bool checkComplete = true;

    protected override void OnStart() {} //Unused

    protected override void OnStop() {} //Unused

    protected override State OnUpdate()
    {
        TreeTimer timer;
        bool valueFound = blackboard.TryGetValue(timerName, out timer);
        if (valueFound)
        {
            return (timer.IsComplete() == checkComplete) ? State.Success : State.Failure;
        }
        else return State.Failure;
    }
}
SetTimerPaused
A behaviour tree node that can pause or unpause a timer.
using UnityEngine;
using TheKiwiCoder;

[NodeMenuAttribute("Timer")]
public class SetTimerPaused : ActionNode
{
    [SerializeField] private string timerName;
    [SerializeField] private bool paused;

    protected override void OnStart() {} //Unused

    protected override void OnStop() {} //Unused

    protected override State OnUpdate()
    {
        TreeTimer timer;
        bool valueFound = blackboard.TryGetValue(timerName, out timer);
        if (valueFound)
        {
            timer.SetPaused(paused);
            return State.Success;
        }
        else return State.Failure;
    }
}
RecordTimer
A behaviour tree node used to store the current progress of a timer in a blackboard variable.
using UnityEngine;
using TheKiwiCoder;

[NodeMenuAttribute("Timer")]
public class RecordTimer : ActionNode
{
    [SerializeField] private string timerName;
    [SerializeField] private string writeKey;
    [SerializeField] private bool reverse = false;
    [SerializeField] private bool normalise;

    protected override void OnStart() {} //Unused

    protected override void OnStop() {} //Unused

    protected override State OnUpdate()
    {
        TreeTimer timer;
        bool valueFound = blackboard.TryGetValue(timerName, out timer);
        if (valueFound)
        {
            
            blackboard.SetValue(writeKey, timer.GetRemainingTime(normalise, reverse));
            return State.Success;
        }
        else return State.Failure;
    }
}

This timer system is one of multiple parts that support the behaviour tree logic in this project. There is also the collection of nodes for reading and writing blackboard variables, as well as other collections such as predicate nodes that compare variables and values for branching behaviour. When combined, these parts allow for complex behaviours without much custom functionality for each new use case. The weapon system itself is mainly comprised of these generic nodes with only a few scripts for things like attack hit detection, invincibility frames, etc.