Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Super basic crowd causing significant frame rate hit - am I missing something?

Discussion in 'General Graphics' started by AussieSwissDave, Aug 27, 2020.

  1. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    I'm making a little sports game and I have a very basic crowd simulation. I'm going for a stylized look where you just have some very basic block figures with random colours roughly matching the competing teams. I have code that knows where each row of the simplified grandstand is and randomly populates them according to how full the attendance is.

    The code to get all of that working is very easy.

    upload_2020-8-27_16-21-46.png

    The problem is that the frame rate is taking a significant hit. There can be up to ~ 3000 of these little guys, and they aren't even moving and the frame rate is down to about 30 fps. That's before any of the actual code to simulate the sport is added! As you can see, they are simple cylinders with a cube head, all made in Pro Builder within Unity. Super basic. They don't even cast any shadows.

    I wonder, am I doing something basic wrong? For every fan I instantiate a prefab and so you end up with obviously a huge list like this...

    How do people deal with large numbers of things in their game (even static objects) without it lagging the hell out of it?

    upload_2020-8-27_16-27-5.png
     
  2. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    122
    Without looking at the Frame Debugger or Stats Window I suspect that each spectator generates a separate draw call. Are you by any chance modifying the material on each spectator instance something like this?:
    Code (CSharp):
    1. var spectator = Instantiate(spectatorPrefab);
    2. var material = spectator.GetComponent<MeshRenderer>().material;
    3. material.SetColor(/* ... */);
    4. spectator.GetComponent<MeshRenderer>().material = material;
    That would create a separate material copy for each spectator and raise the number of draw calls dramatically. You can check the number of draw calls by turning on "Stats" in the Game View (look for "SetPass calls: XX") or using the Frame Debugger (Window > Analysis > Frame Debugger), which also helps tracking down problems that cause draw calls beinf batched together to break.
    You want them to only generate a single draw call by either being batched with static batching, which requires them to share one single material and makes the individual colors a bit more difficult, or use instancing and vary the colors via Material Property Blocks.
     
  3. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    Hello,

    Thank you very much for your reply. Here is how I create the spectators. For each team there are 3 sets of 3 colours, so 9 total. So I have created 18 materials

    Code (CSharp):
    1.         Material body_home_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    2.         Material top_home_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    3.         Material bottom_home_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    4.         Material body_home_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    5.         Material top_home_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    6.         Material bottom_home_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    7.         Material body_home_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    8.         Material top_home_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    9.         Material bottom_home_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    10.         Material body_away_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    11.         Material top_away_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    12.         Material bottom_away_guernsey1 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    13.         Material body_away_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    14.         Material top_away_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    15.         Material bottom_away_guernsey2 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    16.         Material body_away_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorBody") as Material);
    17.         Material top_away_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorTop") as Material);
    18.         Material bottom_away_guernsey3 = Instantiate(Resources.Load("StadiumCreator/SpectatorBottom") as Material);
    19.         body_home_guernsey1.color = home_guernsey1_colour1;
    20.         top_home_guernsey1.color = home_guernsey1_colour3;
    21.         bottom_home_guernsey1.color = home_guernsey1_colour2;
    22.         body_home_guernsey2.color = home_guernsey2_colour1;
    23.         top_home_guernsey2.color = home_guernsey2_colour3;
    24.         bottom_home_guernsey2.color = home_guernsey2_colour2;
    25.         body_home_guernsey3.color = home_guernsey3_colour1;
    26.         top_home_guernsey3.color = home_guernsey3_colour3;
    27.         bottom_home_guernsey3.color = home_guernsey3_colour2;
    28.         body_away_guernsey1.color = away_guernsey1_colour1;
    29.         top_away_guernsey1.color = away_guernsey1_colour3;
    30.         bottom_away_guernsey1.color = away_guernsey1_colour2;
    31.         body_away_guernsey2.color = away_guernsey2_colour1;
    32.         top_away_guernsey2.color = away_guernsey2_colour3;
    33.         bottom_away_guernsey2.color = away_guernsey2_colour2;
    34.         body_away_guernsey3.color = away_guernsey3_colour1;
    35.         top_away_guernsey3.color = away_guernsey3_colour3;
    36.         bottom_away_guernsey3.color = away_guernsey3_colour2;
    Now I create the spectators (this is inside a larger loop but just showing the bit where the spectator is created)

    Code (CSharp):
    1.                                 Vector3 spectatorPos = new Vector3(start_x + 4f * kk - 0.3f + 0.6f*UnityEngine.Random.value,offset_y + step_y * jj, offset_z + 3f * jj - 0.2f + 0.4f*UnityEngine.Random.value);
    2.                                 GameObject spectatorPrefab = Resources.Load("StadiumCreator/SpectatorPrefabRounded") as GameObject;
    3.                                 //spectatorPrefab = (GameObject)Instantiate(spectatorPrefab,spectatorPos,Quaternion.EulerAngles(0f,0f,0f));
    4.                                 spectatorPrefab = (GameObject)Instantiate(spectatorPrefab,stadium[ii].model.transform);
    5.                                 spectatorPrefab.transform.localPosition = spectatorPos;
    6.                                 GameObject body =  spectatorPrefab.transform.Find("Body").gameObject;
    7.                                 GameObject top =  spectatorPrefab.transform.Find("Top").gameObject;
    8.                                 GameObject bottom =  spectatorPrefab.transform.Find("Bottom").gameObject;
    9.                                 GameObject head =  spectatorPrefab.transform.Find("Head").gameObject;
    And now I assign home or away randomly

    Code (CSharp):
    1.                                // is it a home or away fan?
    2.  
    3.                                 string fanTeam = "-27";
    4.                                 float fanPicker = UnityEngine.Random.value;
    5.                                 Color32 topColour, bottomColour, bodyColour;
    6.                                 if (fanPicker < homeFanFraction)
    7.                                 {
    8.                                     fanTeam = "Home";
    9.                                     // guernsey picker
    10.                                     float guernseyPicker = UnityEngine.Random.value;
    11.                                     string guernsey = "-27";
    12.                                     if (guernseyPicker < 0.6)
    13.                                     {
    14.                                         guernsey = "Home";
    15.                                         body.GetComponent<MeshRenderer>().material = body_home_guernsey1;
    16.                                         top.GetComponent<MeshRenderer>().material = body_home_guernsey1;
    17.                                         head.GetComponent<MeshRenderer>().material = top_home_guernsey1;
    18.                                         bottom.GetComponent<MeshRenderer>().material = bottom_home_guernsey1;
    19.                                     } else if (guernseyPicker > 0.6 && guernseyPicker < 0.8)
    20.                                     {
    21.                                         guernsey = "Away";
    22.                                         body.GetComponent<MeshRenderer>().material = body_home_guernsey2;
    23.                                         top.GetComponent<MeshRenderer>().material = body_home_guernsey2;
    24.                                         head.GetComponent<MeshRenderer>().material = top_home_guernsey2;
    25.                                         bottom.GetComponent<MeshRenderer>().material = bottom_home_guernsey2;
    26.                                     } else {
    27.                                         body.GetComponent<MeshRenderer>().material = body_home_guernsey3;
    28.                                         top.GetComponent<MeshRenderer>().material = body_home_guernsey3;
    29.                                         head.GetComponent<MeshRenderer>().material = top_home_guernsey3;
    30.                                         bottom.GetComponent<MeshRenderer>().material = bottom_home_guernsey3;
    31.                                     }
    32.                                  
    33.                                 } else if (fanPicker > homeFanFraction && fanPicker < homeFanFraction + awayFanFraction)
    34.                                 {
    35.                                     fanTeam = "Away";
    36.                                                                         // guernsey picker
    37.                                     float guernseyPicker = UnityEngine.Random.value;
    38.                                     string guernsey = "-27";
    39.                                     if (guernseyPicker < 0.6)
    40.                                     {
    41.                                         guernsey = "Home";
    42.                                         body.GetComponent<MeshRenderer>().material = body_away_guernsey1;
    43.                                         top.GetComponent<MeshRenderer>().material = body_away_guernsey1;
    44.                                         head.GetComponent<MeshRenderer>().material = top_away_guernsey1;
    45.                                         bottom.GetComponent<MeshRenderer>().material = bottom_away_guernsey1;
    46.                                     } else if (guernseyPicker > 0.6 && guernseyPicker < 0.8)
    47.                                     {
    48.                                         guernsey = "Away";
    49.                                         body.GetComponent<MeshRenderer>().material = body_away_guernsey2;
    50.                                         top.GetComponent<MeshRenderer>().material = body_away_guernsey2;
    51.                                         head.GetComponent<MeshRenderer>().material = top_away_guernsey2;
    52.                                         bottom.GetComponent<MeshRenderer>().material = bottom_away_guernsey2;
    53.                                     } else {
    54.                                         guernsey = "Alternate";
    55.                                         body.GetComponent<MeshRenderer>().material = body_away_guernsey3;
    56.                                         top.GetComponent<MeshRenderer>().material = body_away_guernsey3;
    57.                                         head.GetComponent<MeshRenderer>().material = top_away_guernsey3;
    58.                                         bottom.GetComponent<MeshRenderer>().material = bottom_away_guernsey3;
    59.                                     }
    60.                                 }
    So I don't *think* I am doing exactly what you say, and indeed I was trying to avoid that. I think that I only create 18 materials and then assign them randomly, rather than creating a new one each time. But maybe I am doing that inherently....
     
  4. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    Also just looking more into your profiling advice:

    upload_2020-8-27_20-10-6.png

    How does that look to you?
     
  5. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    So definitely some of the fans are being batched and drawn together, shown here in consecutive screenshots. The number of vertices and indices though... seems high?

    The blue and white parts are a cylinder, whereas the red part is a square, which explains why its vertices are much less.

    upload_2020-8-27_20-21-19.png
    upload_2020-8-27_20-21-58.png
    upload_2020-8-27_20-22-8.png
     
  6. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    And so I see that even with the same material, batching has to get split up, as you can see here with the dark blue stuff coming in multiple batches, due to there being too many indices. I guess I need to reduce that somehow...

    upload_2020-8-27_20-27-23.png
    upload_2020-8-27_20-27-32.png
    upload_2020-8-27_20-27-42.png
     
  7. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    So looking at each little spectator and doing a merge on the faces in Probuilder:

    upload_2020-8-27_20-34-54.png

    This has gotten the batches down from 627 -> 617... so not much

    upload_2020-8-27_20-38-52.png
     
  8. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    Also in case I was thinking it was the stadium, no that seems fine with no fans:

    upload_2020-8-27_20-42-18.png
     
  9. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    122
    EDIT: Sorry, just noticed that the spectators are four separate parts, while below I was going of the assumption that it was a single mesh and the colors were separated based on mesh edges / texture coordinates. I would definitely recommend merging into a single mesh per spectator and getting the colors in using one of the ways below. I think having as few materials as possible is always a good idea, no matter the target platform.

    Okay, a couple of things:
    The stadium alone producing 439 batches is still a whole lot, especially if you're targeting platforms with lower specs like mobiles. Static batching should be able to reduce that number dramatically given the stadium doesn't move and uses the same material all over (which it should; also make sure it's marked static as described below). You could also just merge the meshes or at least parts of them by hand.

    Are the spectators moving or not? If not, they should not be batched dynamically. All that "Draw Dynamic" might also explain why the CPU time is pretty high for a scene where nothing much is happening at the moment (I assume), the documentation says "Dynamic batching works by transforming all GameObject vertices into world space on the CPU, so it is only an advantage if that work is smaller than doing a draw call" (emphasis mine, source).

    I can think of three ways to remedy that:
    The first two rely on the spectators being batched statically by marking them as "Batching Static" (checkbox next to the GameObject name) and assigning the same material to all of them. Note that they cannot move then or the batching breaks. You could try with the three materials instead of one as you're using now, but in my experience that often breaks up batches.
    To get multiple different colors into the same material, you could either
    • use multiple meshes with different UV coordinates and use a texture atlas (really just a small 3x3 texture you can generate at runtime where each pixel is a different color - assuming the parts are uniformly colored)
    • or use vertex colors. This of course requires a shader which uses vertex colors and bumps up the number of indices, thus the batches reach their maximum size faster ergo more batches (documentation: "Batch limits are 64k vertices and 64k indices on most platforms (48k indices on OpenGLES, 32k indices on macOS)")
    The third way is to not rely on batching but instancing. That would require modifying the shader to support instancing and adding three instanced Color properties for the parts with varying colors. You would then use the same material on each spectator instance and adjust the colors when instantiating the spectators using:
    Code (CSharp):
    1. // do this once outside the loop, MaterialPropertyBlocks can be reused as they are copied when passed to a Renderer
    2. var propertyBlock = new MaterialPropertyBlock();
    3.  
    4. // ...inside the loop:
    5. propertyBlock.SetColor("_NameOfTheColorInShader", someColor);
    6. spectator.GetComponent<MeshRenderer>.SetPropertyBlock(propertyBlock);
    Personally, I would go with the third way.
    The documentation on Draw Call Batching and this introduction to Material Property Blocks and Instancing should provide some good info.
     
    Last edited: Aug 28, 2020
  10. AussieSwissDave

    AussieSwissDave

    Joined:
    Apr 20, 2013
    Posts:
    97
    Thank you very much @uwdlg for your reply. I'll look into this when I get off work :)

    I'll answer for now that the grandstands definitely don't move but the spectators are ultimately planned to move, just in a very simple way like bouncing up and down when their team scores. But I can try for now static batching assuming they don't move.
     
  11. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    122
    I would suggest looking into instancing and going with that right from the start, then movement is no problem and it's easier to get all of them into one draw call than with Static Batching (where things like opaque sort order can break up batches). A bit of shader fiddling but ultimately the best fit I would say.