Code organization: How do you separate client & server projects?

Hello everyone,

I’m developing a multiplayer game with an authoritative server and the game client. The server is a .Net Core project, the client is - of course - a Unity project, and they communicate via REST calls (JSON).

I’m struggling a bit with the code organization (as in: where to put what) at the moment. Clearly, the server isn’t part of the client source code, and I don’t want unity to ship it. Also, it has vastly different nuget dependencies than the client; it is definitely its own project. However, there are some classes (e.g. for the definition of the exchange format) which I would very much like to share between the server and client projects.

How do you organize your client/server projects? Is it possible to have client and server as totally separate projects, yet share some common code between them without too much hassle?

The typical strategy is to create a model library. This can be pure C# classes if both client and server are C#. Then you simply have the client and server both use that same library.

In my professional life, client and server are often not using the same language. So we often use a “glue” framework such as Apache Avro, Protobufs, or Swagger Codegen

All of these frameworks allow you to defines your model in a separate model definition language, and then they can generate code for your client and server independently in whatever languages they need to use. Since you have C# on both sides you don’t necessarily need one of these, but in case you want to learn more:

https://avro.apache.org/
Protocol Buffers Documentation (protobufs in particular has its own binary commnication protocol which is more compact than JSON. So it replaces the model definition as well as the actual over-the-wire exchange format).
API Code & Client Generator | Swagger Codegen

2 Likes

Okay, that sounds good. Yeah, I’m a lot more familiar with the JVM world than .NET, but I figured that I should probably reuse as much code as I can, so I’m trying to write both in C#.

Sorry for the noob question, but how would I reference this model library project in Unity? Sure, I can build a DLL and include that, but doing that each time the model code changes seems very tedious.

Very much do exactly what @PraetorBlue suggests. Just put your model data in some shared library. Then both projects reference the same project.

You can do that. What I would do is create some script to copy the dll into the project any time it updates. Could be as simple as a post build script in Visual Studio that xcopy’s the dll output into the folder of you Unity project.
6902546--808097--upload_2021-3-4_15-37-38.png

Another option is you actually copy the source code files into Unity itself. You could do various things to embed your model project into Unity. For example you could have a symbolic link (os-level directory redirect from one folder to another elsewhere in the file system). Or you could have git’s submodules (or whatever vcs version of that you may have). Or again just have a build script that instead of copying the dll copied over the files.

Personally I’d just go the dll route. This way you can version the dll and easily validate that the server and client have the correct model version with a very basic test on the dll file itself.

2 Likes

I think I’ve found yet another way to do this. I don’t know if it’s considered “hacky” or not (CLR noob here, I usually work on the JVM) but you can manually edit your *.csproj file and include the source files in folders from somewhere else. So, the common library is currently residing within the unity project (no harm done, and unity is the more sensitive environment due to AOT compilation for WebGL), and in the web API project I do this in the *.csproj file:

  <ItemGroup>
    <Compile Include="..\..\unity\MyProject\Assets\Code\Main\Common\**\*.cs">
      <Link>Common\%(RecursiveDir)%(FileName)%(Extension)</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Compile>
  </ItemGroup>

At the very least, Rider seems to have zero issues with this. As an added bonus, I can now go ahead and edit the files from both projects easily. The only unity-import the common module had was for logging, and while that could be thrown out entirely, I’m trying a conditional compilation approach:

namespace myproject.common.util

public static class Log {

        public static void Info(string message,
            [System.Runtime.CompilerServices.CallerMemberName]
            string callerClassName = "",
            [System.Runtime.CompilerServices.CallerLineNumber]
            int sourceLineNumber = 0
        ) {
            var msg = Format("INFO", callerClassName, sourceLineNumber, message);
#if UNITY_EDITOR
            UnityEngine.Debug.Log(msg);
#elif UNITY_WEBGL
            UnityEngine.Debug.Log(msg);
#else
            System.Console.WriteLine(msg);
#endif
        }
       
        public static void Warn(string message,
            [System.Runtime.CompilerServices.CallerMemberName]
            string callerClassName = "",
            [System.Runtime.CompilerServices.CallerLineNumber]
            int sourceLineNumber = 0
        ) {
            var msg = Format("WARN", callerClassName, sourceLineNumber, message);
#if UNITY_EDITOR
            UnityEngine.Debug.LogWarning(msg);
#elif UNITY_WEBGL
            UnityEngine.Debug.LogWarning(msg);
#else
            System.Console.WriteLine(msg);
#endif
        }
       
        public static void Error(string message,
            [System.Runtime.CompilerServices.CallerMemberName]
            string callerClassName = "",
            [System.Runtime.CompilerServices.CallerLineNumber]
            int sourceLineNumber = 0
        ) {
            var msg = Format("ERROR", callerClassName, sourceLineNumber, message);
#if UNITY_EDITOR
            UnityEngine.Debug.LogError(msg);
#elif UNITY_WEBGL
            UnityEngine.Debug.LogError(msg);
#else
            System.Console.WriteLine(msg);
#endif
        }
       
        private static string Format(string level, string callerClass, int lineNumber, string message) {
            return $"[{level}] [{callerClass,32}:{lineNumber:000}] {message}";
        }

    }

This solution seems to work. Both unity and rider are happy, and I can actually already run the web API, so that seems to be good to go too.

Am I doing something truly dirty here? I genuinely have no idea, but it seems to work okay.

1 Like

You shouldn’t edit the proj files in Unity as Unity will overwrite them randomly and you’ll lose this configuration you forced into it.

If you google about editing the csproj file you’ll find tons of posts by people complaining that when they reload Unity it gets overwritten:
https://www.google.com/search?channel=fs&client=ubuntu&q=unity+edit+csproj+file

Thanks for the warning. You’re completely right about that (been there, done that myself…), but in this case I didn’t touch my *.csproj file in Unity at all :slight_smile: I’ve created a .net core web API project outside of unity, and this project has its own *.csproj file. In there, I’ve added the code above, instructing the build process to reach into the unity folder and grab the necessary *.cs files from there. So, for Unity, it looks as if it had the sole ownership over the code, and as far as Unity is concerned, there is no web api project.

1 Like

Oh, so you did it the other way around.

That works.

You may still wanna come up with a way to validate server and client are on the same version when you do builds. Maybe create a resource in the project that both Unity and the dll reference that holds its version info.

Could be as simple as a static/const string somewhere in it.

I totally agree. It’s the old problem of server version vs. client version. As with all network-based multiplayer games, I’m afraid the answer will be: clients have to keep up with server updates. I’m not going to bother with API versioning on this one. But some version check, at least, sounds like a good idea.

Thanks for the help and the input, I appreciate it!

2 Likes

Another neat solution is to just create a hardlink / junction of the folder that contains the shared library files. So it virtually exists in two places at the same time but only exist once physically. All you need to do is create an empty folder and use

mklink /J <link> <target>

Where “<link>” is the path to that empty folder and “<target>” is the path to the folder you want to link. After that both folders will virtually reference the same folder node. So all files inside that folder now have two locations from where they can be accessed. Of course you still have to be careful when you try to open the same file multiple times.