Memory optimization help needed.
Hello there !
I am running into some problems with unity's garbage collector and I am hoping someone can help.
I have a simple trailrenderer script based on this (http://wiki.unity3d.com/index.php?title=OptimizedTrailRenderer) that creates a mesh based on the points in a list. But the way it works is causing lots of data to be created and destroyed constantly. The problem is that when you want to create a new mesh you have to specify the vertices in an array and since I am adding vertices frequently I have to create an array each time. This causes the garbage collector to slow the game down quite significantly.
I tried writing a version where I used lists but even if you use a list the mesh still requires you to parse it an array which is created and destroyed, again causing lots of garbage to be collected.
So is this just a limitation of the unity Mesh implementation or can I do something about this ?
Here is my script :
I am running into some problems with unity's garbage collector and I am hoping someone can help.
I have a simple trailrenderer script based on this (http://wiki.unity3d.com/index.php?title=OptimizedTrailRenderer) that creates a mesh based on the points in a list. But the way it works is causing lots of data to be created and destroyed constantly. The problem is that when you want to create a new mesh you have to specify the vertices in an array and since I am adding vertices frequently I have to create an array each time. This causes the garbage collector to slow the game down quite significantly.
I tried writing a version where I used lists but even if you use a list the mesh still requires you to parse it an array which is created and destroyed, again causing lots of garbage to be collected.
So is this just a limitation of the unity Mesh implementation or can I do something about this ?
Here is my script :
using UnityEngine; using System.Collections; using System.Collections.Generic; public class Trail_scr : MonoBehaviour { public Material material; public float segmentDistance; public float lineWidth; bool active = false; Material instanceMaterial; GameObject trailObj; Mesh mesh; int trailObjCount; List<Vector3> points; Vector3 previousPoint; void Start () { previousPoint = transform.position; trailObjCount = 0; SetActive(true); } // Update is called once per frame void Update () { if (Input.GetKeyDown(KeyCode.Space)) { SetActive(!active); } if (active == false) { return; } if (Vector3.Distance(previousPoint, transform.position) > segmentDistance) { NewPoint(); previousPoint = transform.position; } } void NewPoint () { points.Add(transform.position); // Rebuild mesh Vector3[] vertices = new Vector3[points.Count * 2]; Vector2[] uvs = new Vector2[points.Count * 2]; int[] tris = new int[(points.Count-1) * 6]; for (int i=0; i < points.Count; i ++) { Vector2 delta = new Vector2(1, 0); if (i > 1) { delta.x = points[i].x - points[i-1].x; delta.y = points[i].y - points[i-1].y; delta.Normalize(); delta = new Vector2(delta.y, -delta.x); // Get perpindicular vector } vertices[i * 2] = new Vector3(points[i].x - delta.x * lineWidth, points[i].y - delta.y * lineWidth, points[i].z); vertices[i * 2 + 1] = new Vector3(points[i].x + delta.x * lineWidth, points[i].y + delta.y * lineWidth, points[i].z); // UVs uvs[i * 2] = new Vector2(0 , 0); uvs[(i * 2) + 1] = new Vector2(0, 1); // tris if(i > 0) { // Triangles int triIndex = (i - 1) * 6; int vertIndex = i * 2; tris[triIndex+0] = vertIndex - 2; tris[triIndex+1] = vertIndex - 1; tris[triIndex+2] = vertIndex - 0; tris[triIndex+3] = vertIndex + 1; tris[triIndex+4] = vertIndex + 0; tris[triIndex+5] = vertIndex - 1; } } mesh.Clear(); mesh.vertices = vertices; mesh.uv = uvs; mesh.triangles = tris; } public void SetActive (bool state) { if (state == true && active == false) { trailObjCount ++; active = true; points = new List<Vector3>(0); trailObj = new GameObject(gameObject.name+"_trail_"+trailObjCount.ToString()); MeshFilter meshFilter = (MeshFilter) trailObj.AddComponent<MeshFilter>(); mesh = meshFilter.mesh; trailObj.AddComponent<MeshRenderer>(); instanceMaterial = new Material(material); trailObj.GetComponent<MeshRenderer>().material = instanceMaterial; } if (state == false) { active = false; } } }
Comments
But, if you are ok with the current performance of newpoint, then simply moving your variables out of the method into private class scope will already reduce gc at the small cost of keeping the used memory alive for longer.
Sorry for the multiple posts.
So firstly : I need to rewrite the trailrenderer cause I want it to be distance based instead of time based. In the unity one you can only specify the lifetime of the segments.
While researching I discovered that string concatenation is not kosher but this is not my fundamental problem so I am ignoring it for now. The main problem is that I need to recreate the vertices, uvs and tris arrays everytime I want to rebuild the mesh. Even if I were to move the array variables into the private class scope I am still recreating the array each time, which means the old array needs to be collected by the GC.
I am aware that there is lots of room for improving the CPU performance of the script but it is kind of irrelevant until I find a solution to the garbage collector problem.
So, your GC problem is stemming from allocating new arrays every frame. Namely these three lines:
Vector3[] vertices = new Vector3[points.Count * 2];
Vector2[] uvs = new Vector2[points.Count * 2];
int[] tris = new int[(points.Count-1) * 6];
The "new" keyword is always a red flag when it comes to garbage generation. What you need to do is pool those vertices, uvs and triangles. In other words, just maintain one array for each that you keep around and re-use between frames.
Updating the mesh (ie. mesh.vertices = blah) can still be costly, but can't really be avoided because the new mesh needs to be pushed to the GPU.
@Squidcor How would I go about reusing the array ? Are you talking of the same method as @farsicon ?
Thanks for the replies so far.
However, now you have a different problem. Arrays are allocated with a fixed length up front, and you need the number of vertices in your mesh to change on a per frame basis.
There are two ways around this (that I can see). Firstly, you can do what Farsicon is suggesting and just allocate some maximum number of vertices, and then just let all the extra ones sit on top of each other.
A better method however would be to use Mesh.SetVertices(), and use lists instead of arrays. Note that lists can still cause allocations when you add to them (if the underlying array needs to "grow" to accommodate the new element). If this is a problem, you can set an appropriate capacity when you first create the lists. In terms of initial allocation, using a list with some large-enough capacity is the same as using an array (Farsicon's method), but only a slice of the underlying array is getting passed through to SetVertices, so you haven't got all the extra vertices in your mesh.
I hope I explained this clearly enough?
I don't think having extra vertices would be a trainsmash, I'd just put them at the end of the trail instead (assuming the trail fades out or scales to 0 width) because then you're not going to have a weird start to the trail. Might also be an idea to flip those extra verts to that clipping nukes them early.
If the worry is about having loads of extra vertices if only a fraction of the total number of verts in the re-used array are being used, then it's a trivial matter to allocate two or three arrays of different sizes: few points, more points, ALL the points. Then you can switch between them and only use the smallest one that will hold all your current vertices.
@Squidcor So the SetVertices way of doing things creates much less garbage. There is just a couple of infrequent garbage collections which I assume is due to the lists that needs to expand their underlying arrays. Many thanks for your help !!
Here is the modified code :
As far as I know, when you create a new List, it's good to give it a number for a reasonable maximum that the list is likely to be (if your list is likely to change size regularly). I believe it reserves that space in memory, and internally has to create a new array if the list has to grow larger than that number. If you start with a zero-sized list, then I believe you're creating new arrays every time you add to the list. It could help to log how large your list typically ends up being, and just set it to be that large from the start. (Not a degreed programmer, but it's an speed/optimization problem I've had to deal with before on my own tools.)
Some Performance Considerations:
-- Lists are slow (slower than arrays, at least), but if your need them, use SetVertices on the mesh renderer. I use only arrays whenever I deal with procedural or dynamic meshes.
-- Instead of using a MeshRenderer, you "could" just use Graphics.DrawMesh instead. This way you can render your trail with fewer overall components (you eliminate the need for both [i]MeshFilter and MeshRenderer)[/i]. You can put Graphics.DrawMesh() within an Update or LateUpdate() method. Also besure to use RecalculateBounds() on the mesh at least once before using Graphics.DrawMesh().
Graphics.DrawMesh() also used to work when called within Coroutines as well, but since unity 5.3 this does not seem to function the same way anymore (things were simply not being rendered) - this is probably a bug.