Hey,
I hope someone can help out with this, because I am at my wits end.
I had a suspicion when I first began the course and got to the parts where we were building UI and started doing the Redraw() methods.
Now that I had to go back and actually revamp and design my UI, my fears came true.
To give some context, let’s take the Shop UI as an example. This is more to explain the process to people that might not understand at first on how this “dynamic” UI works:
This is the Redraw method in there (a part of it):
foreach (Transform child in listRoot)
{
//Debug.Log($"Destroying {child.name}");
Destroy(child.gameObject);
}
foreach (ShopItem item in currentShop.GetFilteredItems())
{
RowUI row = Instantiate(rowPrefab, listRoot);
row.Setup(currentShop, item);
}
So the scenario is as follows, for a mild content populated “RPG” game:
- You arrive after some hours of gameplay to a town and decide to view the shops in the area;
- Since you were away exploring for a while, you ran short oh healing salves and you find an apothecary that has a total number of 100 small healing salves available;
Now you will start to click the + button (Add) button as many times as you need to get to the number of total potions that you want to purchase.
Now, our Shop UI has a reference to the currentShop, and we are actually subscribing to the shops onChange action. The Add() method (as well as the Remove() method) of the button adds the selected item to the current transaction. The adding of the item to the transaction is triggering the onChange action event from the Shop script.
Since the ShopUI Redraw method is subscribed to the onChange action of the Shop, this means that each time that you click the + button, the action is triggered and the ShopUI’s Rerdaw method is called. What this means is that every shop row that contains the information of the items in the shop, they all get at first, destroyed, then for each item inside the shop, we instantiate a new row to store in the information of the items.
Now imagine that the shop has 10 or 15 items available at once. Each time you click the + button, all those 15 game objects will get destroyed all at once, then all those 15 items will have to be instantiated all at once again in order to “refresh” and display an updated state of the items.
This is a huge problem, because this will introduce small lag spikes (stutters) each time you click the + or - button, especially if you have a lot more items available in the shop.
Also each time you would click any filter button, depending on how many items were available for that category, the Redraw process is triggered. So, if the Armor category had 9 items, then all the previous objects are deleted, then 9 new objects are being created.
The image above shows just that. I was clicking the + on the potions in order to get to the number I wanted to buy. As you can see, the blue vertical lines at the top represent the lag spikes (stutter) that the Redraw method is producing. Below you can see that the Instantiate.Produce is actually creating those 9 new objects that hold the item information. This happens for each spike you see. And in total, there is a 30+ms spike. That’s pretty noticeable, and it can get really really bad, the more items you have available.
Now another strange and weird behavior I saw is what actually happens once I finish adding the potions to the transaction, and then click the Buy button. There’s a whole new surprise there.
OK so, how to explain this… I really don’t know. But as you can see, the game had a literal 3 second lag spike in total. That is really really bad and really game breaking from my perspective. If you look at the highlighted areas, especially the Calls row, you will see that the Instantiate process (alongside other processes) had a LOT of calls being done. I don’t know why this is happening. Also, as an added WTH moment, if you look at the panel on the right side, you can see that there are a LOT of shop row game objects being instantiated. This is the WTH moment. It seems that for each item that I had in my transaction ( I think I had added something like 45+ potions), the Redraw method somehow got triggered for EACH of the item in my current transaction? I don’t know if I read that correctly, but that could only explain the 3 seconds lag spike that I had once I clicked the Buy button.
As a solution, I tried switching from Destroy / Instantiate to an Object Pooling system… And to my dismay that didn’t really help at all… Maybe it helps a bit because you avoid the extra garbage that the whole destroy / instantiate process is creating.
Object Pooling for those of you who are not familiar with, is when you create a bunch of objects that you might need, when the game starts. Think of it like some sort of factory. If I know that my shop will have a max number of 20 items displayed at once, I can create those 20 row prefabs at the start of the game, and then just set the as disabled. They will be there, and when I talk to a shopkeeper NPC, the Redraw method will simple pool the number needed of row prefabs from my Object Pooler. The process simple takes the number of row prefabs from the factory, sets their correct position under the listRoot, set the listRoot as a parent and then enables the game objects.
This sounded like a much better plan. But when I got to test the exact scenario, to my surprise this did not really help at all.
Here is how Enabling and Disabling objects works in Unity:
If you will want to have some decent UI / UX for your game, you will want it to look good. Good looking UI means that you will have to use a lot of child objects within a main object and those will all have a bunch of components on them. Components are things like scripts, layout elements, layout groups, Images, Toggles, Buttons, etc. (Talking from an UI pov.) When you have 1 shop row game object that has at least 6+ child game objects and each will have at least 3+ components on them, some a bit more, this will make the Disabling and Enabling process harder. Why, might you ask? Well that’s because each time you do a gameObject.SetActive(false / true) this means that Unity has to go in, and it doesn’t just Tick the enable box for the main game object only, no no, it has to do so for EACH of the available component in it, and child objects. So yes, even object pooling does not help in this problem.
Here is a quick view of the part of the Redraw method that replaced the destroy / instantiate:
int rowCount = listRoot.childCount;
for (int i = rowCount - 1; i > -1; i--)
{
if (listRoot.GetChild(i).gameObject.activeSelf) listRoot.GetChild(i).gameObject.SetActive(false);
}
int itemIndex = currentShop.GetFilteredItems().ToList().Count;
for (int i = itemIndex - 1; i > -1; i--)
{
var rowGo = shopPooler.SpawnFromPool("Shop1Row", listRoot.position, Quaternion.identity, listRoot, i);
RowUI row = rowGo.GetComponent<RowUI>();
row.Setup(currentShop, currentShop.GetFilteredItems().ToList()[i]);
}
So this part here simply disables all the shop items under the shops listRoot, then it grabs as many row prefabs as it needs from the pool, sets their correct location and then enables them.
Like I said, this did not help at all. I had the same exact spikes as before.
I think that the main issue is how we are doing all of this Redrawing.
I think that we need a more performant method where we do not just go in and delete / disable every object, and then instantiate / enable all of the items again.
We need a way to somehow tell the shop UI which row object that holds the item is being operated one ( either Adding, Removing and then Buying).
That way, if we click the + on one item, then that object that holds that items info should be marked somehow and that object would then be destroyed / disabled and then only 1 object needs to be instantiated / grabbed and enabled instead of all the items. This would significantly improve performance. Also, the part where you Buy the items you have in your transaction needs to be investigate because of that immense lag spike it produces. (Investigate why it instantiate for example 9 objects x 45 times, the number of items in the current transaction)
I tried myself to make this happen, but to no avail, and it’s driving me crazy to the point that it drains all my motivation and makes me want to just stop trying to build anything at all.
I have no other ideas, nor do I have the experience need to actually pull this through. I only got some months of experience when it comes to programming in general. I had 0 knowledge of programming prior to taking the course. But working on it a lot and experimenting has enabled me to make small things happen. But things that require actual experience go way over my head.
Sorry for this long post, but I wanted to try to explain the problem to a certain degree, so that people that are not so experienced like me can understand what happens.
I hope someone is able to help, as this would benefit everyone that is taking the course and is serious about actually developing and releasing a game with decent user experience.
Thank you.