Unity Utility Based AI Setup
using System.Collections.Generic; | |
using UnityEngine; | |
namespace UGPXFramework { | |
public class AIController { | |
private DynamicBlackboard _blackboard = new DynamicBlackboard(); | |
private List<IAIScan> _scans = new List<IAIScan>(); | |
private List<IAIAction> _actions = new List<IAIAction>(); | |
public AIController(GameObject gameObject) { | |
// @todo(refactor) these should be dynamically loaded from data files | |
_scans.Add(new AISelfScan(_blackboard, gameObject)); | |
_scans.Add(new AINearestTargetScan(BlackboardKey.MY_TARGET, GOTag.PLAYER, 5)); | |
// @todo(refactor) these should be dynamically loaded from data files | |
_actions.Add(new AIMoveAction( | |
new[] { | |
new Vector2(0, 0), | |
new Vector2(0.8f, 0), | |
new Vector2(1, 0), | |
new Vector2(1, 1) | |
}, | |
1 | |
)); | |
_actions.Add(new AIIdleAction( | |
new[] { | |
new Vector2(0, 0), | |
new Vector2(0.8f, 0), | |
new Vector2(1, 0), | |
new Vector2(1, 1) | |
}, | |
0.5f | |
)); | |
} | |
public void RunAI() { | |
foreach (IAIScan scan in _scans) { | |
scan.Scan(_blackboard); | |
} | |
DecideActionAndRun(); | |
} | |
public void DecideActionAndRun() { | |
float currentHighScore = 0; | |
IAIAction actionToRun = null; | |
// @todo(performance) can this be better? | |
foreach (IAIAction action in _actions) { | |
float currentActionScore = action.Score(_blackboard); | |
if (currentHighScore >= currentActionScore) { | |
continue; | |
} | |
currentHighScore = currentActionScore; | |
actionToRun = action; | |
} | |
actionToRun?.Act(_blackboard); | |
} | |
} | |
} |
using UnityEngine; | |
namespace UGPXFramework { | |
public class AIControllerMB : MonoBehaviour { | |
private AIController _aiController; | |
public void Awake() { | |
_aiController = new AIController(gameObject); | |
} | |
public void Update() { | |
_aiController.RunAI(); | |
} | |
} | |
} |
using UnityEngine; | |
namespace UGPXFramework { | |
public class AIIdleAction : IAIAction { | |
public float IdleDuration { get; private set; } | |
public float IdleLeft { get; private set; } | |
public Vector2[] CurvePoints { get; private set; } | |
public AIIdleAction(Vector2[] curvePoints, float idleDuration) { | |
IdleDuration = idleDuration; | |
IdleLeft = idleDuration; | |
// @todo(error) how to handle if there are not 4 points? | |
CurvePoints = curvePoints; | |
} | |
public float Score(DynamicBlackboard blackboard) { | |
float timeSinceLastIdle = Time.time - blackboard.GetValue<float>(BlackboardKey.LAST_IDLE); | |
float timeFactor = Mathf.Clamp(timeSinceLastIdle / 5, 0, 1); | |
float score = blackboard.GetValue<AIAction>(BlackboardKey.CURRENT_AI_ACTION) == AIAction.MOVING | |
? 0 | |
: MathUtility.GetCubicBezierPoint( | |
CurvePoints[0], | |
CurvePoints[1], | |
CurvePoints[2], | |
CurvePoints[3], | |
timeFactor | |
).y; | |
return Mathf.Clamp(score, timeSinceLastIdle > 1 ? 0.1f : 0, 1); | |
} | |
public void Act(DynamicBlackboard blackboard) { | |
// @todo(performance) if setting this every frame going to be a performance issue | |
blackboard.SetValue(BlackboardKey.CURRENT_AI_ACTION, AIAction.IDLE); | |
if (IdleLeft <= 0) { | |
blackboard.RemoveValue(BlackboardKey.CURRENT_AI_ACTION); | |
blackboard.SetValue(BlackboardKey.LAST_IDLE, Time.time); | |
IdleLeft = IdleDuration; | |
return; | |
} | |
IdleLeft -= Time.deltaTime; | |
} | |
} | |
} |
using UnityEngine; | |
namespace UGPXFramework { | |
public class AIMoveAction : IAIAction { | |
public float _moveDuration; | |
public float _moveLeft; | |
public Vector2[] CurvePoints { get; private set; } | |
public AIMoveAction(Vector2[] curvePoints, float moveDuration) { | |
_moveDuration = moveDuration; | |
// @todo(error) how to handle if there are not 4 points? | |
CurvePoints = curvePoints; | |
} | |
public float Score(DynamicBlackboard blackboard) { | |
float timeFactor = Mathf.Clamp((Time.time - blackboard.GetValue<float>(BlackboardKey.LAST_IDLE)) / 5, 0, 1); | |
float score = blackboard.GetValue<AIAction>(BlackboardKey.CURRENT_AI_ACTION) == AIAction.IDLE | |
? 0 | |
: MathUtility.GetCubicBezierPoint( | |
CurvePoints[0], | |
CurvePoints[1], | |
CurvePoints[2], | |
CurvePoints[3], | |
timeFactor | |
).y; | |
; | |
return score; | |
} | |
public void Act(DynamicBlackboard blackboard) { | |
GameObject selfGO = blackboard.GetValue<GameObject>(BlackboardKey.SELF_GO); | |
CharacterMB characterMB = blackboard.GetValue<CharacterMB>(BlackboardKey.SELF_CHARACTER_MB); | |
WorldTileData selfWorldTileData = blackboard.GetValue<WorldTileData>(BlackboardKey.MY_CURRENT_WORLD_TILE_DATA); | |
Vector3 myMoveDirection = DetermineMyMoveDirection(blackboard); | |
float speed = (characterMB.StatSystem.Speed.CurrentValue * selfWorldTileData.SpeedModifier) * Time.deltaTime; | |
// @todo(performance) should not be calling getComponent | |
selfGO.GetComponent<CharacterMoverMB>().Move( | |
selfGO.transform.position + (myMoveDirection.normalized * speed) | |
); | |
_moveLeft -= Time.deltaTime; | |
if (_moveLeft <= 0) { | |
blackboard.RemoveValue(BlackboardKey.CURRENT_AI_ACTION); | |
blackboard.RemoveValue(BlackboardKey.MY_MOVE_DIRECTION); | |
} | |
} | |
private Vector3 DetermineMyMoveDirection(DynamicBlackboard blackboard) { | |
Vector3 myMoveDirection; | |
if (blackboard.HasValue(BlackboardKey.MY_MOVE_DIRECTION)) { | |
myMoveDirection = blackboard.GetValue<Vector3>(BlackboardKey.MY_MOVE_DIRECTION); | |
} else { | |
float randomRangeX = Random.Range(-1f, 1f); | |
float randomRangeY = Random.Range(-1f, 1f); | |
myMoveDirection = new Vector3(randomRangeX, randomRangeY); | |
blackboard.SetValue(BlackboardKey.MY_MOVE_DIRECTION, myMoveDirection); | |
blackboard.SetValue(BlackboardKey.CURRENT_AI_ACTION, AIAction.MOVING); | |
_moveLeft = _moveDuration; | |
UpdateFacingDirection(blackboard); | |
} | |
return myMoveDirection; | |
} | |
private void UpdateFacingDirection(DynamicBlackboard blackboard) { | |
GameObject selfGO = blackboard.GetValue<GameObject>(BlackboardKey.SELF_GO); | |
Vector3 myMoveDirection = blackboard.GetValue<Vector3>(BlackboardKey.MY_MOVE_DIRECTION); | |
VerticalDirection verticalDirection = myMoveDirection.y > 0 | |
? VerticalDirection.UP | |
: VerticalDirection.DOWN; | |
HorizontalDirection horizontalDirection = myMoveDirection.x > 0 | |
? HorizontalDirection.RIGHT | |
: HorizontalDirection.LEFT; | |
// @todo(performance) should not be calling getComponent | |
selfGO.GetComponent<CharacterMoverMB>().SetDirections(verticalDirection, horizontalDirection); | |
} | |
} | |
} |
using System.Collections.Generic; | |
using UnityEngine; | |
namespace UGPXFramework { | |
// @todo add support for selecting multiple types of targets | |
public class AINearestTargetScan : IAIScan { | |
public float _nextCheck; | |
public string Tag { get; private set; } | |
public float Range { get; private set; } | |
public BlackboardKey TargetName { get; private set; } | |
public AINearestTargetScan(BlackboardKey targetName, string tag, float range) { | |
Range = range; | |
Tag = tag; | |
TargetName = targetName; | |
} | |
public void Scan(DynamicBlackboard blackboard) { | |
if (_nextCheck <= 0) { | |
_nextCheck -= Time.deltaTime; | |
return; | |
} | |
GameObject nearestTargetGO = null; | |
float distance = Mathf.Infinity; | |
GameObject selfGO = blackboard.GetValue<GameObject>(BlackboardKey.SELF_GO); | |
Vector3 myPosition = selfGO.transform.position; | |
List<GameObject> allFoundGO = RaycastUtility.GetObjectsInRange(myPosition, Range, Tag); | |
foreach (GameObject foundGO in allFoundGO) { | |
float currentDistance = Vector2.Distance(myPosition, foundGO.transform.position); | |
if (distance < currentDistance) { | |
continue; | |
} | |
distance = currentDistance; | |
nearestTargetGO = foundGO; | |
} | |
blackboard.SetValue(TargetName, nearestTargetGO); | |
// this is a little expensive so we only call this so often since it does not need to be updated every frame | |
_nextCheck = 0.1f; | |
} | |
} | |
} |
using UnityEngine; | |
namespace UGPXFramework { | |
public class AISelfScan : IAIScan { | |
public AISelfScan(DynamicBlackboard blackboard, GameObject selfGO) { | |
blackboard.SetValue(BlackboardKey.SELF_GO, selfGO); | |
blackboard.SetValue(BlackboardKey.SELF_CHARACTER_MB, selfGO.GetComponent<CharacterMB>()); | |
} | |
public void Scan(DynamicBlackboard blackboard) { | |
GameObject selfGO = blackboard.GetValue<GameObject>(BlackboardKey.SELF_GO); | |
WorldTileData worldTileData = UniqueReferenceManager | |
.WorldManagerMB | |
.GetTile(VectorUtility.Vector3ToVector2Int(selfGO.transform.position)); | |
blackboard.SetValue(BlackboardKey.MY_CURRENT_WORLD_TILE_DATA, worldTileData); | |
} | |
} | |
} |
using System.Collections.Generic; | |
namespace UGPXFramework { | |
public class DynamicBlackboard { | |
private Dictionary<BlackboardKey, dynamic> _data = new Dictionary<BlackboardKey, dynamic>(); | |
public DynamicBlackboard() { } | |
public void SetValue(BlackboardKey key, dynamic value) { | |
if (_data.ContainsKey(key) == false) { | |
_data.Add(key, value); | |
return; | |
} | |
_data[key] = value; | |
} | |
public T GetValue<T>(BlackboardKey key) { | |
if (_data.ContainsKey(key) == false) { | |
return default; | |
} | |
return (T) _data[key]; | |
} | |
public void RemoveValue(BlackboardKey key) { | |
if (_data.ContainsKey(key) == false) { | |
return; | |
} | |
_data.Remove(key); | |
} | |
public bool HasValue(BlackboardKey key) { | |
return _data.ContainsKey(key); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment