Building a fighting circle

This recipe is based on the Kung-Fu Circle algorithm devised for the game, Kingdoms of Amalur: Reckoning. Its purpose is to offer an intelligent way for enemies to approach a given player and set attacks on it. It is very similar to the formation recipe, but it uses a stage manager that handles approach and attack permissions based on enemy weights and attack weights. It is also implemented so that the manager has the capability to handle a list of fighting circles; this is especially aimed at multiplayer games.

Getting ready

Before implementing the fighting circle algorithm, it is important to create some components that accompany the technique. First, the Attack class is a pseudo-abstract class for creating general-purpose attacks for each enemy, and it works as a template for our custom attacks in our game. Second, we need the Enemy class, which is the holder of the enemy's logic and requests. As we will see, the Enemy class holds a list of the different attack components found in the game object.

The code for the Attack class is as follows:

using UnityEngine;
using System.Collections;

public class Attack : MonoBehaviour
{
    public int weight;

    public virtual IEnumerator Execute()
    {
        // your attack behaviour here
        yield break;
    }
}

The steps to build the Enemy component are as follows:

  1. Create the Enemy class:
    using UnityEngine;
    using System.Collections;
    
    public class Enemy : MonoBehaviour
    {
        public StageManager stageManager;
        public int slotWeight;
        [HideInInspector]
        public int circleId = -1;
        [HideInInspector]
        public bool isAssigned;
        [HideInInspector]
        public bool isAttacking;
        [HideInInspector]
        public Attack[] attackList;
    }
  2. Implement the Start function:
    void Start()
    {
        attackList = gameObject.GetComponents<Attack>();
    }
  3. Implement the function for assigning a target fighting circle:
    public void SetCircle(GameObject circleObj = null)
    {
        int id = -1;
        if (circleObj == null)
        {
            Vector3 position = transform.position;
            id = stageManager.GetClosestCircle(position);
        }
        else
        {
            FightingCircle fc;
            fc = circleObj.GetComponent<FightingCircle>();
            if (fc != null)
                id = fc.gameObject.GetInstanceID();
        }
        circleId = id;
    }
  4. Define the function for requesting a slot to the manager:
    public bool RequestSlot()
    {
        isAssigned = stageManager.GrantSlot(circleId, this);
        return isAssigned;
    }
  5. Define the function for releasing the slot from the manager:
    public void ReleaseSlot()
    {
        stageManager.ReleaseSlot(circleId, this);
        isAssigned = false;
        circleId = -1;
    }
  6. Implement the function for requesting an attack from the list (the order is the same from the Inspector):
    public bool RequestAttack(int id)
    {
        return stageManager.GrantAttack(circleId, attackList[id]);
    }
  7. Define the virtual function for the attack behavior:
    public virtual IEnumerator Attack()
    {
        // TODO
        // your attack behaviour here
        yield break;
    }

How to do it…

Now, we implement the FightingCircle and StageManager classes

  1. Create the FightingCircle class along with its member variables:
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    public class FightingCircle : MonoBehaviour
    {
        public int slotCapacity;
        public int attackCapacity;
        public float attackRadius;
        public GameObject player;
        [HideInInspector]
        public int slotsAvailable;
        [HideInInspector]
        public int attackAvailable;
        [HideInInspector]
        public List<GameObject> enemyList;
        [HideInInspector]
        public Dictionary<int, Vector3> posDict;
    }
  2. Implement the Awake function for initialization:
    void Awake()
    {
        slotsAvailable = slotCapacity;
        attackAvailable = attackCapacity;
        enemyList = new List<GameObject>();
        posDict = new Dictionary<int, Vector3>();
        if (player == null)
            player = gameObject;
    }
  3. Define the Update function so that the slots' positions get updated:
    void Update()
    {
        if (enemyList.Count == 0)
            return;
        Vector3 anchor = player.transform.position;
        int i;
        for (i = 0; i < enemyList.Count; i++)
        {
            Vector3 position = anchor;
            Vector3 slotPos = GetSlotLocation(i);
            int enemyId = enemyList[i].GetInstanceID();
            position += player.transform.TransformDirection(slotPos);
            posDict[enemyId] = position;
        }
    }
  4. Implement the function for adding enemies to the circle:
    public bool AddEnemy(GameObject enemyObj)
    {
        Enemy enemy = enemyObj.GetComponent<Enemy>();
        int enemyId = enemyObj.GetInstanceID();
        if (slotsAvailable < enemy.slotWeight)
            return false;
        enemyList.Add(enemyObj);
        posDict.Add(enemyId, Vector3.zero);
        slotsAvailable -= enemy.slotWeight;
        return true;
    }
  5. Implement the function for removing enemies from the circle:
    public bool RemoveEnemy(GameObject enemyObj)
    {
        bool isRemoved = enemyList.Remove(enemyObj);
        if (isRemoved)
        {
            int enemyId = enemyObj.GetInstanceID();
            posDict.Remove(enemyId);
            Enemy enemy = enemyObj.GetComponent<Enemy>();
            slotsAvailable += enemy.slotWeight;
        }
        return isRemoved;
    }
  6. Implement the function for swapping enemy positions in the circle:
    public void SwapEnemies(GameObject enemyObjA, GameObject enemyObjB)
    {
        int indexA = enemyList.IndexOf(enemyObjA);
        int indexB = enemyList.IndexOf(enemyObjB);
        if (indexA != -1 && indexB != -1)
        {
            enemyList[indexB] = enemyObjA;
            enemyList[indexA] = enemyObjB;
        }
    }
  7. Define the function for getting an enemy's spatial position according to the circle:
    public Vector3? GetPositions(GameObject enemyObj)
    {
        int enemyId = enemyObj.GetInstanceID();
        if (!posDict.ContainsKey(enemyId))
            return null;
        return posDict[enemyId];
    }
  8. Implement the function for computing the spatial location of a slot:
    private Vector3 GetSlotLocation(int slot)
    {
        Vector3 location = new Vector3();
        float degrees = 360f / enemyList.Count;
        degrees *= (float)slot;
        location.x = Mathf.Cos(Mathf.Deg2Rad * degrees);
        location.x *= attackRadius;
        location.z = Mathf.Cos(Mathf.Deg2Rad * degrees);
        location.z *= attackRadius;
        return location;
    }
  9. Implement the function for virtually adding attacks to the circle:
    public bool AddAttack(int weight)
    {
        if (attackAvailable - weight < 0)
            return false;
        attackAvailable -= weight;
        return true;
    }
  10. Define the function for virtually releasing the attacks from the circle:
    public void ResetAttack()
    {
        attackAvailable = attackCapacity;
    }
  11. Now, create the StageManager class:
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    public class StageManager : MonoBehaviour
    {
        public List<FightingCircle> circleList;
        private Dictionary<int, FightingCircle> circleDic;
        private Dictionary<int, List<Attack>> attackRqsts;
    }
  12. Implement the Awake function for initialization:
    void Awake()
    {
        circleList = new List<FightingCircle>();
        circleDic = new Dictionary<int, FightingCircle>();
        attackRqsts = new Dictionary<int, List<Attack>>();
        foreach(FightingCircle fc in circleList)
        {
            AddCircle(fc);
        }
    }
  13. Create the function for adding circles to the manager:
    public void AddCircle(FightingCircle circle)
    {
        if (!circleList.Contains(circle))
            return;
        circleList.Add(circle);
        int objId = circle.gameObject.GetInstanceID();
        circleDic.Add(objId, circle);
        attackRqsts.Add(objId, new List<Attack>());
    }
  14. Also, create the function for removing circles from the manager:
    public void RemoveCircle(FightingCircle circle)
    {
        bool isRemoved = circleList.Remove(circle);
        if (!isRemoved)
            return;
        int objId = circle.gameObject.GetInstanceID();
        circleDic.Remove(objId);
        attackRqsts[objId].Clear();
        attackRqsts.Remove(objId);
    }
  15. Define the function for getting the closest circle, if given a position:
    public int GetClosestCircle(Vector3 position)
    {
        FightingCircle circle = null;
        float minDist = Mathf.Infinity;
        foreach(FightingCircle c in circleList)
        {
            Vector3 circlePos = c.transform.position;
            float dist = Vector3.Distance(position, circlePos);
            if (dist < minDist)
            {
                minDist = dist;
                circle = c;
            }
        }
        return circle.gameObject.GetInstanceID();
    }
  16. Define the function for granting an enemy a slot in a given circle:
    public bool GrantSlot(int circleId, Enemy enemy)
    {
        return circleDic[circleId].AddEnemy(enemy.gameObject);
    }
  17. Implement the function for releasing an enemy from a given circle ID:
    public void ReleaseSlot(int circleId, Enemy enemy)
    {
        circleDic[circleId].RemoveEnemy(enemy.gameObject);
    }
  18. Define the function for granting attack permissions and adding them to the manager:
    public bool GrantAttack(int circleId, Attack attack)
    {
        bool answer = circleDic[circleId].AddAttack(attack.weight);
        attackRqsts[circleId].Add(attack);
        return answer;
    }
  19. Step:
    public IEnumerator ExecuteAtacks()
    {
        foreach (int circle in attackRqsts.Keys)
        {
            List<Attack> attacks = attackRqsts[circle];
            foreach (Attack a in attacks)
                yield return a.Execute();
        }
        foreach (FightingCircle fc in circleList)
            fc.ResetAttack();
    }

How it works…

The Attack and Enemy classes control the behaviors when needed, so the Enemy class can be called from another component in the game object. The FightingCircle class is very similar to FormationPattern, in that it computes the target positions for a given enemy. It just does it in a slightly different way. Finally, the StageManager grants all the necessary permissions for assigning and releasing enemy and attack slots for each circle.

There is more…

It is worth noting that the fighting circle can be added as a component of a game object that works as the target player itself, or a different empty object that holds a reference to the player's game object.

Also, you could move the functions for granting and executing attacks to the fighting circle. We wanted to keep them in the manager so that attack executions are centralized, and the circles just handle target positions, just like formations.

See also

  • Refer to the Handling formations recipe
  • For further information on the Kung-Fu Circle algorithm, please refer to the book, Game AI Pro, by Steve Rabin
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.156.251