Diagnosing the issue
Vanilla code path (6 ms):
vanilla / deep profile, hierarchy view:
As you can see the biggest cpu cost in vanilla code path is zombie.transform.position += new Vector3();
line. But that’s beside the point.
Jobified code path (10 ms):
Question arises immediately here: *Why is jobified code path slower than the old-timey one?
Maybe it’s this job at fault? But no, not at all; it’s so fast it’s hardly even visible without a zoom (it takes less than 0.1 ms spread across multiple cores).
So, what is the issue here then? To find out we need to enable Deep Profile
and switch to Hierarchy
view in Profiler
window, then look for OurScriptName.Update
entry.
Be aware that Deep Profile
makes everything execute a lot slower, so absolute time values are no longer useful, but relative time values (%) still are.
And here they are - our offenders in plain sight:
As you can clearly see here, it’s caused by:
-
NativeArray
read & write operations, which are super slow outside Burst-compiled jobs
-
= transform.position
-
transform.position =
-
and few other minor operations, like casting, enumeration and List reading, all magnified by this huge 10k loop.
Which lines are causing the biggest problem? It’s
position _= zombieList*.transform.position;
and
zombieList_.transform.position = position;
Fixing the issue
TransformAccess
to the rescue. It fixes all the problems mentioned above and leaves you with the eternal one: “how to draw 10k moving meshes under 60fps”. Which is a better problem to have (as it’s arbitrary one).
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Jobs;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Jobs;
using Random = UnityEngine.Random;
public class TestSystem : MonoBehaviour
{
[SerializeField] GameObject _prefab = null;
TransformAccessArray _transformsAccess = default(TransformAccessArray);
NativeArray<float3> _velocities = default(NativeArray<float3>);
public JobHandle Dependency = default(JobHandle);
void Start ()
{
int numInstances = 10000;
var transforms = new Transform[ numInstances ];
_velocities = new NativeArray<float3>( numInstances , Allocator.Persistent );
for( int i=0 ; i<numInstances ; i++ )
{
GameObject instance = Instantiate(
_prefab ,
new Vector3( Random.Range(-8f,8f) , Random.Range(-5f,5f) ) ,
Quaternion.identity
);
transforms *= instance.transform;
_velocities *= new float3{ y=Random.Range(1f,2f) };
}
_transformsAccess = new TransformAccessArray( transforms );
}
void OnDestroy ()
{
Dependency.Complete();
if( _velocities.IsCreated ) _velocities.Dispose();
if( _transformsAccess.isCreated ) _transformsAccess.Dispose();
}
void Update ()
{
Dependency.Complete();
var job = new MyJob{
deltaTime = Time.deltaTime ,
velocity = _velocities
};
Dependency = job.Schedule( _transformsAccess );
}
[Unity.Burst.BurstCompile]
public struct MyJob : IJobParallelForTransform
{
public NativeArray<float3> velocity;
[ReadOnly] public float deltaTime;
void IJobParallelForTransform.Execute ( int index , TransformAccess transform )
{
float3 vel = velocity[index];
float3 pos = transform.position;
{
pos += vel * deltaTime;
if( pos.y>5f ) vel = -math.abs(vel);
if( pos.y<-5f ) vel = math.abs(vel);
}
transform.position = pos;
velocity[index] = vel;
}
}
}
But wait, there’s more!
The same behaviour, but converted to DOTS. 10k entities with Hybrid Renderer V2 enabled gave me 100-200 FPS :T
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Random = UnityEngine.Random;
public class SpawnZombiesFixedV2 : MonoBehaviour
{
[SerializeField] GameObject _prefab = null;
void Start ()
{
var world = World.DefaultGameObjectInjectionWorld;
var command = world.EntityManager;
Entity entityPrefab;
using( var blobAssetStore = new BlobAssetStore() )
{
var conversionSettings = GameObjectConversionSettings.FromWorld( world , blobAssetStore );
entityPrefab = GameObjectConversionUtility.ConvertGameObjectHierarchy( _prefab , conversionSettings );
}
command.AddComponent<Velocity>( entityPrefab );
for( int i=0 ; i<10000 ; i++ )
{
Entity instance = command.Instantiate( entityPrefab );
command.SetComponentData( instance , new Translation{
Value = new float3{ x=Random.Range(-8f,8f) , y=Random.Range(-5f,5f) }
} );
command.SetComponentData( instance , new Velocity{
Value = new float3{ y=Random.Range(1f,2f) }
} );
}
}
}
public class SpawnZombiesFixedV2System : SystemBase
{
protected override void OnUpdate ()
{
float deltaTime = Time.DeltaTime;
Entities
.WithName("update_positions_job")
.ForEach( ( ref Translation position , in Velocity vel ) =>
{
position.Value += vel.Value * deltaTime;
} )
.Schedule();
Entities
.WithName("update_velocity_job")
.ForEach( ( ref Velocity vel , in Translation position ) =>
{
if( position.Value.y>5f ) vel.Value = -math.abs(vel.Value);
if( position.Value.y<-5f ) vel.Value = math.abs(vel.Value);
} )
.Schedule();
}
}
public struct Velocity : IComponentData
{
public float3 Value;
}