Making already fired Projectiles sync with newly connected Clients

How can I make projectiles,which were fired in server-host, to make them accessible to newly joined clients

The order to recreate:

  1. Make very slow projectiles with long lifetime
  2. Launch server-host
  3. On host fire several projectiles
  4. On client try to join host
  5. The client would not see already fired projectiles by host

I think you can use OnNetworkSpawn in the ProjectileLauncher. When a client joins it will join the network and this will run on it. The server will then need to find any currently active projectiles and recreate them on this newly joined client with all the details like speed, rotation, lifetime, etc. in tact. I don’t have a copy that matches the course atm, so testing this and giving you a detailed ‘how-to’ would take some time. None of my half-done multiplayer projects allow you to ‘shoot’ before the client joined, and no client can join after the game has started

I made some changes in ProjectileLauncher


Note: My code is little bit different than in lectures, I prefer different approaches and naming, but the logic is exactly the same as in lectures

These changes fixed my issue, but after thorough testing various scenarious I came to this error:


These error messages appear when:

  1. Build version becomes Host and fires projectiles
  2. Editor triest to join host as a client
  3. Editor sees all projectiles, which were fired befor joining
  4. When Editor tries to touch these projectiles, then these error messages appear

Thanks for the hint, will try to implement your idea

Interesting question and a strange bug you have there.
I think our Projectiles in the course are not NetworkObjects. Did you turn them into NetworkObjects for your purposes?

We create the projectile simultaneously on both server and clients and rely on their constant speed and trajectory to make them function correctly.

So what you would need to do on the client, is just spawn the visual that is missing. You would need to send a ClientRpc that runs SpawnDummyProjectile only on the client that is new, so probably pass a clientId of the new player in there.

On the server side you you need to call this new Rpc for all the projectiles in the scene and pass their latest position, and direction values in so as to spawn them at the right point.

Hope that helps!

The bugs come from are where I try to destroy network object which were spawned on server.
The issue is when I try to destroy these objects from client (which is absolutely a mistake)

I think the root is in the design, the lectures show a way to deal this issue but also provide user with responsive game - it spawns client projectiles and server projectiles separately.
At this point I understood that I should pick one of two options:

  1. Make every projectile on server - it will solve my issue, but at cost of responsiveness on clients’ side
  2. Stick with lectures’ approach - but it will require lots of checks and gathering info for newly joined clients so they could pre-spawn all already fired projectiles

At this point I’m still thinking of implementing the 2nd idea.
I’m still a learner, and I’m trying to find a way to cache all fired and alive projectiles on server and provide this info for newly joined clients, so each client could pre-spawn these projectiles.

1 Like

You don’t necessarily have to cache the information. Since all our projectiles have a projectile component attached, you could use a FindObjectOfType() to get all the projectiles in the scene. It’s a heavier operation, but you would only be doing it once when a new player connects.

Then you can loop through those and spawn them on the new client.

I finally made it in a way that I wanted

But I had to use a static field, I’m still a learner though, and I couldn’t find other way

Main changes:
Add these variables in ProjectileFire class

private static ProjectileFire serverProjectileFire = null;
private List<Projectile> activeProjectiles = new List<Projectile>();

On network spawn store itself in a new static variable

public override void OnNetworkSpawn()
{
    if (!IsOwner) { return; }
    inputReader.PrimaryFireEvent.AddListener(HandleFilre);
    if (IsServer)
    {
        serverProjectileFire = this;
    }
    else
    {
        CallForServerAlliveProjectilesServerRpc();
    }
}

On server fire rpc add aditional code to store all fired projectiles

private void ClearProjectilesList(Projectile projectile)
{
    serverProjectileFire.activeProjectiles.Remove(projectile);
}

[ServerRpc]
private void FireProjectileServerRpc(Vector3 position, Vector3 direction, ServerRpcParams serverRpc = default)
{
    spawnedPrefab = Instantiate(serverProjectilePrefab, position, Quaternion.identity);
    spawnedPrefab.transform.up = direction;
    Physics2D.IgnoreCollision(playerCollider, spawnedPrefab.GetComponent<Collider2D>());
    if (spawnedPrefab.TryGetComponent<DealDamageOnContact>(out DealDamageOnContact dealDamageOnContact))
    {
        dealDamageOnContact.SetOwner(OwnerClientId);
    }
    if (spawnedPrefab.TryGetComponent<Rigidbody2D>(out Rigidbody2D rb))
    {
        rb.velocity = rb.transform.up * projectileSpeed;
    }
    FireProjectileClientRpc(position, direction);
    if(!IsServer) { return; }
    if (serverProjectileFire != null && spawnedPrefab.TryGetComponent<Projectile>(out spawnedProjectile))
    {
        spawnedProjectile.OnProjectileDestroy.AddListener(ClearProjectilesList);
        serverProjectileFire.activeProjectiles.Add(spawnedProjectile);
    }
}

Note: I use a Projectile class for all projectiles, inside I also made an UnityEvent, which is called whenever the projectile is destroyed so it can clear the

private List<Projectile> activeProjectiles = new List<Projectile>();

and keep it up-to-date

So, whenever a new client tries to join it will call this ServerRpc function in OnNetworkSpawn code:

[ServerRpc]
private void CallForServerAlliveProjectilesServerRpc(ServerRpcParams serverRpc = default)
{
    foreach (Projectile item in serverProjectileFire.activeProjectiles)
    {
        SpawnMissingProjectileClientRpc(serverRpc.Receive.SenderClientId, item.transform.position, item.transform.rotation, item.Rigidbody2D.velocity, item.LifeTimer);
    }
}

Which ultimately will call this client rpc:

[ClientRpc]
private void SpawnMissingProjectileClientRpc(ulong ownerClientId, Vector3 position, Quaternion rotation, Vector3 velocity, float lifeTimer)
{
    if (ownerClientId == OwnerClientId)
    {
        spawnedPrefab = Instantiate(clientProjectilePrefab, position, Quaternion.identity);
        spawnedPrefab.transform.rotation = rotation;
        if (spawnedPrefab.TryGetComponent<Rigidbody2D>(out Rigidbody2D rb))
        {
            rb.velocity = velocity;
        }
        if (spawnedPrefab.TryGetComponent<Projectile>(out spawnedProjectile))
        {
            spawnedProjectile.LifeTimer = lifeTimer;
        }
    }
}

In order to above code to work you need to make a Projectile class which will include Lifetimer, destory on collision and other necessary stuff:

[RequireComponent(typeof(Rigidbody2D))]
public class Projectile : NetworkBehaviour
{
    [SerializeField] private LayerMask targetMask;
    [SerializeField] private float lifeTime = 1.5f;
    public float LifeTimer { get => lifeTimer; set => lifeTimer = value; }
    public Rigidbody2D Rigidbody2D { get => rb; }
    public UnityEvent<Projectile> OnProjectileDestroy = new UnityEvent<Projectile>();

    private Rigidbody2D rb;
    private float lifeTimer;
    private void Awake()
    {
        lifeTimer = lifeTime;
        rb = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        lifeTimer -= Time.deltaTime;
        if (lifeTimer < 0)
        {
            DestroySelf(this);
        }
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (Utils.CheckLayer(targetMask, other.gameObject.layer))
        {
            DestroySelf(this);
        }
    }

    private void DestroySelf(Projectile projectile)
    {
        OnProjectileDestroy?.Invoke(projectile);
        Destroy(gameObject);
    }
}

The Utils.CheckLayer looks like this:

public static class Utils
{
    public static bool CheckLayer(LayerMask layerMask, int layer)
    {
        if ((layerMask.value & (1 << layer)) > 0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

Thanks Yitzchak_Cohen and bixarrio for provided ideas

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms