An interface defines the public members of a class, without defining the implementation of those members.
It is intended to support polymorphic contracts between disparate types.
I know… that sounds like mumbo jumbo. So to break it down:
polymorphic/ism - the concept that 2 similar types can be treated the same way. A HashSet and a List both have ‘Contains’ methods… they’re both implemented very differently underneath, but at the end of the day you can ask either of them if they Contain an element.
polymorphic contract - A contract is an agreement that 2 types have the same methods that have similar results. It’s defining in code terms that ‘Contains’ exists on both HashSet and List. Basically just because they both have a Contains method isn’t enough… there needs to be some ‘contract’ that tells the compiler that they both definitely do. In terms of C# this ‘contract’ is ICollection:
disparate types - means types that aren’t actually related via inheritance. HashSet and List do not inherit Contains from a like base class with those members. This makes them “disparate” (disparate - adj - essentially different in kind; not allowing comparison). HashSet and List are not the same “kind” or “type” so there for you can’t use them interchangeably.
Unless you define a contract that relates them. Hence ICollection.
Well… lets take an example that I personally wrote:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/GridGraph.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
This is ‘GridGraph’, it defines a graph that is a 2d grid represented as an array. This GridGraph is used in A* calculations to solve for paths through said graph. Now how A* works is that you work backwards from the goal stepping through neighbours until you reach the start point and take the shortest path (including any penalty weights). This means we need a function that returns the “neighbours” of a given node in the graph.
Now… I could just return that as some collection, like an an array/list/etc. Like so:
public IEnumerable<T> GetNeighbours(int x, int y)
{
if (x >= 0 && x < _colCount && y > -1 && y < _rowCount - 1)
yield return this[x, y + 1]; //n
if (_includeDiagonals && x >= -1 && x < _colCount - 1 && y > -1 && y < _rowCount - 1)
yield return this[x + 1, y + 1]; //ne
if (x >= -1 && x < _colCount - 1 && y >= 0 && y < _rowCount)
yield return this[x + 1, y]; //e
if (_includeDiagonals && x >= -1 && x < _colCount - 1 && y > 0 && y < _rowCount)
yield return this[x + 1, y - 1]; //se
if (x >= 0 && x < _colCount && y > 0 && y <= _rowCount)
yield return this[x, y - 1]; //s
if (_includeDiagonals && x > 0 && x <= _colCount && y > 0 && y <= _rowCount)
yield return this[x - 1, y - 1]; //sw
if (x > 0 && x <= _colCount && y >= 0 && y < _rowCount)
yield return this[x - 1, y]; //w
if (_includeDiagonals && x > 0 && x <= _colCount && y > -1 && y < _rowCount - 1)
yield return this[x - 1, y + 1]; //nw
}
Here it is implemented as an iterator function.
Problem is that this creates a bit of garbage every time. So what if instead of returning a new iterator object every time I instead take in some collection that I fill with the neighbours. Similar to how GetComponents has an overload that takes a List to avoid the allocation of an array:
Unity - Scripting API: GameObject.GetComponents
private List<Collider> colliders = new List<Collider>();
void OnCollisionEnter(Collision c)
{
colliders.Clear();
c.gameObject.GetComponents<Collider>(colliders); //gets all colliders on the target, not just the one that hit
foreach(var c in colliders) { } //do stuff with them
}
You’ll notice though that it takes in a “List”, kind of restrictive (there’s a reason though… unity actually directly accesses the array internally since this method forwards into the C++ side of things. They need that limitation).
Well in our situation… what if I want to allow any collection that has an ‘Add’ method?
Well I take ICollection as the buffer:
public int GetNeighbours(int x, int y, ICollection<T> buffer)
{
if (buffer == null) throw new ArgumentNullException("buffer");
int cnt = buffer.Count;
if (x >= 0 && x < _colCount && y > -1 && y < _rowCount - 1)
buffer.Add(this[x, y + 1]); //n
if (_includeDiagonals && x >= -1 && x < _colCount - 1 && y > -1 && y < _rowCount - 1)
buffer.Add(this[x + 1, y + 1]); //ne
if (x >= -1 && x < _colCount - 1 && y >= 0 && y < _rowCount)
buffer.Add(this[x + 1, y]); //e
if (_includeDiagonals && x >= -1 && x < _colCount - 1 && y > 0 && y < _rowCount)
buffer.Add(this[x + 1, y - 1]); //se
if (x >= 0 && x < _colCount && y > 0 && y <= _rowCount)
buffer.Add(this[x, y - 1]); //s
if (_includeDiagonals && x > 0 && x <= _colCount && y > 0 && y <= _rowCount)
buffer.Add(this[x - 1, y - 1]); //sw
if (x > 0 && x <= _colCount && y >= 0 && y < _rowCount)
buffer.Add(this[x - 1, y]); //w
if (_includeDiagonals && x > 0 && x <= _colCount && y > -1 && y < _rowCount - 1)
buffer.Add(this[x - 1, y + 1]); //nw
return buffer.Count - cnt;
}
Now in my A* logic I can recycle whatever collection type I’d like:
public int Reduce(IList<T> path)
{
if (_calculating) throw new InvalidOperationException("PathResolver is already running.");
if (_graph == null || _heuristic == null || _start == null || _goal == null) throw new InvalidOperationException("PathResolver is not initialized.");
this.Reset();
_calculating = true;
try
{
_open.Add(this.CreateInfo(_start, _heuristic.Weight(_start), _goal));
while (_open.Count > 0)
{
var u = _open.Pop();
if (u.Node == _goal)
{
int cnt = 0;
while (u.Next != null)
{
path.Add(u.Node);
u = u.Next;
cnt++;
}
path.Add(u.Node);
return cnt + 1;
}
_closed.Add(u.Node);
_graph.GetNeighbours(u.Node, _neighbours);
var e = _neighbours.GetEnumerator();
while(e.MoveNext())
{
var n = e.Current;
if (_closed.Contains(n)) continue;
float g = u.g + _heuristic.Distance(u.Node, n) + _heuristic.Weight(n);
int i = GetInfo(_open, n);
if (i < 0)
{
var v = this.CreateInfo(n, g, _goal);
v.Next = u;
_open.Add(v);
}
else if (g < _open[i].g)
{
var v = _open[i];
v.Next = u;
v.g = g;
v.f = g + v.h;
_open.Update(i);
}
}
_neighbours.Clear();
}
}
finally
{
this.Reset();
}
return 0;
}
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/AStarPathResolver.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Sure I use List here, but I could use HashSet/Queue/Stack any ICollection I so chose. Since GetNeighbours just expects an ICollection.
…
Actually this entire example of a graph is ALSO an interface.
Note that GridGraph implements IGraph:
public interface IGraph<T> : IEnumerable<T>
{
int Count { get; }
IEnumerable<T> GetNeighbours(T node);
int GetNeighbours(T node, ICollection<T> buffer);
bool Contains(T node);
}
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/IGraph.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
And that there are other classes that can implement IGraph.
Like HexGraph, a graph of hexagonal nodes like this in Civilization:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/HexGraph.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Or LinkedNodeGraph, a graph of arbitrary nodes that I explicitly tell which are neighbours (think a free romaing map where I just place down control points in the editor):
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/LinkedNodeGraph.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Or NodeGraph, a graph where the nodes themselves return what their neighbours are:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/NodeGraph.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
And back in AStarPathResolver I don’t care what kind of graph is being used:
public class AStarPathResolver<T> : ISteppingPathResolver<T> where T : class
{
#region Fields
private IGraph<T> _graph;
private IHeuristic<T> _heuristic;
private BinaryHeap<VertexInfo> _open;
private HashSet<T> _closed = new HashSet<T>();
private HashSet<VertexInfo> _tracked = new HashSet<VertexInfo>();
private List<T> _neighbours = new List<T>();
private T _start;
private T _goal;
private bool _calculating;
#endregion
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/AStarPathResolver.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
A* doesn’t care what the graph looks like… it cares about being able to get what each node’s neighbours are. So IGraph just needs to implement a GetNeighbours method (I also have Contains/Count for convenience).
…
We can go even FURTHER!
AStarPathResolver implement the IPathResolver/ISteppingPathResolver interfaces:
public interface IPathResolver<T>
{
T Start { get; set; }
T Goal { get; set; }
IList<T> Reduce();
int Reduce(IList<T> path);
}
public interface ISteppingPathResolver<T> : IPathResolver<T>
{
/// <summary>
/// Start the stepping path resolver for reducing.
/// </summary>
void BeginSteppedReduce();
/// <summary>
/// Take a step at reducing the path resolver.
/// </summary>
/// <returns>Returns true if reached goal.</returns>
bool Step();
/// <summary>
/// Get the result of reducing the path.
/// </summary>
/// <param name="path"></param>
int EndSteppedReduce(IList<T> path);
/// <summary>
/// Reset the resolver so a new Step sequence could be started.
/// </summary>
void Reset();
}
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/IPathResolver.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
I can have other pathfinding algorithms like Dijkstra:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Graphs/DijkstraPathResolver.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Because at the end of the day my AI agent doesn’t care what the graph looks like… doesn’t care how the algorithm is calculated… it just cares that there is a method that returns a list of nodes that represent the path. And the agent just walks from node to node.
The interface facilitates all of this.
If I now distribute this API to some 3rd party to be used. And they decide they need to create a custom graph for their game… like a spherical graph, similar to how Mario Galaxy has spherical maps. Well, all they need to do is implement IGraph and shove it into the existing system.
…
You could even go the inverse. I have an entire “IPathSeeker” interface that allows me to inject the path solving algorithm into my movement scripts:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Pathfinding/IPathSeeker.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Then with this I can create a wrapper around my A* algorithm. Or say I wanted to use Aron Granberg’s A* algorithm. Or Unity’s pathfinding system.
I could implement custom IPathSeeker’s for each engine:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.agastarextensions/Runtime/src/AGAstarPathSeeker.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.agastarextensions/Runtime/src/AGAstarPathAgent.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Aron Granberg PathSeeker/PathAgent
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Pathfinding/Unity/UnityPathSeeker.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.pathfinding/Runtime/src/Pathfinding/Unity/UnityStandardPathAgent.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Unity PathSeeker/PathAgent
Then I can have movement scripts that rely on that…
So my character movement script:
using UnityEngine;
using System.Collections;
using com.spacepuppy;
using com.spacepuppy.Anim;
using com.spacepuppy.Anim.Legacy;
using com.spacepuppy.Motor;
using com.spacepuppy.Pathfinding;
using com.spacepuppy.Utils;
using com.vivarium.Movement;
namespace com.vivarium.Entities.Actors.Character
{
public class CharacterWalkMovementStyle : VivariumPathingMovementStyle
{
#region Fields
[SerializeField()]
private GroundingResolver _groundingResolver;
[Header("Walk")]
[SerializeField()]
private float _walkSpeed = 1f;
[SerializeField()]
private float _walkTurnSpeed = 90f;
[SerializeField()]
private float _walkPaceDownDistance = 1f;
[Header("Chase")]
[SerializeField()]
private float _chaseSpeed = 2f;
[SerializeField()]
private float _chaseTurnSpeed = 90f;
[SerializeField()]
private float _chasePaceDownDistance = 1f;
[Header("Flee")]
[SerializeField()]
private float _fleeSpeed = 2f;
[SerializeField()]
private float _fleeTurnSpeed = 90f;
[SerializeField()]
private float _fleePaceDownDistance = 1f;
#endregion
#region CONSTRUCTOR
#endregion
#region Properties
public new CharacterEntity Entity { get { return base.Entity as CharacterEntity; } }
#endregion
#region AIMovementStyle Interface
protected override void UpdateMovement()
{
if (this.PathFollower == null || this.PathFollower.CurrentPath == null)
{
//idle
this.UpdateIdle();
}
else
{
//move
this.UpdateMove(_walkSpeed, _walkTurnSpeed, _walkPaceDownDistance);
}
}
private void UpdateIdle()
{
var mv = this.Motor.Velocity;
mv.x *= 0.5f;
mv.z *= 0.5f;
mv.y -= Game.Settings.GravityMagnitude * Time.deltaTime;
this.Motor.Move(mv * Time.deltaTime);
}
private void UpdateMove(float speed, float turnSpeed, float paceDownDist)
{
var path = this.PathFollower.CurrentPath;
if (path == null || path.Waypoints == null || path.Waypoints.Count == 0)
{
this.UpdateIdle();
if (path.IsDone()) this.OnPathComplete(false);
return;
}
int pathindex = 0;
CalculateNextTarg:
var mv = this.Motor.Velocity;
var targ = path.GetBestTarget(this.Motor.Position, ref pathindex);
var dir = (targ - this.Motor.Position).SetY(0f);
var dist = dir.magnitude;
if(MathUtil.FuzzyEqual(dist, 0f, 0.1f))
{
if(pathindex == (path.Waypoints.Count - 1))
{
this.UpdateIdle();
this.OnPathComplete(true);
return;
}
else
{
//we landed right on top of a node and GetBestTarget didn't catch it. This should seldom if ever happen... but if it does try all over again.
pathindex++;
goto CalculateNextTarg;
}
}
else
{
dir /= dist;
}
var ds = speed * Time.deltaTime;
bool approachingTarg = (pathindex == path.Waypoints.Count - 1);
if(approachingTarg && dist <= ds)
{
mv.x = dir.x * dist;
mv.z = dir.z * dist;
mv.y -= Game.Settings.GravityMagnitude * Time.deltaTime;
if (mv.y > 0f && !_groundingResolver.IsGrounded) mv.y *= 0.5f; //damp upward movement if not on ground
this.Motor.Move(mv);
this.Motor.transform.rotation = Quaternion.LookRotation(dir.SetY(0f));
//done
this.OnPathComplete(true);
}
else
{
mv.x = Mathf.Lerp(mv.x, dir.x * speed, 0.9f);
mv.z = Mathf.Lerp(mv.z, dir.z * speed, 0.9f);
mv.y -= Game.Settings.GravityMagnitude * Time.deltaTime;
if (mv.y > 0f && !_groundingResolver.IsGrounded) mv.y *= 0.5f; //damp upward movement if not on ground
this.Motor.Move(mv * Time.deltaTime);
this.Motor.transform.rotation = Quaternion.LookRotation(mv.SetY(0f));
}
}
private void UpdateFlee()
{
//need to determine what to do for flee
this.UpdateIdle();
}
private void OnPathComplete(bool reachedGoal)
{
if(this.Entity?.AIController != null)
{
this.Entity.AIController.SignalFinishedPath(reachedGoal);
}
else if(this.PathFollower != null)
{
this.PathFollower.SetPath(null);
}
}
#endregion
}
}
And the AI Logic that updates tells it to decide on a new path:
using UnityEngine;
using System.Collections.Generic;
using com.spacepuppy;
using com.spacepuppy.AI;
using com.spacepuppy.Collections;
using com.spacepuppy.Pathfinding;
using com.spacepuppy.Mecanim;
using com.spacepuppy.Motor;
using com.spacepuppy.Sensors;
using com.spacepuppy.Tween;
using com.spacepuppy.Utils;
using com.vivarium.AI;
using com.vivarium.Interactable;
namespace com.vivarium.Entities.Actors.Character
{
public sealed class CharacterAIController : SPComponent, IAIMobStateMachineBridge, IPathAgent
{
public enum EntityState
{
Standard = 0,
Cutscene = 1
}
#region Anim State Constants
public const string STATE_ANNOUNCEMOOD = "Base Layer.Announce-Mood";
public const string STATE_ANNOUNCENEED = "Base Layer.Announce-Need";
public const string STATE_FOCUSPLAYER = "Base Layer.Focus-Player";
public const string STATE_FOCUSREQUEST = "Base Layer.Focus-Request";
public const string STATE_FOCUSTANK = "Base Layer.Focus-Tank";
public const string STATE_GETIDEA = "Base Layer.GetIdea";
public const string STATE_GETPLAYERATTENTION = "Base Layer.GetPlayerAttention";
public const string STATE_IDLE = "Base Layer.Idle";
public const string STATE_IDLEACTION = "Base Layer.IdleAction";
public const string STATE_MISSINGSOMETHING = "Base Layer.MissingSomething";
public const string STATE_MOVERUN = "Base Layer.Move-Run";
public const string STATE_MOVEWALK = "Base Layer.Move-Walk";
public const string STATE_WAKEUPGROUND = "Base Layer.WakeupGround";
public const string STATE_SPOT = "Base Layer.Spot";
public const string STATE_INTERACT = "Base Layer.Interact";
public static readonly int HASH_STATE_ANNOUNCEMOOD = Animator.StringToHash(STATE_ANNOUNCEMOOD);
public static readonly int HASH_STATE_ANNOUNCENEED = Animator.StringToHash(STATE_ANNOUNCENEED);
public static readonly int HASH_STATE_FOCUSPLAYER = Animator.StringToHash(STATE_FOCUSPLAYER);
public static readonly int HASH_STATE_FOCUSREQUEST = Animator.StringToHash(STATE_FOCUSREQUEST);
public static readonly int HASH_STATE_FOCUSTANK = Animator.StringToHash(STATE_FOCUSTANK);
public static readonly int HASH_STATE_GETIDEA = Animator.StringToHash(STATE_GETIDEA);
public static readonly int HASH_STATE_GETPLAYERATTENTION = Animator.StringToHash(STATE_GETPLAYERATTENTION);
public static readonly int HASH_STATE_IDLE = Animator.StringToHash(STATE_IDLE);
public static readonly int HASH_STATE_IDLEACTION = Animator.StringToHash(STATE_IDLEACTION);
public static readonly int HASH_STATE_MISSINGSOMETHING = Animator.StringToHash(STATE_MISSINGSOMETHING);
public static readonly int HASH_STATE_MOVERUN = Animator.StringToHash(STATE_MOVERUN);
public static readonly int HASH_STATE_MOVEWALK = Animator.StringToHash(STATE_MOVEWALK);
public static readonly int HASH_STATE_WAKEUPGROUND = Animator.StringToHash(STATE_WAKEUPGROUND);
public static readonly int HASH_STATE_SPOT = Animator.StringToHash(STATE_SPOT);
public static readonly int HASH_STATE_INTERACT = Animator.StringToHash(STATE_INTERACT);
public const string PROP_MOVESPEED = "MoveSpeed";
public static readonly int HASH_PROP_MOVESPEED = Animator.StringToHash(PROP_MOVESPEED);
#endregion
#region Fields
[SerializeField]
[DefaultFromSelf(EntityRelativity.Entity)]
private Animator _animator;
[SerializeField]
[DefaultFromSelf]
private Transform _subStateBridgeContainer;
[SerializeField]
private Sensor _sensor;
[SerializeField]
[DefaultFromSelf]
private PathSeekerRef _pathSeeker;
[SerializeField]
[DefaultFromSelf]
private MovementStyleController _movementStyleController;
[SerializeField]
private float _idleDelay = 0.33f;
[SerializeField]
private AIVariableCollection _variables;
[System.NonSerialized]
private CharacterEntity _entity;
[System.NonSerialized]
private IPath _currentPath;
[System.NonSerialized]
private bool _pathPaused = true;
[System.NonSerialized]
private RadicalCoroutine _coreLogicManualRoutine;
[System.NonSerialized()]
private EntityState _currentState;
#endregion
#region CONSTRUCTOR
protected override void Start()
{
base.Start();
_entity = CharacterEntity.CharacterPool.GetFromSource(this);
if (_animator != null)
{
this.InitializeBridge();
}
//TODO - determine if we need to do spawn logic and start that instead
_coreLogicManualRoutine = new RadicalCoroutine(this.CoreLogic());
}
#endregion
#region Properties
public CharacterEntity Entity => _entity;
public Sensor Sensor => _sensor;
public CharacterPriorityMood PriorityMood { get { return _entity?.Stats?.PriorityMood ?? CharacterPriorityMood.Neutral; } }
public CharacterMood Mood { get { return _entity?.Stats?.Mood ?? CharacterMood.Neutral; } }
public ComplexTarget MoveTarget { get; set; } // { get { return _variables.GetAsComplexTarget("MoveTarget"); } set { _variables.SetAsComplexTarget("MoveTarget", value); } }
public IPath LastCompletedPath { get; private set; }
public bool LastCompletedPathReachedGoal { get; private set; }
#endregion
#region Methods
public void SetEntityState(EntityState mode)
{
if (_currentState == mode) return;
_currentState = mode;
}
public void InteractWith(InteractableAspect aspect)
{
#if UNITY_EDITOR
Debug.Log("Interacting With: " + IEntity.Pool.GetFromSource(aspect).name, aspect);
#endif
//force character to target a specific target
this.Variables[Constants.AI_VAR_TARGET] = aspect;
_animator.Play(Constants.AI_STATEHASH_MOVE);
}
public void AlertOfEntity(IEntity entity)
{
if (entity == null) return;
using (var aspects = com.spacepuppy.Collections.TempCollection.GetList<InteractableAspect>())
{
//TODO - Conversion - this should probably be refactored, it was a property on IEntity in v1
entity.GetComponentsInChildren<InteractableAspect>(aspects);
if (aspects.Count == 0) return;
aspects.Sort((a, b) =>
{
return -a.GetPrecedence(this.Entity).CompareTo(b.GetPrecedence(this.Entity));
});
if (aspects[0].GetPrecedence(_entity) != float.NegativeInfinity)
{
this.InteractWith(aspects[0]);
return;
}
}
}
public void SignalFinishedPath(bool reachedGoal)
{
this.LastCompletedPath = _currentPath;
this.LastCompletedPathReachedGoal = reachedGoal && _currentPath != null;
(this as IPathFollower).SetPath(null);
}
#endregion
#region IAIController Interface
public AIVariableCollection Variables { get { return _variables; } }
IPathAgent IAIMobStateMachineBridge.PathAgent { get { return this; } }
#endregion
#region IPathAgent Interface
IPathFactory IPathSeeker.PathFactory { get { return _pathSeeker.Value?.PathFactory ?? AGAstarPathFactory.Default; } }
bool IPathSeeker.ValidPath(IPath path)
{
return _pathSeeker.Value?.ValidPath(path) ?? false;
}
void IPathSeeker.CalculatePath(IPath path)
{
_pathSeeker.Value?.CalculatePath(path);
}
public IPath CurrentPath => _currentPath;
public bool IsTraversing => !_pathPaused && _currentState == EntityState.Standard; //we only traverse if not paused and not in cutscene
public IPath PathTo(Vector3 target)
{
var p = (this as IPathSeeker).PathFactory.Create(this, target);
this.PathTo(p);
return p;
}
public void PathTo(IPath path)
{
if(path.Status == PathCalculateStatus.NotStarted)
_pathSeeker.Value?.CalculatePath(path);
_currentPath = path;
_pathPaused = _currentPath == null;
}
void IPathFollower.SetPath(IPath path)
{
_currentPath = path;
_pathPaused = _currentPath == null;
}
void IPathFollower.ResetPath()
{
_currentPath = null;
_pathPaused = true;
}
void IPathFollower.StopPath()
{
_pathPaused = true;
}
void IPathFollower.ResumePath()
{
_pathPaused = _currentPath == null;
}
#endregion
#region IAIMobStateMachineBridge Interface
public Animator Animator => _animator;
public RuntimeAnimatorController InitialRuntimeAnimatorController => null;
public Transform SubStateBridgeContainer => _subStateBridgeContainer;
#endregion
#region Core Logic
private void Update()
{
switch(_currentState)
{
case EntityState.Standard:
{
if (_coreLogicManualRoutine == null)
{
_coreLogicManualRoutine = new RadicalCoroutine(this.CoreLogic());
}
_coreLogicManualRoutine.ManualTick(this);
switch (_coreLogicManualRoutine.OperatingState)
{
case RadicalCoroutineOperatingState.FatalError:
case RadicalCoroutineOperatingState.Cancelled:
case RadicalCoroutineOperatingState.Cancelling:
case RadicalCoroutineOperatingState.Completing:
case RadicalCoroutineOperatingState.Complete:
_coreLogicManualRoutine = null;
break;
}
_animator.SetFloat(HASH_PROP_MOVESPEED, _entity.Motor.Velocity.SetY(0f).magnitude);
}
break;
case EntityState.Cutscene:
//do nothing
break;
}
}
private System.Collections.IEnumerable SpawnLogic()
{
//TODO: spawn logic
yield break;
}
private System.Collections.IEnumerable CoreLogic()
{
Focus:
//TODO - instead of wait, we should play a focus animation
yield return WaitForDuration.Seconds(1f);
//EvaluateMood:
_entity.Stats?.EvaluatePriorityMood();
var pmood = _entity.Stats?.PriorityMood ?? CharacterPriorityMood.Neutral;
var mood = _entity.Stats?.Mood ?? CharacterMood.Neutral;
if (pmood != CharacterPriorityMood.Neutral)
{
//Has Priority Mood
}
else if (mood != CharacterMood.Neutral)
{
//Has Simple Mood
}
else
{
//Neutral
}
//FindTarget:
InteractableAspect target = null;
IPath path = null;
using (var ptargets = TempCollection.GetList<InteractableAspect>())
{
var smask = pmood.GetRelatedStatus().ToMask();
var mmask = pmood == CharacterPriorityMood.Neutral ? mood.ToMask() : CharacterMoodMask.None;
if(this.Sensor?.SenseAll<InteractableAspect>(ptargets, (a) => a.SatisfiesStatus.Intersects(smask) || a.SatisfiesMood.Intersects(mmask)) > 0)
{
ptargets.Sort((a, b) => -a.Precedence.CompareTo(b.Precedence));
for (int i = 0; i < ptargets.Count; i++)
{
var p = this.CreatePath(ptargets[i].transform.position);
(this as IPathSeeker).CalculatePath(p);
while(p.Status == PathCalculateStatus.Calculating)
{
yield return null;
}
if(p.Status == PathCalculateStatus.Success)
{
target = ptargets[i];
path = p;
break;
}
}
}
else
{
//found no target goals
yield return WaitForDuration.Seconds(1f);
goto GetPlayersAttention;
}
}
if (target == null || path == null)
{
//all goals are unreachable
yield return WaitForDuration.Seconds(1f);
goto GetPlayersAttention;
}
//PathToTarget:
this.PathTo(path);
while(this.IsTraversing)
{
if(this.CurrentPath != path)
{
//some how ended up on a diferent path... what do???
}
switch (target.Positioning)
{
case CharacterPosition.Near:
case CharacterPosition.Face:
var dist = (target.transform.position - _entity.transform.position).SetY(0f).sqrMagnitude;
if (dist <= target.NearEnoughDistance * target.NearEnoughDistance)
{
this.SignalFinishedPath(true);
}
break;
case CharacterPosition.Exact:
//do nothing, we want to go all the way up to it
break;
}
yield return null;
}
//LookAtTarget:
var targEntity = IEntity.Pool.GetFromSource(target);
switch (target.Positioning)
{
case CharacterPosition.Near:
//do nothing
break;
case CharacterPosition.Face:
{
var dir = (targEntity.transform.position - this.transform.position).SetY(0f);
if (!VectorUtil.NearZeroVector(dir))
{
var rot = Quaternion.LookRotation(dir);
var angleBetween = Quaternion.Angle(this.transform.rotation, rot);
yield return SPTween.Tween(this.transform)
.Prop(this.transform.rotation_ref())
.To(angleBetween / 90f, rot)
.Play(false);
}
}
break;
case CharacterPosition.Exact:
{
var curPos = this.transform.position;
this.transform.position = target.transform.position.SetY(curPos.y);
var dir = target.transform.forward.SetY(0f);
if (!VectorUtil.NearZeroVector(dir))
{
var rot = Quaternion.LookRotation(dir);
var angleBetween = Quaternion.Angle(this.transform.rotation, rot);
yield return SPTween.Tween(this.transform)
.Prop(this.transform.rotation_ref())
.To(angleBetween / 180f, rot)
.Play(false);
}
}
break;
}
target.OnReachedTarget.ActivateTrigger(target, _entity);
//InteractWithTarget:
yield return _entity.InteractWithAsync(target);
target.OnInteractionComplete.ActivateTrigger(target, _entity);
//Idle
yield return WaitForDuration.Seconds(_idleDelay);
GetPlayersAttention:
yield return null;
goto Focus;
}
#endregion
#region Editor-Only Private
#if UNITY_EDITOR
private void OnDrawGizmos()
{
var targ = this.Variables.GetAsComplexTarget(Constants.AI_VAR_TARGET);
if (!targ.IsNull)
{
Gizmos.color = Color.blue;
Gizmos.DrawSphere(targ.Position, 0.1f);
}
}
public static bool FilterVariableNames(System.Reflection.MemberInfo member)
{
if (TypeUtil.IsType(typeof(SPComponent), member.DeclaringType)) return false;
switch (member.Name)
{
case "SubStateBridgeContainer":
return false;
}
var rtp = com.spacepuppy.Dynamic.DynamicUtil.GetReturnType(member);
return (rtp == typeof(ComplexTarget) ||
rtp == typeof(Vector2) ||
rtp == typeof(Vector3) ||
rtp == typeof(Vector4) ||
TypeUtil.IsType(typeof(Transform), rtp) ||
TypeUtil.IsType(typeof(IAspect), rtp));
}
#endif
#endregion
}
}
And you can see how it relies on some PathSeeker. I attach the PathSeeker as follows:
But at the end of the day… I could replace that AG Astar Path Seeker with the Unity Path Seeker if I wanted to stop using Aron Granberg and instead use Unity. I could even mix and match in my game… some maps might be better done in Unity, others in AG, others in mine.
And I don’t have to refactor any of my code.
I just swap out the PathSeeker component, and my AI/walk scripts automatically start working with the new system. Despite the fact that none of these engines were designed to work with one another!
…
You can see even more polymorphic design in here as well. Note how to ‘sense’ targets around the character there is a ‘Sensor’ property of the AIController. I have an ‘OmnispectiveSensor’ attached:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.sensors/Runtime/src/Sensors/Visual/OmnispectiveSensor.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
An Omnispective Sensor sees all aspects in the world (think like how god is omnipresent… well this character is omnispective, it can see all things in the world regardless of location). But say I wanted to limit my characters ability to see to a region… I could replace that sensor with a SphericalVisualSensor:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.sensors/Runtime/src/Sensors/Visual/SphericalVisualSensor.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Or a RightCylindricalVisualSensor:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.sensors/Runtime/src/Sensors/Visual/RightCylindricalVisualSensor.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Or say I wanted to stop relying on my octree/aspects, and instead use colliders:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.sensors/Runtime/src/Sensors/Collision/ColliderSensor.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub
Or raycasts:
spacepuppy-unity-framework-4.0/Framework/com.spacepuppy.sensors/Runtime/src/Sensors/Collision/RaycastSensor.cs at master · lordofduct/spacepuppy-unity-framework-4.0 · GitHub