I've recently been developing a networking application layer (or at least attempting to) for my game I've been working on. I think I've got a decent basic idea for the system now, but there is multiple ways of implementing it (I refer to the two difference ways as class-inheritence and struct-interface) and I'm at a crossroads between choosing which way is better.
The basic idea for the system is:
- Each packet definition and implementation is contained within it's own .cs file.
- These packet files will be contained in a SharedProject so they can be referenced by both the client and the server projects from a single source.
- Each packet will have the same member variables, but will be handled differently depending on the target build (e.g. PLAYER_CLIENT, GAME_SERVER) using a conditional compiler directive, so they can access target specific namespaces, classes, etc.
- The header (aka the "id") of each packet will be computed and not hardcoded. Right now I'm doing it at runtime (during the initialization), which carries some limitations.
Here are examples of what the system would look like:
There are few things to notice:
I am uncertain about a few choices I made here.
In the struct-interface system, when the PacketManager receives a packet, it uses a compiled lambda expression to create an instance of the appropriate type based on the header it reads.
In the class-inheritance system, to get a packet instance from type (e.g. for serializing and sending), I use e.g.
PacketManager.GetPacketInstance(typeof(ExamplePacket)) as ExamplePacket;I guess for performance reasons, is there anything here that looks really bad? I'm okay having a sub-optimal system, as long as it's not actually terrible.
In the struct-interface system, packet files are longer as they have to contain the redundant boilerplate code for throwing "not implemented" exceptions. Default method implementations can be used to eliminate this problem, which is what the class-inheritance system does. As of C# 8, interfaces can have default method implementations as well, but unfortunately I use Unity which doesn't support them yet. If Unity were to support them in the future, would it even be an appropriate use in this scenario? From what I've read, it wouldn't really fit what they were intended for; but it would perhaps have the same effect and solve the problem nonetheless?
In the struct-interface system, packets are able to be sent without referencing some dictionary or pool (I cache the packets to minimize GC in the class-inheritance system); so they can be created from anywhere.
Perhaps knocking any benefit from point 3, in both systems the process of sending packets requires interacting with the PacketManager class: the struct-interface system uses it to serialize and send the packet, and the class-inheritance system uses it to get a pooled instance.
A few more notes:
I've thought about using codegen to try and alleviate the limitations that come with the struct-interface system (i.e. reduce the amount of repetitive work and perhaps to also compute headers pre-build), but I have a hunch that doing so may cause more issues down the road by adding another layer of "complexity" to the system. I don't plan on multithreading the system, and in the unlikely event that I do, it probably won't go beyond a couple of threads.
I guess most of these "limitations" are pretty minor. Both systems are quite similar and would probably work fine either way.
My intuition tells me that structs are better suited as being the container for packets. A couple of reasons for this for this are:
- Size is not a concern. On average, the packets will by tiny (< 16 bytes) and are passed by reference anyways to avoid copying.
- Packets are "short lived". They aren't used anymore after handling or sending.
I guess I should make it clear I'm referring to my own "RpcPackets" here, not the ENet packets that might also appear in my code examples.
Please correct me if I've made any wrong assumptions (especially on the correct use of structs) in this post.
I want to know if I'm correct in thinking that structs are an appropriate choice in this situation, or if I'd actually be better off using classes. Also any insights or opinions about the system in general (I hope nothing is too bad) would be greatly appreciated.