IntoTheDev / MultiTag-System-for-Unity

This package allows you to Tag Game Objects with ScriptableObjects

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question] A few questions about usage and typical setup

MostHated opened this issue · comments

-- Edit: It feels like something is missing from this package to tie it all together. Did you perhaps forget to upload something? I have been checking your other repos that you have, and I see one of them has a TagCreator, a TagType, and an interface called IBranch (in which I can't find the actual implementation anyhere) that this repo doesn't have. I cannot figure out how I am supposed to reference the actual tags in code to check, add, remove them, etc aside from manually placing them into the container or onto a GameObject that has the taggable class.

Hey there,
I came across this and thought it sounded like really good idea, especially for my use case of checking tags on hundreds of waypoints in the editor to draw different things for my waypoint tools. I am trying to piece together exactly how to use it, though. I would venture a guess that I need to modify it slightly, probably adding [ExecuteInEditMode] for some of it to work in the editor.

I took the included "New Tag" and called it "Waypoint", added it in the TagContainer, added the "Taggable" script to a waypoint and then added the Waypoint tag object to it. I had a few questions from here, though.

My first one is, I see that there is the ability to make multiple tag containers, and there is a HasTags method that takes in a container, which looks like it checks the container for an entity. What is an example scenario in which you might use multiple containers? Do you reuse the already created tags within multiple containers? If so, it sort of sounds like ECS's Archetypes.

My second question is, do you have a quick example of the proper use to check/add tags? My first instinct was to do waypointList[i].GetComponent() as I was looping through my waypoints but I feel like that sort of defeats the purpose and I could just check for my Waypoint class that is already there, so I would imagine there is a different/better way to do this that I am just not aware of? I tried to do

TagExtensions.HasTag(waypointList[i].gameObject, ?? );

I am not sure exactly where I am supposed to get the reference to a specific tag from, in order to pass it in?

I definitely appreciate your work on this, I am excited to see if it might help out a bit on performance instead of checking all of the normal built-in tags.
Thanks for your time,
-MH

Tag Creator is just needed to quickly create tags in the folder I need.
image

Thanks for the clarification, I do appreciate it. A little while after I messaged I tried a few things out.

In TagExtensions I added the following:

        public static Tag waypointTag = Resources.FindObjectsOfTypeAll<Tag>().FirstOrDefault(x => x.name.Equals("WPTag"));
        public static Tag wpVehTag = Resources.FindObjectsOfTypeAll<Tag>().FirstOrDefault(x => x.name.Equals("VWPTag"));
        public static Tag wpPedTag = Resources.FindObjectsOfTypeAll<Tag>().FirstOrDefault(x => x.name.Equals("PWPTag"));

Then in my waypoint container class, I added this switch to test it out, :

for (var i = 0; i < waypointList.Count; i++)
{
    switch (waypointClass)
    {
        case WaypointClass.Vehicles when !TagExtensions.HasTag(waypointList[i].gameObject, TagExtensions.wpVehTag):
            TagExtensions.AddTag(waypointList[i].gameObject, TagExtensions.wpVehTag);
            break;
        case WaypointClass.Pedestrians when !TagExtensions.HasTag(waypointList[i].gameObject, TagExtensions.wpPedTag):
            TagExtensions.AddTag(waypointList[i].gameObject, TagExtensions.wpPedTag);
            break;
    }

    var hasvehwp = TagExtensions.HasTag(waypointList[i].gameObject, TagExtensions.wpVehTag);
    var haspedwp = TagExtensions.HasTag(waypointList[i].gameObject, TagExtensions.wpPedTag);
    Debug.Log($"WP: {waypointList[i].name} Has Tag? Veh:{hasvehwp} Ped:{haspedwp}");
}

Everything worked well after that point.

I am currently using a few different methods for different parts of my waypoint tooling (such as knowing which lines/colors to draw where, checking which waypoint type one is for my connection tool, etc)
https://i.imgur.com/YiZk902.gifv

I am excited to try out some benchmarks and see if there is any difference. Honestly, even if the performance ended up being the same, just having the ability to have multiple tags, of which I can quickly add and remove this easily, will be a huge help in reducing the necessary complexity for all my different checks.

Thanks again for the info, and for sharing this project. After seeing what you shared, I am going to make a few changes to how I am doing it currently, then things should be great. 👍
-MH

I tried optimizing the code you wrote above. I will be happy to learn about the performance results.

[SerializedField] private Tag wpVehTag = null;
[SerializedField] private Tag wpPedTag = null;

private void YourMethod()
{
    int count = waypointList.Count;
    
    for (var i = 0; i < count; i++)
    {
        var waypoint = waypointList[i].gameObject;
        bool hasvehwp = waypoint.HasTag(wpVehTag);
        bool haspedwp = waypoint.HasTag(wpPedTag);
        
        switch (waypointClass)
        {
            case WaypointClass.Vehicles when !hasvehwp:
                waypoint.AddTag(wpVehTag);
                hasvehwp = true;
                break;
            case WaypointClass.Pedestrians when !haspedwp:
                waypoint.AddTag(wpPedTag);
                haspedwp = true;
                break;
        }
    
        Debug.Log($"WP: {waypoint.name} Has Tag? Veh:{hasvehwp} Ped:{haspedwp}");
    }
}

Ok, so here is what I came up with:

Test was ran with 1000 active GameObject waypoints in scene, then each test ran 100 times.
I tried to test in a manner that might resemble something that someone might actually do, lol.
Results are ElapsedMilliseconds / 1000f and ElapsedTicks / 100f.

All in all, the results are great.
Nearly 4x as fast as tag == "string" (which isn't saying much, really)
But basically 2x as fast as go.CompareTag("Tag")
👍

Typical result

Test1: Comparison using '=='
Test2: Comparison using .CompareTag()
Test1: Comparison using ScriptableObject Tagging

[Log][09:23:18]
Test1 time: 0.04ms (4064.89 ticks) Count: 100000
Test2 time: 0.024ms (2426.44 ticks) Count: 100000
Test3 time: 0.012ms (1235.77 ticks) Count: 100000
Results from 10 runs in succession
[Log][09:23:18]
Test1 time: 0.04ms (4064.89 ticks) Count: 100000
Test2 time: 0.024ms (2426.44 ticks) Count: 100000
Test3 time: 0.012ms (1235.77 ticks) Count: 100000

[Log] [09:23:37] 
Test1 time: 0.038ms (3857.31 ticks) Count: 100000
Test2 time: 0.022ms (2256.76 ticks) Count: 100000
Test3 time: 0.012ms (1203.38 ticks) Count: 100000

[Log] [09:23:40] 
Test1 time: 0.039ms (3948.82 ticks) Count: 100000
Test2 time: 0.022ms (2279.46 ticks) Count: 100000
Test3 time: 0.011ms (1185.3 ticks) Count: 100000

[Log] [09:23:41] 
Test1 time: 0.038ms (3813.32 ticks) Count: 100000
Test2 time: 0.022ms (2232.82 ticks) Count: 100000
Test3 time: 0.011ms (1172.94 ticks) Count: 100000

[Log] [09:23:42] 
Test1 time: 0.039ms (3955.35 ticks) Count: 100000
Test2 time: 0.023ms (2398.23 ticks) Count: 100000
Test3 time: 0.012ms (1221.21 ticks) Count: 100000

[Log] [09:23:43] 
Test1 time: 0.038ms (3830.27 ticks) Count: 100000
Test2 time: 0.023ms (2325.18 ticks) Count: 100000
Test3 time: 0.012ms (1257.11 ticks) Count: 100000

[Log] [09:23:44] 
Test1 time: 0.037ms (3788.95 ticks) Count: 100000
Test2 time: 0.022ms (2269.83 ticks) Count: 100000
Test3 time: 0.011ms (1194.56 ticks) Count: 100000

[Log] [09:23:45] 
Test1 time: 0.04ms (4028.16 ticks) Count: 100000
Test2 time: 0.023ms (2310.31 ticks) Count: 100000
Test3 time: 0.012ms (1220.13 ticks) Count: 100000

[Log] [09:23:45] 
Test1 time: 0.039ms (3947.15 ticks) Count: 100000
Test2 time: 0.023ms (2304.94 ticks) Count: 100000
Test3 time: 0.012ms (1214.33 ticks) Count: 100000

[Log] [09:23:46] 
Test1 time: 0.038ms (3812.68 ticks) Count: 100000
Test2 time: 0.023ms (2310.99 ticks) Count: 100000
Test3 time: 0.012ms (1255.62 ticks) Count: 100000
Code used to test
[Button("Run Performance Test")]
public void PerformanceGauge()
{
    // ------------------------------------------------------------------------------ Test1
    // -- Waypoint Test1 Comparison using '==' --------------------------------------------
    var test1Tagged = new Dictionary<GameObject, bool>();
    var test1Count = 0;
    var test1Stopwatch = new Stopwatch();
    test1Stopwatch.Start();
    for (int w = 0; w < 100; w++)
    {
        for (int i = 0; i < waypointList.Count; i++)
        {
            test1Tagged.Add(waypointList[i].gameObject, waypointList[i].gameObject.tag == "Waypoint");
            test1Count++;
        }

        test1Tagged.Clear();
    }
    test1Stopwatch.Stop();

    // ------------------------------------------------------------------------------ Test2
    // -- Waypoint Test2 Comparison using .CompareTag() -----------------------------------
    var test2Tagged = new Dictionary<GameObject, bool>();
    var test2Count = 0;
    var test2Stopwatch = new Stopwatch();
    test2Stopwatch.Start();
    for (int w = 0; w < 100; w++)
    {
        for (int i = 0; i < waypointList.Count; i++)
        {
            test2Tagged.Add(waypointList[i].gameObject, waypointList[i].gameObject.CompareTag("Waypoint"));
            test2Count++;
        }

        test2Tagged.Clear();
    }
    test2Stopwatch.Stop();

    // ------------------------------------------------------------------------------ Test3
    // -- Waypoint Test3 Comparison using ScriptableObject Tagging ------------------------
    var test3Tagged = new Dictionary<GameObject, bool>();
    var test3Count = 0;
    var test3Stopwatch = new Stopwatch();
    test3Stopwatch.Start();

    for (int w = 0; w < 100; w++)
    {
        for (int i = 0; i < waypointList.Count; i++)
        {
            test3Tagged.Add(waypointList[i].gameObject, waypointList[i].gameObject.HasTag(TagExtensions.wpPedTag));
            test3Count++;
        }

        test3Tagged.Clear();
    }
    test3Stopwatch.Stop();

    var output = "Test1: Comparison using '=='\n" +
                 $"Test1 time: {test1Stopwatch.ElapsedMilliseconds / 1000f}ms ({test1Stopwatch.ElapsedTicks / 100f} ticks) Count: {test1Count}\n" +
                 "Test2: Comparison using .CompareTag()\n" +
                 $"Test2 time: {test2Stopwatch.ElapsedMilliseconds / 1000f}ms ({test2Stopwatch.ElapsedTicks / 100f} ticks) Count: {test2Count}\n" +
                 "Test1: Comparison using ScriptableObject Tagging\n" +
                 $"Test3 time: {test3Stopwatch.ElapsedMilliseconds / 1000f}ms ({test3Stopwatch.ElapsedTicks / 100f} ticks) Count: {test3Count}\n";
    
    Debug.Log(output);
}

Very good result! :) If I'm not mistaken, you are currently getting tags using Resources.FindObjectsOfTypeAll() and Linq. I didn't compare in terms of performance, but I guess it will be faster to make a Tag Field and pass the desired tag to it through the inspector. In any case, I am very glad that my package was able to help you, good luck with your projects! :)

I had only set it up that way to quickly test it out before I really knew how it did/should work. I am going to make some sort of tag manager similar to what you are recommending so I can better manage and access everything. 👍

I was going through some of my code and converting things over to use these tags when I decided I wanted to test one more thing I used quite often, and I was a bit surprised to see the results.

I moved the SO tags down to test #4, and test #3 is now the following:
I am not sure yet if that is just because it's using an enum, but I use those a lot throughout my code, so that is just another thing in which I can switch over to using SO tags and cut it's time in half.

I actually want to post these results on the Unity forums, just to share with folks. I am guessing you are ok with me linking directly to this package?

for (int w = 0; w < 100; w++)
{
    for (int i = 0; i < waypointList.Count; i++)
    { 
        test3Tagged.Add(waypointList[i].gameObject, waypointList[i].gameObject.GetComponent<Waypoint>().waypointClass == WaypointClass.Vehicles);
        test3Count++;
    }

    test3Tagged.Clear();
}
Test1 time: 0.039ms (3992.57 ticks) Count: 100000
Test2 time: 0.023ms (2344.68 ticks) Count: 100000
Test3 time: 0.026ms (2652.61 ticks) Count: 100000
Test4 time: 0.012ms (1234.35 ticks) Count: 100000

No problem, when you create a topic on the forum just put a link here so I can take a look :)

Sure thing, here it is. I added in another test (GetComponent<>().Equals(i), as well as GetComponent<>() == i) just to see any differences.

https://forum.unity.com/threads/some-comparison-results-testing-comparetag-equals-getcomponent-scriptableobject-tags.940575/

Hey again, I wanted to share a bit of my experience I have had today. I have been working with the system and I am not sure if maybe I am just not understanding something that I should be understanding, or there is just something goofy going on, but I have been fighting with it all day today and last night. It keeps seeming like it wants to work properly but then decides it has had enough. Though, at this point, I have tried, changed, tried again, changed again so many things, I can't even remember how it was supposed to be setup to begin with.

The first and most major thing was possibly due to my having not used Odin's AssetList before, but that is what caused me to start messing with other things. I do understand that with the AssetList attribute that it shows all of the files of that particular type and then the ones that have a check on the box means it is actually added to the collection. The thing is, though, I have never seen them become checked when adding a new tag to an object via code, only when I manually check the box. I try to do most everything possible via code, and not through UI. For AssetList, though, that might just be part of the design of that attribute, though, so I removed it.

This caused me to start looking more at the tags wondering why they were not becoming part of the collection when I would do:

go.AddTag(TagClass.tag);

I started looking at the actual HashSet in the inspector and noticed that the number of hashes in there was up into the thousands. I then noticed that initially the private int _hash is used from the start, but then in OnValidate(), a local hash is used, which gets the hash again, but even though it seems like that would be a fairly safe value to use, the value ended up being pretty far from consistent, especially considering that OnValidate seems to call every time there is a change in data. Being that I am using things in the editor for the most part and not at runtime (yet, I will eventually), between awake, OnValidate, and compile domain reloads/compilations, I thought it might be best to try and just get the hash once for each waypoint and continue using the same one, which eventually brought the number of hashes in the tags set down to a more reasonable number, and they actually matched up.

I was then trying to figure out if when I pass the actual tag/tag container to the target, if I needed to manually assign it to something within Taggable. At first glance, it seemed like calling go.AddTag(TagClass.tag) and passing the tag into it would then be all that needed to be done, but that never worked. So what I tried to do was move the code from OnValidate into another method, making Tags[] a property and then upon _tags being set/written, I would call the code that was in OnValidate when I pass in a set of tags.

Here is pretty much where it starts, where I was looping through the waypoints to see if they needed the tags on them.

WaypointManager
// WaypointManager.cs
// ---------------------------------------


var wpList = waypointContainers[i].RefreshWaypoints();
foreach (var t in wpList)
{
    if (t.waypointHash == 0)
    {
        t.waypointHash = t.GetHashCode(); // -- Setting the waypoint hash
        t.gameObject.SetHash(t.waypointHash);
    }

    if (t.connections.Count == 0) Debug.LogError($"Waypoint {t.name} missing connections!");
    try
    {
        switch (t.waypointClass)
        {
            case WaypointClass.Vehicles when !t.gameObject.HasTag(tg.wpVehTag):
                newTags = new[] 												
                {
                    TagConfig.Instance.waypointTag,
                    TagConfig.Instance.wpVehTag
                };
                t.gameObject.AddTags(newTags); // -- 1. Creating a new tags array, and using .AddTags ---------------
                break;
            case WaypointClass.Pedestrians when !t.gameObject.HasTag(tg.wpPedTag):
                newTags = new[]
                {
                    TagConfig.Instance.waypointTag,
                    TagConfig.Instance.wpPedTag
                };
                t.gameObject.AddTags(newTags);
                break;
        }
    }
    catch (Exception e)
    {
        Debug.Log(e);
        throw;
    }

    waypointList.Add(t);
}

Clicking within the project and other places were causing an issue as some OnSceneGui things were now trying to use the tags, so these changes helped get rid of those.

TagExtensions
// TagExtensions.cs
// ---------------------------------------
// 
   
 public static class TagExtensions
    {
     public static void AddTags(this GameObject entity, Tag[] tags)
        {
            if (!entity.activeInHierarchy) return;
            entity.GetOrAddComponent<Taggable>().Tags = tags; // -- 2. Adding the actual tags to the _tags in Taggable ---------------
        }													 		 // -- which then runs the RefreshTagData() method in Taggable ---------------

        public static void SetHash(this GameObject entity, int hash)
        {
            if (!entity.activeInHierarchy) return;
            entity.GetOrAddComponent<Taggable>().Hash = hash; // I was using a persisten hash as Waypoint ID, so just passing and using that.
        }
    }

Some modifications I was trying out

Taggable
// Taggable.cs
// ---------------------------------------

        [SerializeField, ReadOnly] private int _hash;
        [SerializeField] public Tag[] _tags;

        public int Hash
        {
            get => _hash;
            set => _hash = value;
        }

        public Tag[] Tags
        {
            get => _tags; 
            set
            {
                _tags = value; // -- 3. Tags gets set here ---------------
                RefreshTagData();
            }
        }

        public void RefreshTagData() // -- 4. Which then calls here  ---------------
        {
            try
            {
                foreach (var t in TagConfig.Instance.allTags) // TagConfig is an Odin derived GlobalConfig ScriptableObject
                    if (!ArrayUtility.Contains(_tags, t))
                    {
                        t.Remove(Hash);
                    }

                for (var i = 0; i < _tags.Length; i++)
                    _tags[i].Add(Hash);
            }
            catch (Exception e)
            {
                Debug.LogException(e);
            }
        }

Here is where I was storing the reference to the Tags and serving them throughout the editor.

TagConfig
// TagConfig.cs
// ---------------------------------------
	[Serializable]
    [ShowOdinSerializedPropertiesInInspector]
    [GlobalConfig("Assets/instance.id/ECS/Objects/System/Settings", UseAsset = true)]
    public class TagConfig : GlobalConfig<TagConfig>, ISerializationCallbackReceiver
    {
        [SerializeField] [AssetList] public Tag[] allTags;
        [SerializeField] public Tag waypointTag;
        [SerializeField] public Tag wpVehTag;
        [SerializeField] public Tag wpPedTag;
        [SerializeField] public Tag wpConTag;
        [SerializeField] public Tag wpConVTag;
        [SerializeField] public Tag wpConPTag;
        [SerializeField] public Tag selected;
        [SerializeField] public TagsContainer WPTagContainer;
        [SerializeField] public TagsContainer WPPContainer;
        [SerializeField] public TagsContainer WPVContainer;
        [SerializeField] public TagsContainer WaypointContainer;
        [SerializeField] public TagsContainer WPGroupContainer;

#if UNITY_EDITOR
        [HideInInspector]
        [Button("Save Data", ButtonSizes.Medium), PropertyOrder(-1)]
        private void SaveData()
        {
            EditorUtility.SetDirty(this);
            AssetDatabase.SaveAssets();
        }

        [Button("Update Data", ButtonSizes.Medium), PropertyOrder(-1)]
        [MenuItem("Tools/instance.id/Save Tags")]
        public void UpdateTagConfig(bool search = false)
        {
            if (search) allTags = Resources.FindObjectsOfTypeAll<Tag>();
        }
#endif
}

I am considering just wiping out the changes I made and reverting it back to how it was out of the box, but I mostly just wanted to get your input on if any of the things I was attempting to do to try and do made any sense for this package, or if I was going about it the wrong way due to missing something with the overall architecture of using scriptableobject. I feel like maybe I am misunderstanding a concept or something here and it is throwing me off, lol.

Thanks,
-MH

Install the package again and change the code of these scripts. I hope this helps.

Taggable

using Sirenix.OdinInspector;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace ToolBox.Tags
{
	[DisallowMultipleComponent]
	public sealed class Taggable : MonoBehaviour
	{
		[SerializeField, Required, AssetList] private Tag[] _tags = default;

		private int _hash = 0;

		private void Awake() =>
			_hash = gameObject.GetHashCode();

#if UNITY_EDITOR
		private void OnValidate()
		{
			var obj = gameObject;

			if (!obj.activeInHierarchy)
				return;

			int hash = obj.GetHashCode();
			for (int i = 0; i < _tags.Length; i++)
				_tags[i].Add(hash);
		}

		private void OnDestroy()
		{
			int hash = gameObject.GetHashCode();

			for (int i = 0; i < _tags.Length; i++)
				_tags[i].Remove(hash);
		}
#endif

		private void OnEnable()
		{
			for (int i = 0; i < _tags.Length; i++)
				_tags[i].Add(_hash);
		}

		private void OnDisable()
		{
			for (int i = 0; i < _tags.Length; i++)
				_tags[i].Remove(_hash);
		}

#if UNITY_EDITOR
		public void UpdateTags()
		{
			var tags = Resources.FindObjectsOfTypeAll<Tag>();
			int hash = gameObject.GetHashCode();

			foreach (var tag in tags)
			{
				bool hasTag = tag.HasEntity(hash);
				bool inArray = ArrayUtility.Contains(_tags, tag);

				if (!hasTag && inArray)
				{
					ArrayUtility.Remove(ref _tags, tag);
					continue;
				}

				if (hasTag && !inArray)
				{
					ArrayUtility.Add(ref _tags, tag);
					continue;
				}
			}
		}
#endif
	}
}

TagEditor

using Sirenix.OdinInspector;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace ToolBox.Tags
{
	[InitializeOnLoad]
	public class TagEditor
	{
		static TagEditor() =>
			Selection.selectionChanged += OnSelectionChanged;

		public static void OnSelectionChanged()
		{
			var obj = Selection.activeGameObject;

			if (obj == null)
				return;

			var taggable = obj.GetComponent<Taggable>();

			if (taggable != null)
				taggable.UpdateTags();
		}
	}
}

Tag

using Sirenix.OdinInspector;
using System.Collections.Generic;
using UnityEngine;

namespace ToolBox.Tags
{
	[CreateAssetMenu(menuName = "ToolBox/Tagging/Tag"), AssetSelector, Required]
	public sealed class Tag : ScriptableObject
	{
		[ShowInInspector, ReadOnly] private HashSet<int> _entities = new HashSet<int>();

		public void Add(int entity)
		{
			_entities.Add(entity);

#if UNITY_EDITOR
			TagEditor.OnSelectionChanged();
#endif
		}

		public void Remove(int entity)
		{
			_entities.Remove(entity);

#if UNITY_EDITOR
			TagEditor.OnSelectionChanged();
#endif
		}

		public bool HasEntity(int entity) =>
			_entities.Contains(entity);
	}
}

I made the changes you recommended and have been using it for about an hour or so and have not had a warning or error since, and the hashes of the tags look right-on and all that. Looks like things are good now. I definitely appreciate it. 👍

I figured out something else that was causing me issues which I didn't realize at the time. I figured it might be a good idea to share it, just in case anyone else runs into something similar down the road.

My waypoints are all prefab variants and I am using Addressables, so a lot of my things are connected at the underlying prefab original level via AssetReferences, then the GameObjects are assigned to each other via script by finding the AssetReference's instance.

I am assigning things programmatically, such as which tags the waypoints are supposed to have (if it's a vehicle or pedestrian waypoint, etc) based on which waypoint container (unrelated to TagContainer) the waypoints are a child of, but when editing the waypoints I use a Scope asset that adds some convenience functions:

 using (var prefab = new LoadPrefabContentsScope(path))
{
	... edit prefab here
}
Scoped prefab editing
public sealed class LoadPrefabContentsScope : IDisposable
	{
		public string     PrefabPath { get; }
		public GameObject Prefab     { get; }

		public bool IsSave { get; set; } = true;

		public LoadPrefabContentsScope( string prefabPath )
		{
			PrefabPath = prefabPath;
			Prefab     = PrefabUtility.LoadPrefabContents( prefabPath );
		}

		public void Dispose()
		{
			if ( IsSave )
			{
				PrefabUtility.SaveAsPrefabAsset( Prefab, PrefabPath );
			}

			PrefabUtility.UnloadPrefabContents( Prefab );
		}
	}

Because of this, for some reason, whenever I was adding tags/containers to the prefab asset, the objects that were supposed to be selected/added to the _tags AssetList didn't get saved to the original prefab. I would run my assignments and at the time the prefab instances being used would work just fine, but the waypoints original prefab asset would show zero items selected/in the _tags array, some of them might have one, when all of them should have had two, so when they would get reloaded next time, it would not have the correct tags/containers.

I removed AssetList and made a few other small changes, which then allowed the array to be properly updated on the original prefab, which then allowed the actual waypoint instances being used to have all of the proper tags and containers.