Spawning too many objects when settings [GhostField] on NetworkTick

I’m trying to spawn a lot of bullets in a circle around the player. However, when using Play Mode Tools and setting it to (Broadband [WIFI] / Regional [50th Percentile]). I see a lot more bullets being spawned, then I would expect. It does clean them up given enough time.

netcode-doing-it-wrong

I’ve tried different solutions, looking at sample code and so on. Hope anyone can help figure out my mistake.

Removing the [GhostField] from WeaponFireTick makes it look more correct, but the samples I’ve found says that it should be there, which also seems right?

The projectile ghost authoring component:

image

The main components:

public struct PlayerInput : IInputComponentData
{
    public InputEvent Fire;
}

public struct Weapon : IComponentData
{
    public uint TicksBetweenShots;
    public float3 Offset;
    public Entity Prefab;
}

public struct WeaponFireTick : IComponentData
{
    [GhostField] public NetworkTick Value;
}

[GhostComponent(PrefabType = GhostPrefabType.AllPredicted, SendTypeOptimization = GhostSendType.OnlyPredictedClients)]
public struct Velocity : IComponentData
{
    [GhostField(Quantization=100, Smoothing=SmoothingAction.InterpolateAndExtrapolate)] public float3 Value;
}

The input system:

[UpdateInGroup(typeof(GhostInputSystemGroup))]
public partial class PlayerInputSystem : SystemBase
{
    private InputActions _input;

    protected override void OnCreate()
    {
        _input = new InputActions();
        _input.Enable();
    }
    
    protected override void OnUpdate()
    {
        var fire = _input.Player.Attack.IsPressed();

        foreach (var playerInput in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
        {
            playerInput.ValueRW = default;
            
            if (fire)
                playerInput.ValueRW.Fire.Set();
        }
    }
} 

Weapon Fire system:

[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[BurstCompile]
public partial struct WeaponFireSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var networkTime = SystemAPI.GetSingleton<NetworkTime>();

        if (!networkTime.IsFirstTimeFullyPredictingTick)
            return;

        var ecb = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
        
        foreach (var (weapon, weaponFireTick, transform, ghostOwner) in SystemAPI.Query<RefRO<Weapon>, RefRW<WeaponFireTick>, RefRO<LocalTransform>, RefRO<GhostOwner>>().WithAll<Simulate>())
        {
            if (weapon.ValueRO.IsFiring && (!weaponFireTick.ValueRO.Value.IsValid || networkTime.ServerTick.IsNewerThan(weaponFireTick.ValueRO.Value)))
            {
                weaponFireTick.ValueRW.Value = networkTime.ServerTick;
                weaponFireTick.ValueRW.Value.Add(weapon.ValueRO.TicksBetweenShots);

                var velocity = state.EntityManager.GetComponentData<Velocity>(weapon.ValueRO.Prefab).Value;

                for (var i = 0; i < 32; i++)
                {
                    var wr = math.mul(quaternion.AxisAngle(math.up(), i * (math.PI2 / 32)), transform.ValueRO.Rotation);
                    var wp = transform.ValueRO.Position + math.mul(wr, weapon.ValueRO.Offset);

                    var e = ecb.Instantiate(weapon.ValueRO.Prefab);
                    ecb.SetComponent(e, LocalTransform.FromPositionRotationScale(wp, wr, 0.4f));
                    ecb.SetComponent(e, new Velocity { Value = math.mul(wr, velocity) });
                    ecb.SetComponent(e, new GhostOwner { NetworkId = ghostOwner.ValueRO.NetworkId });
                }
            }
        }
    }
}

Projectile Move System:

[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
[BurstCompile]
public partial struct ProjectileMoveSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var dt = SystemAPI.Time.DeltaTime;

        foreach (var (transform, velocity) in SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>().WithAll<Simulate>())
        {
            transform.ValueRW.Position += velocity.ValueRO.Value * dt;
        }
    }
}

Do not make individual bullets network synchronized! The traffic these will generate is going to be insane!

You are already using DOTS so determinism is what you use. Each circle of bullets is a single event: fire ring of bullets. Every client then spawns those bullets, and since they move deterministically, every client simulates their movement completely outside of any networking.

1 Like

Hey! Two things to check:

  1. Log the variables to get a timeline of what is happening. E.g. UnityEngine.Debug.Log("[{state.WorldUnamanged.Name}] Spawning 32 bullets on tick {networkTime.ServerTick.ToFixedString()} as is newer than weaponFireTick:{weaponFireTick.ValueRO.Value}! weapon.TicksBetweenShots:{weapon.ValueRO.TicksBetweenShots}!");
  2. Move your if (!networkTime.IsFirstTimeFullyPredictingTick) return; check to be before line 24 (and thus change return to continue), so that, when re-predicting, you still update the ROF cooldowns exactly the same. My guess is that this is the problem. The IsFirstTimeFullyPredictingTick guard should only prevent you from spawning bullets again, not from updating the ROF timers.

And yes, this is almost certainly true (assuming bullets remain 100% predictable), but can be an optimization applied later.

1 Like

Thanks for the reply.

I should have clarified that I was just doing some stress testing. But thanks for providing a better solution for this. Definitely appreciated.

Ahh, that was the problem. Thank you.

Considering one may want them to e.g. bounce on other predicted entities & cause effect on the predicted world. How would you handle rollback & reconciliation for these non synced physics entities?