Skip to content

Instantly share code, notes, and snippets.

@ryanzec

ryanzec/AIController.cs

Last active May 19, 2019
Embed
What would you like to do?
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