- Home /
How to improve performance with 10,000s of GameObjects?
I'm in the planning stages of a (2D) bullet hell engine and looking at both Game Maker and Unity. In GM, I can process ~50,000 bullets at 60 FPS—by process, I mean change coordinates via trig, change 1-2 other parameters, run collision detection with the player, and draw to the screen. I accomplish this by keeping all bullets within a single object (therefore running all draw calls within that one object).
In Unity, I can only manage ~25,000 bullets at 60 FPS, and they're just sitting there, not being processed. ~25,000 GameObjects with only a transform component and a sprite renderer component, no physics, doing nothing but being drawn every frame. (Profiler says 64.2% Render.TransparentGeometry, 23.0% Render.Prepare, and 8.9% Culling—that's almost all of it.)
It's kinda a surprise that Game Maker blows Unity out of the water, so I'm sure I'm missing something. I dislike using Game Maker and like C# so I wanted to learn Unity, but these GameObjects are killing me. What can I do to draw 50,000+ bullets at 60 FPS? I'll look at batch call drawing next, but why should I need to?
EDIT: I see now that Unity is grouping the 25,000 bullets into only 9 batches, but Statistics says "Saved by batching: 0"? What does that mean?
Answer by lodendsg · Mar 13, 2018 at 12:31 AM
I know this is an old question but its a problem that is ran into offten.
The specific case we have is several hundred warships in a space game each with multiple turrets, each turret with multiple barrels and a resonably fast fire rate and projectile lifetime.
We implamented a GPU instancing solution and found it to massivly reduce performance impact. The biggest part of the gain is due to there being only 1 gameObject in the scene representing all our projectiles. The following C# code is from our test rig, it has been used in 2017.3.1 succesfuly but isn't optimized so could be further improved Im sure ... never the less its better than pooling assuming your projectiles are simple meshes with an instance enabled material.
PS: ConquestBattleConfiguration is just a ScriptableObject we use to store some common settings in this case it stores the layers we test for collision on.
public class ProjectileRenderer : MonoBehaviour
{
[Header("Data")]
public ConquestBattleConfiguration Config;
public Mesh mesh;
public Material material;
public float life;
public float speed;
public float damage;
[Header("Instances")]
public List<ProjectileData> projectiles = new List<ProjectileData>();
public List<GameObject> splashPool = new List<GameObject>();
//Working values
private RaycastHit[] rayHitBuffer = new RaycastHit[1];
private Vector3 worldPoint;
private Vector3 transPoint;
private List<Matrix4x4[]> bufferedData = new List<Matrix4x4[]>();
public void SpawnProjectile(Vector3 position, Quaternion rotation, int team, float damageScale)
{
ProjectileData n = new ProjectileData();
n.pos = position;
n.rot = rotation;
n.scale = Vector3.one;
n.experation = life;
n.team = team;
n.damage = damage;
n.damageScale = damageScale;
projectiles.Add(n);
}
private void Update()
{
UpdateProjectiles(Time.deltaTime);
BatchAndRender();
}
private void BatchAndRender()
{
//If we dont have projectiles to render then just get out
if (projectiles.Count <= 0)
return;
//Clear the batch buffer
bufferedData.Clear();
//If we can fit all in 1 batch then do so
if (projectiles.Count < 1023)
bufferedData.Add(projectiles.Select(p => p.renderData).ToArray());
else
{
//We need multiple batches
int count = projectiles.Count;
for (int i = 0; i < count; i += 1023)
{
if (i + 1023 < count)
{
Matrix4x4[] tBuffer = new Matrix4x4[1023];
for(int ii = 0; ii < 1023; ii++)
{
tBuffer[ii] = projectiles[i + ii].renderData;
}
bufferedData.Add(tBuffer);
}
else
{
//last batch
Matrix4x4[] tBuffer = new Matrix4x4[count - i];
for (int ii = 0; ii < count - i; ii++)
{
tBuffer[ii] = projectiles[i + ii].renderData;
}
bufferedData.Add(tBuffer);
}
}
}
//Draw each batch
foreach (var batch in bufferedData)
Graphics.DrawMeshInstanced(mesh, 0, material, batch, batch.Length);
}
private void UpdateProjectiles(float tick)
{
foreach(var projectile in projectiles)
{
projectile.experation -= tick;
if (projectile.experation > 0)
{
//Sort out the projectiles 'forward' direction
transPoint = projectile.rot * Vector3.forward;
//See if its going to hit something and if so handle that
if (Physics.RaycastNonAlloc(projectile.pos, transPoint, rayHitBuffer, speed * tick, Config.ColliderLayers) > 0)
{
projectile.experation = -1;
worldPoint = rayHitBuffer[0].point;
SpawnSplash(worldPoint);
ConquestShipCombatController target = rayHitBuffer[0].rigidbody.GetComponent<ConquestShipCombatController>();
if (target.teamId != projectile.team)
{
target.ApplyDamage(projectile.damage * projectile.damageScale, worldPoint);
}
}
else
{
//This project wont be hitting anything this tick so just move it forward
projectile.pos += transPoint * (speed * tick);
}
}
}
//Remove all the projectiles that have hit there experation, can happen due to time or impact
projectiles.RemoveAll(p => p.experation <= 0);
}
private void SpawnSplash(Vector3 worlPoint)
{
//TODO: implament spawning of your splash effect e.g. the visual effect of a projectile hitting something
}
}
public class ProjectileData
{
public Vector3 pos;
public Quaternion rot;
public Vector3 scale;
public float experation;
public int team;
public float damage;
public float damageScale;
public Matrix4x4 renderData
{
get
{
return Matrix4x4.TRS(pos, rot, scale);
}
}
}
Hi, I would like to test your script. However, I have some errors when trying to run it. For example, ConquestShipCombatController and ConquestBattleConfiguration could not be found in the script. And for " bufferedData.Add(projectiles.Select " i get this error. List does not contain a definition for select
Could you please help me to run it ? Thank you very much :)
Answer by phil_me_up · Feb 23, 2016 at 01:32 PM
When you say that your 25,000 bullets are sitting there, are they actually just sitting there or is there any script attached to them? Is there anything that is iterating through the list of objects (even if they are not doing anything)? These things will have an effect so your problem isn't necessarily a GPU issue (I would actually expect it to be CPU).
When dealing with such a large number of objects, small optimisations in key areas can have a dramatic effect. For example, how are you accessing your object transforms. You might find getting and caching references to those transforms is far more efficient than accessing through .transform.
Are you using GetComponent() during updates? If so then don't, it's slow and there are better ways. Same goes for anything like FindObjectInScene
Of course, the first question you should ask is if you ever actually need 25,000 bullets on screen at any one time? This seems like a huge number. If you can only ever see yourself needing say 1000 (which is still quite a few even for a bullet hell) then just have your pool at 1000 objects and properly re-use. No matter what your number though, make sure you are pooling properly, that you are only activating / deactiving as needed, that you are only performing operations on active bullets etc.
25,000 bullets is only Bullet Heck. OP is going for Bullet Hell. :p
Seriously though, all valid points. In addition, often many of those bullets are probably going in the same direction. You can combine those into single objects so you're not dealing with as many individual game objects.
Thanks for the responses!
When you say that your 25,000 bullets are sitting there, are they actually just sitting there or is there any script attached to them?
Literally sitting there being drawn each frame. $$anonymous$$y profiler results:
64.2% Render.TransparentGeometry
23.0% Render.Prepare
8.9% Culling
2.4% other drawing operations
1.5% other operations
Is there anything that is iterating through the list of objects (even if they are not doing anything)?
Nope, the only other object is a master that runs "if (Input.Get$$anonymous$$eyDown($$anonymous$$eyCode.Space))" each update to instantiate 1,000 bullets. If space isn't pressed nothing happens.
Of course, the first question you should ask is if you ever actually need 25,000 bullets on screen at any one time? This seems like a huge number.
That's an excellent question. I'm not too sure myself, haha. $$anonymous$$y goal is to be able to process 100k simple bullets at 60 FPS, to allow enough overhead for a) very complex bullet processing, b) bad hardware, c) higher frame rates (for 120/144 Hz monitors and/or no VSync for less input lag), and d) just having more overhead (like most shmups, I'm using a fixed timestep and therefore can't afford to drop below 60 FPS).
I can't think of any bullet hell/danmaku that uses pool larger than 10k bullets at any one time (and even that's rare), but basically I want as much overhead as possible so I never need to limit my design.
25,000 bullets is only Bullet Heck. OP is going for Bullet Hell. :p
Yep, haha.
You can combine those into single objects so you're not dealing with as many individual game objects.
How do I render that though? I have a nested hierarchy structure that can process bullets within a single object, but how do I actually render multiple sprites within that one object?—I thought Unity was strict about one sprite renderer per GameObject?
In Game $$anonymous$$aker I put the sprite draw calls in the same nested loop structure that processes the bullets. A simplified example In pseudo-code:
foreach (wave in pattern)
{
change_color();
foreach (arc in wave)
{
change_angle();
foreach (bullet in arc)
{
change_position();
draw_sprite_ext(); // a Game $$anonymous$$aker function that draws to an "application surface" which then renders to the buffer once all drawing is done
}
}
}
Game $$anonymous$$aker can process and draw ~50k bullets at 60 FPS using draw_sprite_ext within a single master object—so how do I accomplish something similar in Unity to unburden the CPU?
Answer by Riderfan · Feb 23, 2016 at 05:01 PM
Sometime in March Unity is scheduled to release version 5.4 and this is supposed to have GPU Mesh Instancing included as a feature. While I don't have any of the specifics of how Unity will implement it, it's going to be critical for my probject (I need to have about 15,000 3D spectators in my stadiums). GPU Mesh Intancing is how pretty much any modern game now handles very large numbers of objects that are all (more or less) the same.
I'm not sure if GPU Mesh instancing works with moving objects (like bullets) but maybe there's a solution on the way.
Answer by HonoraryBob · Jan 22, 2017 at 10:15 PM
@TheMostCuriousThing - I've repeatedly run into big problems if there are even a moderate number of GameObjects at the same time, which I've confirmed slows it down more than having a larger number of polygons in only a few GameObjects. Batching helps, but often cannot be done enough to get decent frame rates. I'm not sure why this is such a huge issue. Let me know if you've found any solution.
Your answer
Follow this Question
Related Questions
4.6 -> 5.0 Dramatic Performance Loss - No batching? 1 Answer
CubemapConvolution.Specular 0 Answers
Model not rendered on Android Lollipop 5.0.1 0 Answers
Client's list of game object prefabs not shown in the Server. How do I solve this? 0 Answers
What does "RenderTexture: Resolving NULL surfaces." means? How to solve it. 0 Answers