Coroutines and NetworkBehaviour's

Ive been working with Netcode for GameObjects and generally Unity Multiplayer for the first time for a few months now.
I am atm “stuck” on a pattern, and Id love to hear from more experienced ppl than me.

So, I have a coroutine to generate the content of my game.

Inside that routine, I have other coroutines for specific tasks.
The first routine inside it is to simply fade the screen to black.

[SerializeField]
private FadeElement _fadeElement;

private IEnumerator IGenerateContent()
{
    yield return _fadeElement.IFade(false);
}

Now, in order for all players to have their screens fade I came up with a bit of a workaround.
I dont know if this is a valid way of solving this, but it seems to work, so I liked it.

public IEnumerator IFade(bool fadeIn)
{
    _isFading = true;
    FadeClientRpc(fadeIn);
    while (_isFading)
        yield return null;
}

[ClientRpc]
private void FadeClientRpc(bool fadeIn)
{
    StartCoroutine(IRealFade(fadeIn));
}

private IEnumerator IRealFade(bool fadeIn)
{
}

The idea/problem Im solving here is that coroutines cannot be ClientRpc’s, so I just “cheat” my way around it, by having a coroutine that just calls an RPC and then idles until the “real” coroutine is done and then yields back to the actual Coroutine that is generating the rest of the stuff.

Like I said, I tested this and it worked.
Because of that, I thought “this is a very repeatable pattern” so I tried to make a class for it.
(dno why the class itself doesnt show up in the code block stuff, sry for that)

public class NetworkCoroutineTool : NetworkBehaviour
{

public IEnumerator INetworkCoroutine(IEnumerator toExecute)
{
    ExecuteClientRpc(toExecute);
    yield return null;
    while (toExecute.MoveNext())
        yield return null;
}


[ClientRpc]
private void ExecuteClientRpc(IEnumerator toExecute)
{
    CoroutineSource.Instance.StartCoroutine(toExecute);
}

}

But here is where I got stuck.
A NetworkBehaviour class cannot deal with the IEnumerator as a variable. The error I got from that is:

Unity.Netcode.Editor.CodeGen.NetworkBehaviourILPP: Assets\Scripts\OnlineFunctionality\NetworkCoroutineTool.cs(23,9): error - ExeClientRpc - Don’t know how to serialize System.Collections.IEnumerator. RPC parameter types must either implement INetworkSerializeByMemcpy or INetworkSerializable. If this type is external and you are sure its memory layout makes it serializable by memcpy, you can replace System.Collections.IEnumerator with ForceNetworkSerializeByMemcpy`1<System.Collections.IEnumerator>, or you can create extension methods for FastBufferReader.ReadValueSafe(this FastBufferReader, out System.Collections.IEnumerator) and FastBufferWriter.WriteValueSafe(this FastBufferWriter, in System.Collections.IEnumerator) to define serialization for this type.

Which I tried to find good examples to solve for, but I didnt find anything super obvious (I likely didnt do a good job).

But that is where my real question comes in.

Is this a legitimate pattern? Does something like this already exist?
Or am I entirely approaching this problem in a wrong way?

Any input is appreciated. Like I said, I am not very experienced with Multiplayer.

No. No. And yes. :slight_smile:

You can’t just send anything with an RPC method, it needs to implement INetworkSerializable. Since the IEnumerator is just an interface, it cannot be sent in general because … on the receiving, what class should this be? It’s just an interface so any number of classes (or structs) could be implementing this interface.

I think where you, like so many others, took a wrong turn is to not mentally separate networked events from the logic / visuals they affect.

In your case it’s as simple as this:

  • Server sends a StartFadeClientRpc to all clients
  • Clients receive the RPC and start the coroutine that does the fading

This is a single network event: fading should start. There is no need to specify how the fading is implemented because every client runs this on the same script. There is no need to track time over the network or call a stop fade RPC because the fading has a fixed time, so it will naturally end for every client.

To have two different things fade at different times, you’d implement two different RPCs.

If you want to make this more flexible, ie fade any number of things, then you only need to send an RPC “CloseThis(n)” with an additional index parameter. The fading of the selected GUI element would be implicit because it can be a built-in behaviour in the element. The index would be for a predefined GUI elements list, for instance a [SerializeField IFadeable[] m_FadableItems; array that you add elements to in the Inspector.

The indexing works the same with any type really, and you may even want to define those lists as ScriptableObject assets for indexing other assets. For instance I do this with Avatars, Weapons, and so forth.

Such a list could also be filled dynamically, eg the player may have a choice of three weapons but the RPC would still send something like ClientChangedWeaponSlotServerRpc(byte slotIndex, byte weaponIndex). This would then instruct the server to assign the weaponIndex to the given slotIndex so every client knows what weapon this player has in that particular slot. Weapon indexes are ScriptableObject list indexes meaning you have predefined weapons. The slotIndex could be hardcoded to have 3 entries at all times.

The values are sent as bytes because the game is unlikely to feature 255 or more weapons, so why send 8 bytes when 2 bytes suffice? That’s 75% less traffic when ignoring the RPC overhead and any internal bitpacking optimizations. Networking is all about keeping the traffic to a minimum. Someone will have to pay for it, either the players in terms of maxing out their upstream with just 4 players rather than 6 or 8 or you if you plan on renting dedicated game servers.

In essence, in my project I only have things like:

NetworkVariable<byte> m_AvatarIndex;
NetworkVariable<byte> m_ActiveWeaponIndex;
// other things could be indexes too, such as character equipment, a list of active (de)buff indexes, and so forth

In case of changing the Avatar, the corresponding change event will destroy the existing child Avatar object (if any) and then instantiate the new Avatar based on an index defined in a ScriptableObject. The values are int on the script side for ease of use and to make it easier to change the netvar type in the future.

It’s bad practice to prefix method names with ‘I’. The ‘I’ is a reserved prefix for interface types but not interface methods. This makes the code confusing to read.