Page 1 of 1

Detect NPC Spawns

Posted: Thu Apr 28, 2022 9:36 pm
by l3lessed
Is there an easy way to detect when the current world space, either the exterior streaming world or an interior location, has spawned an npc?

I want to move my npc detecting script out of a continual update loop, as this is probably one of the bigger CPU demands left in my mod; right now, every set time frame, it scans the location for the differing npc component objects and uses those to get new spawns and assign a script to them. It works, but it is costly scanning the whole area every set amount of time for components and then doing a for each loop through each one to find the npcs without my marker script attached to them.

I've gone through the daggerfallmnobileunit script, the mobileunit script, daggerfallenemy script, and the setupdemoenemy script. My nub eyes are not seeing any virtual classes to hook into.

Right now, this is how I do it:

Code: Select all

using DaggerfallWorkshop.Game;
using System.Collections.Generic;
using UnityEngine;
using DaggerfallWorkshop;
using System.Linq;
using System.IO;
using DaggerfallWorkshop.Utility.AssetInjection;

namespace Minimap
    public class NPCManager : MonoBehaviour
        public float npcUpdateTimer;
        public float npcUpdateInterval;
        public GameObject location;
        public List<MobilePersonNPC> mobileNPCArray = new List<MobilePersonNPC>();
        public List<DaggerfallEnemy> mobileEnemyArray = new List<DaggerfallEnemy>();
        public GameObject interiorInstance;
        public GameObject dungeonInstance;
        public List<StaticNPC> flatNPCArray = new List<StaticNPC>();
        public List<NPCMarker> currentNPCIndicatorCollection = new List<NPCMarker>();
        public int totalNPCs;

        #region GameTextureArrays
        public static int[] maleRedguardTextures = new int[] { 381, 382, 383, 384 };
        public static int[] femaleRedguardTextures = new int[] { 395, 396, 397, 398 };

        public static int[] maleNordTextures = new int[] { 387, 388, 389, 390 };
        public static int[] femaleNordTextures = new int[] { 392, 393, 451, 452 };

        public static int[] maleBretonTextures = new int[] { 385, 386, 391, 394 };
        public static int[] femaleBretonTextures = new int[] { 453, 454, 455, 456 };

        public int[] guardTextures = { 399 };
        public Dictionary<Minimap.MarkerGroups, Material> npcIconMaterialDict = new Dictionary<Minimap.MarkerGroups, Material>();
        public Texture2D npcDotTexture;

        private void Awake()
            npcDotTexture = null;
            byte[] fileData;

            fileData = File.ReadAllBytes(Application.dataPath + "/StreamingAssets/Textures/Minimap/npcDot.png");
            npcDotTexture = new Texture2D(2, 2);
            npcDotTexture.LoadImage(fileData); //..this will auto-resize the texture dimensions.

        private void Start()
            Material friendliesIconMaterial = new Material(Minimap.iconMarkerMaterial);
            Material enemiesIconMaterial = new Material(Minimap.iconMarkerMaterial);
            Material residentIconMaterial = new Material(Minimap.iconMarkerMaterial);

            npcIconMaterialDict = new Dictionary<Minimap.MarkerGroups, Material>()
                {Minimap.MarkerGroups.Friendlies, friendliesIconMaterial },
                {Minimap.MarkerGroups.Enemies, enemiesIconMaterial },
                {Minimap.MarkerGroups.Resident, residentIconMaterial },
                {Minimap.MarkerGroups.None, residentIconMaterial }

        void Update()
            //stop update loop if any of the below is happening.
            if (!Minimap.MinimapInstance.minimapActive)

            npcUpdateTimer += Time.fixedDeltaTime;
            if (npcUpdateTimer > Minimap.MinimapInstance.npcCellUpdateInterval)
                npcUpdateTimer = 0;
                //set exterior indicator size and material and grab npc objects for assigning below.
                if (!GameManager.Instance.IsPlayerInside)
                    location = GameManager.Instance.PlayerEnterExit.ExteriorParent;
                    mobileNPCArray = location.GetComponentsInChildren<MobilePersonNPC>().ToList();
                    mobileEnemyArray = location.GetComponentsInChildren<DaggerfallEnemy>().ToList();
                    flatNPCArray = location.GetComponentsInChildren<StaticNPC>().ToList();
                //set inside building interior indicator size and material and grab npc objects for assigning below.
                else if (GameManager.Instance.IsPlayerInside && !GameManager.Instance.IsPlayerInsideDungeon)
                    interiorInstance = GameManager.Instance.InteriorParent;
                    flatNPCArray = interiorInstance.GetComponentsInChildren<StaticNPC>().ToList();
                    mobileNPCArray = interiorInstance.GetComponentsInChildren<MobilePersonNPC>().ToList();
                    mobileEnemyArray = interiorInstance.GetComponentsInChildren<DaggerfallEnemy>().ToList();

                //set dungeon interior indicator size and material and grab npc objects for assigning below.
                else if (GameManager.Instance.IsPlayerInside && GameManager.Instance.IsPlayerInsideDungeon)
                    dungeonInstance = GameManager.Instance.DungeonParent;
                    flatNPCArray = dungeonInstance.GetComponentsInChildren<StaticNPC>().ToList();
                    mobileNPCArray = dungeonInstance.GetComponentsInChildren<MobilePersonNPC>().ToList();
                    mobileEnemyArray = dungeonInstance.GetComponentsInChildren<DaggerfallEnemy>().ToList();

                //count all npcs in the seen to get the total amount.
                totalNPCs = flatNPCArray.Count + mobileEnemyArray.Count + mobileNPCArray.Count;

            //if the total amount of npcs match the indicator collection total, stop code execution and return from routine.
            if (totalNPCs == currentNPCIndicatorCollection.Count)

            currentNPCIndicatorCollection.RemoveAll(item => item == null);

            mobileEnemyArray.RemoveAll(item => item == null);

            //find mobile npcs and mark as green. Friendly non-attacking npcs like villagers.
            foreach (DaggerfallEnemy mobileEnemy in mobileEnemyArray)
                if (!mobileEnemy.GetComponent<NPCMarker>() && mobileEnemy.isActiveAndEnabled)
                    NPCMarker newNPCMarker = mobileEnemy.gameObject.AddComponent<NPCMarker>();

            flatNPCArray.RemoveAll(item => item == null);

            //find mobile npcs and mark as green. Friendly non-attacking npcs like villagers.
            foreach (StaticNPC staticNPC in flatNPCArray)

                if (!staticNPC.GetComponent<NPCMarker>())
                    NPCMarker newNPCMarker = staticNPC.gameObject.AddComponent<NPCMarker>();

            mobileNPCArray.RemoveAll(item => item == null);

            //find mobile npcs and mark as green. Friendly non-attacking npcs like villagers.
            foreach (MobilePersonNPC mobileNPC in mobileNPCArray)
                if (!mobileNPC.GetComponent<NPCMarker>())
                    NPCMarker newNPCMarker = newNPCMarker = mobileNPC.gameObject.AddComponent<NPCMarker>();

Re: Detect NPC Spawns

Posted: Fri Apr 29, 2022 6:45 am
by Interkarma
GameManager.Instance.PlayerGPS.GetNearbyObjects() is designed to query objects like enemies with minimal overhead. See DetectEnemy.MagicRound() as an example.

I also mentioned this when discussing your token mismatch topic, relevant bit below. :)
Interkarma wrote: Thu Apr 14, 2022 12:54 am One other tip - you should check out the "GameManager.Instance.PlayerGPS.GetNearbyObjects()" method. This is a central helper that keeps track of common scene objects so they can be queried efficiently without needing to search scene yourself. This will also help track enemies that later pop into existence after your script has run (e.g. quest spawns, random spawns). See DetectEnemy script for an example of use. You could use this as the back-end and further track enemies in a dictionary using their LoadID if you need to add some other logic layer over it.
You can safely use GetNearbyObjects() every update if needed. It's always ticking in the back-end and finding objects for you, you're just querying the found objects from list. Use the enemy LoadID to manage uniqueness in a dictionary if you want to handle things going in and out of existence on your end. e.g. If an enemy LoadID doesn't exist in dict > add your component to enemy > add LoadID to dict. Then you're tracking which objects have your custom component attached.

At this time GetNearbyObjects() can query for the following object types based on NearbyObjectFlags.

Code: Select all

public enum NearbyObjectFlags
            None = 0,
            Enemy = 1,
            Treasure = 2,
            Magic = 4,
            Undead = 8,
            Daedra = 16,
            Humanoid = 32,
            Animal = 64,
So mobile or static NPCs aren't included, but I'm happy to add this support if it's an approach you want to use. :)

Re: Detect NPC Spawns

Posted: Fri Apr 29, 2022 4:50 pm
by l3lessed
Thank you for explaining that. It would be nice to be able to scan the area for any and all npc types, flats and friendlies.

I did notice in the notes on that it mentions a slower update cycle for detecting and adding to the object list. I wasn't sure how slow it meant.

It also mentions it touches pre-populated lists. Would this mean any npcs added through mods and not already within the games population records, it won't show up in the GetNearbyObjects list?

Re: Detect NPC Spawns

Posted: Fri Apr 29, 2022 10:13 pm
by Interkarma
The NearbyObjects list is repopulated 3 times a second with all the object types it searches for. This happens in UpdateNearbyObjects().

When you call GetNearbyObjects() it's not searching the scene. Rather it's just querying the pre-populated NearbyObjects list (which is updated 3 times a second). You can run GetNearbyObjects() every update if you need to because it's just a quick list query.

The goal is to centralise the heavy part of searching scene at a controlled rate so we don't end up with tens of different systems trying to find things in the scene in different ways and bogging down performance. Then the results of that controlled search are available to everyone via GetNearbyObjects() which is an efficient query that can be called anytime needed.

Hope that helps explain it.

Re: Detect NPC Spawns

Posted: Mon May 09, 2022 10:28 pm
by l3lessed
It does largely. I'm right with you on that thought. Scene queries are not CPU friendly at all, and why I was asking about this approach.

One last clarifying question. Does this mean it does not pick up anything mods add to the scene? I guess the question I should ask, do mods add content through the pre-populated list?

I can combine this with the population manager object to get current friendly list count when in cities and stop any updates when the population manager list isn't updated/changed.

Those two would cover enemies and friendly npcs, so I could minimize CPU calls via less scene queries.

However, the only one not covered with some form of a query list system is npc flats. Or, is there, and I'm missing it?

Long term, it would be helpful I feel for future mods developers, not only me, to be able to use GetNearbyObjects() call to get flats and friendlies too.

Re: Detect NPC Spawns

Posted: Fri May 13, 2022 5:13 am
by Interkarma
l3lessed wrote: Mon May 09, 2022 10:28 pm One last clarifying question. Does this mean it does not pick up anything mods add to the scene? I guess the question I should ask, do mods add content through the pre-populated list?
See UpdateNearbyObjects(). This does a scene search 3 times a second for DaggerfallEntityBehaviour (includes all enemies and civilian NPCs) and DaggerfallLoot. It also records helpful information like distance from player and flags about that object. I can easily add static NPCs to the search list.

When you call GetNearbyObjects(), this queries the resulting search list generated above based on your parameters. You can query this list every frame if you need to because it's just a cheap list query, not a scene search.

The point is that everyone can query the search list (which is updated at the fixed rate of 3 times a second in the back-end) without adding any additional scene search overhead.