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.
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:
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; }
Start
function:void Start() { attackList = gameObject.GetComponents<Attack>(); }
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; }
public bool RequestSlot() { isAssigned = stageManager.GrantSlot(circleId, this); return isAssigned; }
public void ReleaseSlot() { stageManager.ReleaseSlot(circleId, this); isAssigned = false; circleId = -1; }
public bool RequestAttack(int id) { return stageManager.GrantAttack(circleId, attackList[id]); }
public virtual IEnumerator Attack() { // TODO // your attack behaviour here yield break; }
Now, we implement the FightingCircle
and StageManager
classes
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; }
Awake
function for initialization:void Awake() { slotsAvailable = slotCapacity; attackAvailable = attackCapacity; enemyList = new List<GameObject>(); posDict = new Dictionary<int, Vector3>(); if (player == null) player = gameObject; }
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; } }
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; }
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; }
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; } }
public Vector3? GetPositions(GameObject enemyObj) { int enemyId = enemyObj.GetInstanceID(); if (!posDict.ContainsKey(enemyId)) return null; return posDict[enemyId]; }
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; }
public bool AddAttack(int weight) { if (attackAvailable - weight < 0) return false; attackAvailable -= weight; return true; }
public void ResetAttack() { attackAvailable = attackCapacity; }
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; }
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); } }
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>()); }
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); }
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(); }
public bool GrantSlot(int circleId, Enemy enemy) { return circleDic[circleId].AddEnemy(enemy.gameObject); }
public void ReleaseSlot(int circleId, Enemy enemy) { circleDic[circleId].RemoveEnemy(enemy.gameObject); }
public bool GrantAttack(int circleId, Attack attack) { bool answer = circleDic[circleId].AddAttack(attack.weight); attackRqsts[circleId].Add(attack); return answer; }
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(); }
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.
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.
3.133.156.251