2

I want to keep network code separate from my game logic. Not only do I need to do that to be able to share game logic between single and multiplayer game modes, I also want it because of the Separation Of Concerns thing.

My current approach is to generate the code for my network related classes in such a way that there is an online and an offline version. I do this using T4 templates.

The resulting classes look like this:

Standalone/Singleplayer version:

// T4 GENERATED CODE
// Head (Singleplayer version)
class StandaloneHelloWorld : MonoBehaviour, IHelloWorld
{
    private string name;

    public void SayHello()
    {
        SayHelloInternal();
    }

// Body
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    void SayHelloInternal()
    {
        Debug.Log(Name + ": Hello World");
    }
}

Multiplayer version:

// T4 GENERATED CODE
// Head (Multiplayer version)
class NetworkedHelloWorld : NetworkBehaviour, IHelloWorld
{
    [SyncVar]
    private string name;

    public void SayHello()
    {
        CmdSayHello();
    }

    [Command]
    void CmdSayHello()
    {
        RpcSayHello();
    }

    [ClientRpc]
    void RpcSayHello()
    {
        SayHelloInternal();
    }

// Body
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    void SayHelloInternal()
    {
        Debug.Log(Name + ": Hello World");
    }
}

They both share an interface to hide the implementation from the callers:

interface IHelloWorld
{
    string Name { get; set; }
    void SayHello();
}

So as you can see, both implementations use the same body, sharing most of the code, while the entry points depend on the implementation being networked or not. Also note that the two implementations inherit different base classes.

Advantages:

  • Singleplayer code has no dependencies towards networked code and vice versa
  • No duplicate code (none that has to be maintained manually at least)

Disadvantages:

  • Support for interfaces in Unity is limited. I would not be able to reference scene instances of IHelloWorld from inside the Editor.
  • Having to maintain separate Prefabs for singleplayer/multiplayer game modes
  • Having to meddle with T4/code generation

Do you know of better ways to deal with this? How did you solve this problem?

Thomas Hilbert
  • 3,559
  • 2
  • 13
  • 33

1 Answers1

1

You could structure the code in an event-based fashion. This will allow systems to register to events they're interested in. This naturally separates the logic from the network code.

As an example, let's say you want to fire a projectile. You can fire it by calling:

new Event(EventType.FireProjectile, pos, dir, template)

You can then register systems that are interested in this event:

CollisionSystem.Register(EventType.FireProjectile, (e) => {
  CollisionSystem.AddCollider(e.template.bounds); 
});

AudioSystem.Register(EventType.FireProjectile, (e) => {
  AudioSystem.PlaySound("Woosh"); 
});

AISystem.Register(EventType.FireProjectile, (e) => {
  AISystem.AlertAtPosition(e.pos); 
});

What's cool is next you can register this event to the NetworkSystem that will serialize it, move it across the net, deserialize it, and fire it off on the client's machine. So as far as the client is concerned this event was called locally.

NetworkSystem.Register(EventType.FireProjectile, (e) => {
  NetworkSystem.Broadcast(e, Channel.Reliable); 
});

This is pretty great, except that you'll soon realize that this will cause an infinite loop of events. As you send a FireProjectile event to the other client, they catch it and fire it. Instantly their NetworkSystem catches it and fires it over the net.

To fix this you need two events for every action – a request: FireProjectile, and response: ProjectileFired.

I've worked with a codebase like this for a personal project a while ago. It's in C++, but if you're interested you can read more here. Notice how the server and the client are registering to certain events, which they will forward across.

Iggy
  • 4,767
  • 5
  • 23
  • 34