Question about how the Dialogue works

Hey,

So I noticed for a long time that, every time I go up to an NPC to initiate a dialogue, I get a mini lag spike before the dialogue UI window appears.
In the Editor, this lag spike happens only once, but in a built game, I get this lag spike every time I initiate a conversation with an NPC.

Another issue in the built game is that, each time I get to a player choice, where nodes have to be spawned, depending on how many choice options I have, the game will have a lag spike as well.
I have moved to a better prefab spawning system that was covered by Brian in a different post of mine, but still, I remained with this issue.

You can imagine how annoying it is to experience those lag spikes every time you go and talk to an NPC, or have 3-4+ more choices to spawn. It makes you not want to talk to any NPC ever again.

While I can’t see why in a built game, those lag spikes happen every time that I talk to an NPC and when choice prefabs have to be enabled (because in the editor it happens only once), I took a quick look at the Profiler when I first talked to a dialogue NPC to see what happens.

I discovered that somehow, the PlayerConversant did something like “Loading.ReadObject”. When I checked the details, it seems that the 1st time I talk to an NPC, somehow the game is loading all of my resource folders.
Is this because this is needed in order to know which dialogue nodes to get / load?
I don’t really know if this resource loading is happening all the time in a built game and it’s why the game stutters, but I just found it strange.
Also, I know I have to go into my project files and tidy up a bit, but I still do not understand why those stutters occur.

Here is a quick screenshot of the Profiler:

Thanks!

I’m a bit confused as to why PlayerConversant.Update() would be showing (unless you’re using my IAction setup to have the player move to the speaker, but that rabbit trail would lead to Mover getting spammed with StartActions)

The only things we have in the Resources folders are Quests and Inventory Items. It’s possible that thet HasQuest, CompletedQuest, and HasInventoryItem conditions are causing this. Can you run this same profile on a Dialogue that has no conditions in it whatsoever (build one for test if need be)?

There are a few things I can think of to optimize PlayerConversant right out of the gate that may help. One is dealing with getting IPredicateEvaluators. The list of IPredicateEvaluators shouldn’t change over time, since they’re all components attached to the player. It’s a small change, but we’re getting that list several times in a single dialogue (because each condition needs testing)…

List<IPredicateEvaluator> evaluators = new List<IPredicateEvaluator>();

void Awake()
{
    evaluators = GetComponents<IPredicateEvaluator>().ToList();
}

//replace GetEvaluators with
List<IPredicateEvaluators> GetEvaluators() =>evaluators; 
//alternatively, refactor and replace all references to GetEvaluators with evaluators.

This isn’t a mind shattering optimization, but GetComponents does have a cost, IEnumerables are reevaluated every time they hit a foreach loop.

Hey Brian!

Yes, I am indeed using your method in Update to actually move to the NPC and then start the dialogue.

I just created quick dialogue with 5 choices but no conditions at all, like you suggested and I must say that I saw no Loading.ReadObject when i started to talk to the NPC.
I also replaced the GetEvaluators with what you suggested.
After I talked to the NPC that had no conditions inside the dialogue, I went to an NPC that has conditions in its dialogue, and as you said, that’s when the PlayerConversant Update was triggered and the Loading.ReadObject was triggered.

SO it seems that conditions might be the issue here?

I just built the game real quick.
So, when I start the dialogue with the NPC that has a “clean” dialogue with no conditions, there are no stutters. Not when I first start the dialogue, nor when I get to the choice part where there are 5 choices being activated.
But when I go to the NPC that has a dialogue with 5+ choices and some of them have conditions, that’s when the stutter kicks in. It occurs every time that I start the dialogue, or when I get to pick choices.

I tried to transfer the move logic from the PlayerConvesant to the AIConversant, where the Raycast handler is. Now the Loading.ReadObject is being called from the PlayerController.Update().
So I have no idea what to do next.

No, the move logic belongs in the PlayerConversant, the Update() showing up in the results is because we go from the Update to actually getting a choice, it’s cycling the choices through the evaluators that’s killing us…

The trouble is I don’t see a clean way to refactor the conditions or the handling of them for speed. I’ll have a think on this over the weekend.

This is an interesting case, since the issue appears (from the profiler) to be that the system is loading/unloading resources on a regular basis. What’s puzzling is why it would choose to do that.

You have a lot more inventory items and are already set up with the profiler. You also have a system that is more sensitive to bottlenecks than mine. That makes you the perfect guinea pig for some more profiler tests, and together, we’ll try to get to the bottom of this…

Let’s go back to the Shop issue we tackled in the other post. The question I have is this: Are the resources being reloaded every time the shop is opened/refreshed? To test this, set up a shop with one of every single item you have in your game. The idea here is we want to push the limits. See if when you open a shop with all the inventory items available if you get this load hit. Then see if this load hit continues when you switch between Buying to Selling (to reduce the number of hits).

Along with this test (since there’s a certain amount of latency between you and I posting stuff, both of us having “lives” and all), one more experiment on the Dialogues… We’ve already demonstrated that no Conditions == no lag spike… Do an experiment with each type of condition… So… fill it with 5 “HasItem” condions, then change those conditions to HasQuest conditions, then maybe HasLevel or other conditions that don’t require loading resources, and for the ultimate test, 5 conditions of XYZZY (or any other nonsense word that is NOT a condition covered by any of your IPredicateEvaluator)

Hey Brian,
Thanks for taking your time to help me out!
So I ran your tests, For the Shop test, I’ve put in most of the items that I had made, and I only saw just once a Loading.ReadObject but it had loaded like 3 images that belonged to 3 items. Switching between buying and selling did not trigger any loads. I even stopped the game the played again, opened the shop and there was no sign of any Load after.

Moving on to the dialogues, here is where I found something interesting. During tests, the only predicates that triggered a Loading.ReadObject were any of the QuestList predicates, meaning the HasQuest, CompletedQuest, etc.
I tried with inventoryItems, I saw no resource loading. I even tried with just 1 HasQuest condition and it did all the resource loading of like every Resource folder in my project (some third party assets have their own Resource folders).
So I think that the main culprits here are the Quest predicates. I even tried to go into the Quest.cs script, where the HasQuest method is and we do a Resources.LoadAll("") and I tried to specify the exact path of the quests SO folder, like Assets/Game/Resources/Quests.

What happened next were 2 things:

  1. There was no more Loading.ReadObjects in the profiler when I started a dialogue that had any Quest related predicates.
  2. Those predicates were not working anymore…
    I had the impression that you can specify the exact path where to load, and maybe it would stop doing the massive resource loading.

But I think it is safe to say that somehow the Quest predicates are the ones that are causing this issue. For me at least.
If there a way to move the HasQuest method out from the Quest SO script, like into the QuestList where we could somehow cache all the quest SO resources from the folder? I don’t know how I could do that, but if we could cache the quest resources on awake, then just check against the cached list?
Now I am thinking that if we cache the quests… we can’t really have an updated version of them… And if we do a CompletedObjective, those cache quests will never have said objective complete…
Finally, I can’t understand why quest predicates would do this, but InventoryItems do not, since we use a Resource.LoadAll there as well.

OK, quick update.

I went ahead and applied the caching method we do in InventoryItems, and I still saw a Load.ReadObjects in the editor, but no visual lag spike.
I then built the game, and to my surprise, I had no more lag spikes when I talked to the NPCs that had nodes with Quest predicate conditions.

So I guess that caching the quests might have solved one issue, which was the stutters.
But I am still puzzled by the fact that the Quest predicates load ALL off the resources available, instead of just loading the quests. And even then, I don’t know why the Inventory predicates do not show such loading in the Profiler.

Um… HasQuest should be in QuestList already (it is in the course repo). The Quests themselves do handle the static Quest.GetByName(), which is technically where that belongs…

I did notice, however, that we’re not doing the same thing with regards to Quest retrieval as we do with InventoryItem retrieval… For InventoryItems, the first time we access an InventoryItem, the items are cached in a Dictionary. For Quest items, we do no such thing. Each and every time we try to get Quest.GetByName(), the resources are loaded.

Resources.LoadAll<T>() has to load all items in the Resource folder, and then determine “Is this a T”? and return it.

Take a look at Inventory.cs, and note how the Dictionary is implemented and built, and try to apply that logic to Quest.GetByName(). (I’m pretty confident you’ll apply that easily). I have to go to my weekly breakfast with my folks, but when I get back, I’ll post my solution to caching the Quests (in a static dictionary in Quest).

I actually took a moment to cobble together a quick cache method based on Inventory.

        private static Dictionary<string, Quest> masterQuestDictionary;
        
        public static Quest GetByName(string questName)
        {
            if (masterQuestDictionary == null)
            {
                masterQuestDictionary = new Dictionary<string, Quest>();
                foreach (Quest quest in Resources.LoadAll<Quest>(""))
                {
                    if(masterQuestDictionary.ContainsKey(quest.name)) Debug.Log($"There are two {quest.name} quests in the system");
                    else masterQuestDictionary.Add(quest.name, quest);
                }
            }
            return masterQuestDictionary.ContainsKey(questName) ? masterQuestDictionary[questName] : null;
        }
2 Likes

I think you were posting this as I was posting my solution above. But I checked the course repo and in Quest.cs which is the SO, we have the static method GetByName. Thant’s why I asked. It’s in there that we do the resource loading.
And there is where I did the caching like in InventoryItem.cs

I did the exact same thing, but with different naming. And like I said, it seems to work great now. No more stuttering in a built game or in the editor.
Thanks once again Brian! I will mark this as closed, and maybe we can make sure other people can see it, they will for sure run into the same problems.

Have a nice weekend!

Ok, now I task thee with a quest:

  • Find any other bottlenecks in the code
  • Report them to me so that we may squash them like the bugs they are.

Seriously, though, I’m glad we got that one covered. In my personal projects, I use resources for a LOT of things… in Beneath, virtually every enum is a ScriptableObject. I didn’t go that far in my current project, but it still uses a lot of Resources folder based ScriptableObjects… I never encountered these bottlenecks because they all use the same caching method. Here’s how I accomplished that, from something I did two years ago when the Inventory course first came out:

1 Like

Thanks Brian!
That post is really really interesting and I will try to use that from now on, whenever I need to load resources!
I always try to keep an eye out whenever I feel that the game is starting to perform poorly whenever I try to do something. I always keep an eye on the Stats window and I have the Profiler ready to go.

Besides these two issues, that I would consider major, the Redraw and then the conditions, I haven’t ran into any other issues. Oh and that small issue with the Adding to transaction in shop, that would trigger the redraw * times the number of items you had in the current transaction. There still is some kind of thing that causes a large number of calls for gameObject.Deactivate, but I tested it by buying 100 potions, and the lag was just barely noticeable, like a 100ms spike, which is way better than a 3-5 second lag.

I’m really glad we got to solve those 2 because they were a real pain in butt. Especially using a shop, It was a nightmare and it made me not want to play anymore every time that I was trying to test something involving the Shops.
And with the dialogues, now that we solved the issue with the Quest predicates, and with the implementation of the new way to populate choices, using the new Redraw methods, it’s a delight talking to NPCs now.
For me, exploration will be the major focus for my game. It will have combat, and with the awesome course modular ability system, I’ve put together some nice systems but, I will want to focus more on exploring and question, interacting with the world and conditions are a major part for this. I have implement quite a lot of systems and I’ve also implemented the IPredicateEvaluator in them so that I can create interesting combinations.

I will keep an eye out for any other bottlenecks and will report them here!

1 Like

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms