What are C# Interfaces? Can someone explain them in plain English?

So I’ve watched a couple of videos and read a few articles on Interfaces in C# and Unity, and as far as I can tell they do… something? And it’s important? I’ve seen them called contracts, or security measures, or a number of other things, but the jargon is too thick and people almost seem to talk around the point.

My guess is that they’re publicly available methods that can be called anywhere (like ‘transform’, etc). But that’s only my best guess as none of the tutorials say that, or much of anything else about them except how to write them. Could someone explain them, or link to someone who can?

1 Like

Contracts, protocols, interfaces, yeah, lots of names for the same idea.

Your car has an IDriveable interface. Once you learn a basic car you can probably drive most small cars / trucks because they share the same interface.

Airplanes also have an interface. Once you can fly a small airplane, you can probably fly others.

Light switches in your house are an interface into controlling electricity. I don’t need to teach you how to use lightswitches in my house because I presume you’re familiar with the interface.

Using Interfaces in Unity3D:

Check Youtube for other tutorials about interfaces and working in Unity3D. It’s a pretty powerful combination.

1 Like

So it’s something to do with the UI?

UI can use interfaces. Interfaces are a language feature, not at all just for UI.

Feel free to take another look at the material above or else go look at general C# interface tutorials.

I’m sorry I may be stupid or something but I wasn’t able to work out what these things do from that. I’ve spent over an hour googling this. I’ve looked at two videos & dozens of articles and I didn’t understand any of it. There were so many things talking about contracts or driving cars but they all seem to start in the middle and leave out massive portions of information. If it’s a contract, then what makes if different from a method? I really don’t understand the car metaphor. Are you saying that if I can code other things, I can code this? Or that this is something that is important to the control systems of a car?

I think I may have finally worked it out from this though, so I hope you won’t mind if I give you my definition of what an interface is and then maybe you can correct me on it?

“An Interface is a directory of special methods that can be called on from any other part of the program, much in the way functions like ‘math’ or ‘transform’ can be.”

If it’s not that, I have no clue. Sorry.

How about,

An interface is a collection of methods that an object may implement.

An object implementing all the methods of an interface may then be declared as expressing that interface.

A reference to an interface may refer to any object that expresses the interface.

1 Like

Ah. I think I’m starting to get it. I think I’m going to have to experiment with it as it seems like one of those really fundamental things that sound very meta when people explain them.

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.

So how does this become useful in your code?

Read on in the spoiler:

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

2 Likes