1

I have a bunch of different kind of NPCs in my game and of course they logically similar, they have health, they have vision, they can navigate using agent and stuff.

But each NPC type has it's own custom behavior with states, actions, decisions and hooks. And those scripts require various specific data like coroutines running, target altitude or current leaping direction.

And I have to store it or have it on NPC mono behavior, so it is accessible inside state's scripts (they are scriptable objects called from NPC mono behavior)

Right now what I do is specifying array for each data type and count of it that I assign on NPC prefab. And it feels wrong...

enter image description here

public class Npc : MonoBehaviour
{
public static Dictionary<int, Npc> npcs = new Dictionary<int, Npc>();

public int npcId;
public NpcType type;

public Transform shootOrigin;
public Transform head;

public float maxHealth = 50f;
public float visionRange = 15;
public float visionAngle = 60;
public float headAngle = 120;
public float movementSpeed = 4.5f;

public int indexedActionsCount = 0;
[HideInInspector] public float[] lastActTimeIndexed;
[HideInInspector] public bool[] wasActionCompletedIndexed;

public int indexedVector3DataCount = 0;
[HideInInspector] public Vector3[] vector3DataIndexed;

public int indexedFloatDataCount = 0;
[HideInInspector] public float[] floatDataIndexed;

public int indexedBoolDataCount = 0;
[HideInInspector] public bool[] boolDataIndexed;

public int indexedCoroutineDataCount = 0;
[HideInInspector] public IEnumerator[] coroutineDataIndexed;

public NpcState currentState;
public NpcState remainState;

public float Health { get; private set; }

[HideInInspector] public NavMeshAgent agent;

public static int decisionUpdatesPerSecond = 2; // Check for decisions in 2FPS
public static int actionUpdatesPerSecond = 5; // Act in 5FPS
public static int reportUpdatesPerSecond = 15; // Report in 15FPS

private static int nextNpcId = 10000;


public void Awake()
{
    agent = GetComponent<NavMeshAgent>();
}

public void Start()
{
    npcId = nextNpcId;
    nextNpcId++;
    npcs.Add(npcId, this);

    Health = maxHealth;
    agent.speed = movementSpeed;

    lastActTimeIndexed = new float[indexedActionsCount];
    wasActionCompletedIndexed = new bool[indexedActionsCount];

    floatDataIndexed = new float[indexedFloatDataCount];
    boolDataIndexed = new bool[indexedBoolDataCount];
    vector3DataIndexed = new Vector3[indexedVector3DataCount];
    coroutineDataIndexed = new IEnumerator[indexedCoroutineDataCount];

    ServerSend.SpawnNpc(npcId, type, transform.position);

    InvokeRepeating("GetTarget", 1.0f, 1.0f);
    InvokeRepeating("UpdateDecisions", 0.0f, 1.0f / decisionUpdatesPerSecond);
    InvokeRepeating("UpdateActions", 0.0f, 1.0f / actionUpdatesPerSecond);
    InvokeRepeating("SendUpdates", 0.0f, 1.0f / reportUpdatesPerSecond);

    OnEnterState();
}

public void TakeDamage(float _damage)
{

}

public bool GoTo(Vector3 location)
{

}

public void TransitionToState(NpcState nextState)
{
    OnExitState();
    currentState = nextState;
    OnEnterState();
}

public void StartCoroutineOnNpc(IEnumerator routine)
{
    StartCoroutine(routine);
}

public void StopCoroutineOnNpc(IEnumerator routine)
{
    StopCoroutine(routine);
}

private void OnEnterState()
{
    var hooks = currentState.onEnterHooks;
    for (int i = 0; i < hooks.Length; i++)
    {
        hooks[i].Apply(this);
    }
    stateTimeOnEnter = Time.time;
    wasActionCompleted = false;
}

private void OnExitState()
{
    var hooks = currentState.onExitHooks;
    for (int i = 0; i < hooks.Length; i++)
    {
        hooks[i].Apply(this);
    }
}

private void UpdateDecisions()
{
    currentState.UpdateDecisions(this);
}

private void UpdateActions()
{
    currentState.UpdateState(this);
}

private void SendUpdates()
{
    ServerSend.NpcState(this);
}
}

In JavaScript world I would just have 1 array or object and put any data this particular NPC needs to it. But in C# I need a strongly typed place to put data for each data type my scripts could require.

Example of data usage in script:

enter image description here

I don't think having so many arrays and counters on MonoBehavior is a good idea, especially that there may be a lot of NPCs on scene. Any advice on building better storage while maintaining script flexibility?

Clarification: All the behavior logic is controlled by flexible ScriptableObject states. The problem is these objects cannot store any runtime data, but they have access to my Npc MonoBehavior (component) instance.

enter image description here

Initial code for this approach came from Unity tutorial

N7D
  • 192
  • 3
  • 12
  • 1
    So you have a base class ´Npc´ that has a bunch of arrays that store...what exactly? Sry, I don't really get that part. – B3NII Nov 15 '21 at 21:15
  • 1
    Base class NPC probably doesn't need to know every integer, coroutine, etc its components happen to use - for every small logical grouping of members, put those into a separate component and attach it to the NPC, then any other component on the npc can just get a reference to any components it needs the data on using `GetComponent` and caching the result (or assignment in the inspector in the case of prefabs). See [this wonderful post](https://gamedev.stackexchange.com/questions/23755/using-component-based-entity-system-practically) on the gamedev stackexchange (not unity specific) – Ruzihm Nov 15 '21 at 21:18
  • 1
    [Here](https://spin.atomicobject.com/2020/09/05/unity-component-based-design/) is a random blog post I found searching for "component based design unity" that touches on what this might look like in unity specifically – Ruzihm Nov 15 '21 at 21:21
  • Npc class has the functionality that is generally used by all the NPCs. While the behavior and logic is controlled by ScriptableObject states, ScriptableObjects are basically files on disk so they don't have any storage for runtime values of specific NPCs. The data for the particular NPC logic must come from NPC GameObject in the scene. – N7D Nov 15 '21 at 21:46
  • As I see it now, thanks to your comments, I should probably attach specific MonoBehaviors (components) to prefabs that will hold runtime data for ScriptableObject scripts. This way I can give every variable a proper name and escape from overhead of creating a bunch of arrays even if I don't use them. But it will require calling `GetComponent` a lot of times per frame, as I cannot store it in NPC... so performance will be even worse this way. – N7D Nov 15 '21 at 21:47
  • So the main problem is dynamic type of storage variable. I guess **for example** if I inherit LeaperData, ChaserData, FlyerData from NpcData (MonoBehavior), then I can have variable of NpcData type on Npc that will store cached `GetComponent()` result – N7D Nov 15 '21 at 21:58
  • Then **for example** flying script can grab npc.NpcData and cast data to FlyerData? Will casting hundreds time per frame affect the performance? – N7D Nov 15 '21 at 22:10
  • 1
    It is not needed to call GetComponent all the time. Since MonoBehaviours are also objects, they are stored by reference. This means, if you create an instance of NPC class in every MonoBehaviour that might be attached to an NPC, it is enought to call the GetComponent on Awake and store the retuned value as a private variable in the MonoBehaviours. (I'm referring to components that might be used on NPCs as MonoBehaviours.) This is actually pretty memory "gentle", since only references are stored. – B3NII Nov 15 '21 at 22:22
  • 1
    NpcData is redundant - suppose `FlyerController` needs to modify the health of its object under certain situations such as flying into a wall, and needs to read the stats of the npc, it does `GetComponent` and `GetComponent` *once* in `Awake` for example and save the results as `HealthAttribute myHealth` and `StatsAttribute myStats` fields. Maybe an npc, vehicle, etc can optionally have a `FuelConsumer` component and so `FlyerController` can use getcomponent for that, and if it isn't null, modify the fuel depending on flight speed. – Ruzihm Nov 15 '21 at 22:29
  • @B3NII @Ruzihm I understand the basic concept of caching components on Awake, but unfortunately as I said before the execution logic happens inside ScriptableObject's method (I updated question with additional clarification), and the only connection between logic script with actual runtime object in the scene is npc reference passed into script `public override void Apply(Npc npc)`. – N7D Nov 15 '21 at 22:34
  • Please include your code as text instead of in images so it's easier to copy and paste into answers, be indexed by search engines, etc. – Ruzihm Nov 15 '21 at 22:39
  • So we either have #1: generic NpcData and have to cast it inside the script to subclass every time script is called. #2: reference to every kind of data component on npc and try to get all of them in Awake, so we'll end up with 1 actual reference and a lot of variables storing nulls (so it's 4 bytes * npcTypeCount - 4) useless memory on every NPC in the scene. 1st will be slower, but less memory, 2nd is faster, but more memory usage. – N7D Nov 15 '21 at 22:39
  • And what if you create the components that do something (logic from ScriptableObjects) and store the data in that component (arrays if you must). Thus, you can add only those components to the NPCs, that really require their functions. This way you can avoid unnecessary memory usage for each NPC because they only have the components they actually need. I hope I could express myself. – B3NII Nov 15 '21 at 22:44
  • @Ruzihm added Npc MonoBehavior code (stripped unnecessary stuff) – N7D Nov 15 '21 at 22:47
  • @B3NII I understand your idea, it would work for simpler AIs, but I decided to use more flexible approach that allows complex behavior changes. The stored data might be shared between actions, or cached in decision for actions to reuse it, and it may also be stored between states. Like AI can steal item from player and run away to hide it, but it doesn't do it because it has script attached, it does it because it decided to switch to the state that has this logic on. I think implementing behavior tree with my current approach is more optimal memory and performance wise. – N7D Nov 15 '21 at 22:54
  • 1
    With the addition of these details, it now seems that this isn't really an unanswered question so much as it is an invitation to critique an already existing answer and an invitation to provide alternatives. The already existing answer doesn't belong in the question - it should be a [self-answer](https://stackoverflow.com/help/self-answer) if anything, and the question shouldn't be "how best" it should just be "how" - otherwise it is an opinion-based question. – Ruzihm Nov 15 '21 at 23:08
  • I will provide self answer when I will rewrite my code. The solutions discussed in this comment thread are better than what I have right now. – N7D Nov 15 '21 at 23:14

1 Answers1

0

Let me explain the structure I ended up using for the case I described:

If particular NPC requires some specific data for its behavior I will add another component (in this example leaper NPC needs to store data for leaping behavior)

enter image description here

This data is defined in interface (it's important, because 1 NPC may implement multiple interfaces [several reused behaviors])

public interface ILeaperData
{
    public Vector3 leapTarget { get; set; }
    public Vector3 initialPosition { get; set; }
    public bool startedLeap { get; set; }
    public float lastLeapTime { get; set; }
}

And then this NPC type will have component that implements this interface (and 1 more in this example)

public class LeaperData : NpcData, ILeaperData, ICompletedActionData
{
    public Vector3 leapTarget { get; set; }
    public Vector3 initialPosition { get; set; }
    public bool startedLeap { get; set; }
    public float lastLeapTime { get; set; }

    public bool wasActionCompleted { get; set; }
}

That way I can reuse data interfaces when the same behavior is used on other NPC types.

Example of how it is used in ScriptableObject logic:

[CreateAssetMenu(menuName = "AI/Decisions/CanLeap")]
public class CanLeapDecision : NpcDecision
{
    public int nextAngle = 45;
    public float radius = 4;

    public override bool Decide(Npc npc)
    {
        if (npc.target)
        {
            var dir = (npc.transform.position - npc.target.position).normalized;
            var dir2 = new Vector2(dir.x, dir.z).normalized * radius;
            var dir3 = new Vector3(dir2.x, dir.y, dir2.y);


            if (NavMesh.SamplePosition(RotateAroundPoint(npc.target.position + dir3, npc.target.position, Quaternion.Euler(0, nextAngle * ((Random.value > 0.5f) ? 1 : -1), 0)), out var hit, 3.5f, 1))
            {
                var path = new NavMeshPath();
                npc.agent.CalculatePath(hit.position, path);

                if (path.corners.Length == 2 && path.status == NavMeshPathStatus.PathComplete)
                {
                    ((ILeaperData)npc.npcData).leapTarget = hit.position;
                    ((ILeaperData)npc.npcData).initialPosition = npc.transform.position;
                    ((ILeaperData)npc.npcData).startedLeap = false;
                    return true;
                }
            }
        }
        return false;
    }

    private Vector3 RotateAroundPoint(Vector3 point, Vector3 pivot, Quaternion angle)
    {
        var finalPos = point - pivot;
        //Center the point around the origin
        finalPos = angle * finalPos;
        //Rotate the point.

        finalPos += pivot;
        //Move the point back to its original offset. 
        return finalPos;
    }
}

You can see the cast to (ILeaperData) where I need the data stored on this NPC instance.

N7D
  • 192
  • 3
  • 12