How to optimize the large environments in Unity ECS for mobile devices

I am currently using ECS for my multiplayer shooter game with NetCode for Entities, targeting iOS and Android mobile devices. I have a large environment running in the gameplay sub-scene.

In an attempt to optimize performance, I tried combining meshes to reduce the batch count. However, this didn’t significantly improve performance on iOS. In fact, in some areas of the environment, the FPS dropped.

What optimization techniques can I use to reduce the tris and verts count?

Did you profile the application?

You need to know whether you are CPU or GPU bound, and if so, what area specifically.

1 Like

If you are certain this is the issue, the answer is LODs.

@CodeSmile

I removed many assets from the environment but it is not helping much. Also added the LODs to many assets.

So I profiled the IOS build. I getting the spikes at a certain duration. Mainly for three different systems one after another.

  1. From Physics.System.BuildPhysicsWorld (I am not sure why I am getting a spike from this)
  2. WeaponFiringMechnisum (Used ISystem with BurstCompile but still getting spike)
  3. ThirdPersonPlayerAnimationSystem (Used ISystem with BurstCompile but still getting spike)
using Rukhanka;
using System.Runtime.CompilerServices;
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;

public struct InputStateData
{
    public float Speed;
    public bool Jump;
    public bool Grounded;
    public bool Crouch;
    public float InputX;
    public float InputY;
    public bool IsIdle;
    public bool FreeFall;
    public float MotionSpeed;
    public bool IsShooting;
    public bool Sprint;
    public bool IsDancing;
    public float DanceIndex;
    public bool IsAttecking;
    public bool IsVictim;
    public float FinisherIndex;
    public bool IsDead;
    public int CombinedStates;
}

#if RUKHANKA_WITH_NETCODE
[UpdateInGroup(typeof(RukhankaPredictedAnimationSystemGroup))]
#endif
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
[RequireMatchingQueriesForUpdate]
[BurstCompile]
public partial struct ThirdPersonPlayerAnimationSystem : ISystem
{
    private FastAnimatorParameter Speed;
    private FastAnimatorParameter Jump;
    private FastAnimatorParameter Grounded;
    private FastAnimatorParameter Crouch;
    private FastAnimatorParameter InputX;
    private FastAnimatorParameter InputY;
    private FastAnimatorParameter IsIdle;
    private FastAnimatorParameter IsAiming;
    private FastAnimatorParameter MotionSpeed;
    private FastAnimatorParameter IsShooting;
    private FastAnimatorParameter Sprint;
    private FastAnimatorParameter IsDancing;
    private FastAnimatorParameter DanceIndex;
    private FastAnimatorParameter IsAttecking;
    private FastAnimatorParameter IsVictim;
    private FastAnimatorParameter FinisherIndex;
    private FastAnimatorParameter IsDead;
    private FastAnimatorParameter CombinedStates;

    private float m_SprintSpeedMultiplier;
    private float m_SmoothTime;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        Speed = new FastAnimatorParameter("Speed");
        Jump = new FastAnimatorParameter("Jump");
        Grounded = new FastAnimatorParameter("Grounded");
        Crouch = new FastAnimatorParameter("Crouch");
        InputX = new FastAnimatorParameter("InputX");
        InputY = new FastAnimatorParameter("InputY");
        IsIdle = new FastAnimatorParameter("IsIdle");
        IsAiming = new FastAnimatorParameter("IsAiming");
        MotionSpeed = new FastAnimatorParameter("MotionSpeed");
        IsShooting = new FastAnimatorParameter("IsShooting");
        Sprint = new FastAnimatorParameter("Sprint");
        IsDancing = new FastAnimatorParameter("IsDancing");
        DanceIndex = new FastAnimatorParameter("DanceIndex");
        IsAttecking = new FastAnimatorParameter("IsAttecking");
        IsVictim = new FastAnimatorParameter("IsVictim");
        FinisherIndex = new FastAnimatorParameter("FinisherIndex");
        IsDead = new FastAnimatorParameter("IsDead");
        CombinedStates = new FastAnimatorParameter("CombinedStates");

        m_SprintSpeedMultiplier = 2f;
        m_SmoothTime = 0.2f;

        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<ThirdPersonCharacterComponent, GhostOwnerIsLocal>().Build());
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var deltaTime = SystemAPI.Time.DeltaTime;

        foreach (var (animatorParameterComponent, characterAspect, characterComponent, playerFinisherState, health, entity) in
            SystemAPI.Query<DynamicBuffer<AnimatorControllerParameterComponent>, ThirdPersonCharacterAspect, ThirdPersonCharacterComponent, RefRW<PlayerFinisherState>, Health>()
            .WithAll<ThirdPersonCharacterComponent, GhostOwnerIsLocal>()
            .WithEntityAccess())
        {
            /*if (playerFinisherState.ValueRO.IsPlayerVictim)
                PlayerInputHandler.Instance.crouch = false;*/

            if (characterAspect.CharacterControl.ValueRO.Sprint)
                m_SprintSpeedMultiplier = characterComponent.SprintSpeedMultiplier;
            else if (characterAspect.CharacterControl.ValueRO.Crouch)
                m_SprintSpeedMultiplier = characterComponent.CrouchSpeedMultiplier;
            else
                m_SprintSpeedMultiplier = characterComponent.SprintSpeedMultiplier;

            float speedValue;

            bool isIdle = characterAspect.CharacterControl.ValueRO.MoveVector.x == 0 && characterAspect.CharacterControl.ValueRO.MoveVector.y == 0;
            if (!characterAspect.CharacterControl.ValueRO.Sprint && !isIdle)
            {
                float rawSpeed = math.length(characterAspect.CharacterControl.ValueRO.MoveVector * m_SprintSpeedMultiplier);
                float maxSpeed = m_SprintSpeedMultiplier;
                speedValue = math.remap(0f, maxSpeed, 1.7f, 1f, rawSpeed);
            }
            else
            {
                speedValue = math.length(characterAspect.CharacterControl.ValueRO.MoveVector * m_SprintSpeedMultiplier);
            }

            UpdateAnimatorParameter(animatorParameterComponent, 0, speedValue);
            UpdateAnimatorParameter(animatorParameterComponent, 1, characterAspect.CharacterControl.ValueRO.Jump);
            UpdateAnimatorParameter(animatorParameterComponent, 2, characterAspect.CharacterControl.ValueRO.IsGrounded);
            UpdateAnimatorParameter(animatorParameterComponent, 3, characterAspect.CharacterControl.ValueRO.Crouch);
            UpdateAnimatorParameter(animatorParameterComponent, 4, math.lerp(characterAspect.CharacterControl.ValueRO.MoveAxisValue.x, characterAspect.CharacterControl.ValueRO.MoveAxisValue.x, m_SmoothTime));
            UpdateAnimatorParameter(animatorParameterComponent, 5, math.lerp(characterAspect.CharacterControl.ValueRO.MoveAxisValue.y, characterAspect.CharacterControl.ValueRO.MoveAxisValue.y, m_SmoothTime));
            UpdateAnimatorParameter(animatorParameterComponent, 6, speedValue == 0);
            UpdateAnimatorParameter(animatorParameterComponent, 7, characterAspect.CharacterControl.ValueRO.IsAiming);
            UpdateAnimatorParameter(animatorParameterComponent, 8, 1f);
            UpdateAnimatorParameter(animatorParameterComponent, 9, characterAspect.CharacterControl.ValueRO.IsShooting);
            UpdateAnimatorParameter(animatorParameterComponent, 10, characterAspect.CharacterControl.ValueRO.Sprint);
            UpdateAnimatorParameter(animatorParameterComponent, 11, characterAspect.CharacterControl.ValueRO.IsDancing);
            UpdateAnimatorParameter(animatorParameterComponent, 12, characterAspect.CharacterControl.ValueRO.DanceIndex);
            UpdateAnimatorParameter(animatorParameterComponent, 13, playerFinisherState.ValueRW.IsPlayerAttecker);
            UpdateAnimatorParameter(animatorParameterComponent, 14, playerFinisherState.ValueRW.IsPlayerVictim);
            UpdateAnimatorParameter(animatorParameterComponent, 15, playerFinisherState.ValueRW.AnimationIndex);

            if (!playerFinisherState.ValueRW.IsPlayerVictim)
            {
                UpdateAnimatorParameter(animatorParameterComponent, 16, health.IsDead());
            }

            UpdateAnimatorParameter(animatorParameterComponent, 17, characterAspect.CharacterControl.ValueRO.IsReloading ? 5 : 0);
        }
    }

    private void UpdateAnimatorParameter(DynamicBuffer<AnimatorControllerParameterComponent> buffer, int index, float value)
    {
        var param = buffer[index];
        param.FloatValue = value;
        buffer[index] = param;
    }

    private void UpdateAnimatorParameter(DynamicBuffer<AnimatorControllerParameterComponent> buffer, int index, bool value)
    {
        var param = buffer[index];
        param.BoolValue = value;
        buffer[index] = param;
    }

    private void UpdateAnimatorParameter(DynamicBuffer<AnimatorControllerParameterComponent> buffer, int index, int value)
    {
        var param = buffer[index];
        param.IntValue = value;
        buffer[index] = param;
    }
}
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation | WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(WeaponPredictionUpdateGroup), OrderFirst = true)]
[BurstCompile]
public partial struct WeaponFiringMecanismSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    { 
        state.RequireForUpdate(SystemAPI.QueryBuilder().WithAll<StandardWeaponFiringMecanism, WeaponControl>().Build());
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        StandardWeaponFiringMecanismJob standardMecanismJob = new StandardWeaponFiringMecanismJob
        {
            DeltaTime = SystemAPI.Time.DeltaTime,
        };
        state.Dependency = standardMecanismJob.Schedule(state.Dependency);
    }

    [BurstCompile]
    [WithAll(typeof(Simulate))]
    public partial struct StandardWeaponFiringMecanismJob : IJobEntity
    {
        public float DeltaTime;

        void Execute(Entity entity, ref StandardWeaponFiringMecanism mecanism, ref WeaponControl weaponControl, ref WeaponShotVisuals weaponShotVisuals, in GhostOwner ghostOwner)
        {
            mecanism.ShotsToFire = 0;
            mecanism.ShotTimer += DeltaTime;

            // Detect starting to fire
            if (weaponControl.ShootPressed)
            {
                mecanism.IsFiring = true;
            }

            // Handle firing
            if (mecanism.FiringRate > 0f)
            {
                float delayBetweenShots = 1f / mecanism.FiringRate;
                
                // Clamp shot timer in order to shoot at most the maximum amount of shots that can be shot in one frame based on the firing rate.
                // This also prevents needlessly dirtying the timer ghostfield (saves bandwidth).
                mecanism.ShotTimer = math.clamp(mecanism.ShotTimer, 0f, math.max(delayBetweenShots + 0.01f ,DeltaTime)); 
                
                // This loop is done to allow firing rates that would trigger more than one shot per tick
                while (mecanism.IsFiring && mecanism.ShotTimer > delayBetweenShots)
                {
                    mecanism.ShotsToFire++;
                    
                    // Consume shoot time
                    mecanism.ShotTimer -= delayBetweenShots;

                    // Stop firing after initial shot for non-auto fire
                    if (!mecanism.Automatic)
                    {
                        mecanism.IsFiring = false;
                    }
                }
            }

            // Detect stopping fire
            if (!mecanism.Automatic || weaponControl.ShootReleased)
            {
                mecanism.IsFiring = false;
            }
            
            weaponShotVisuals.TotalShotsCount += mecanism.ShotsToFire;
        }
    }
}

here are some images related to the profiling






In the timeline view, check some of the other thread groups, like the loading thread or the profiling thread. Sometimes jobs get stuck on those threads, and it looks like you have some long-running job getting stuck. If nothing shows up, you should probably report a bug, because it would appear the job system itself is getting stuck.

Here are the loading and profiling thread images. I am not able to figure out what am I doing wrong!





Report a bug. (Cleanup) jobs should not be taking that long. And they aren’t your code.