-3

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:

  1. Each packet definition and implementation is contained within it's own .cs file.
  2. 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.
  3. 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.
  4. 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:

  1. 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.

  2. 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?

  3. 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.

  4. 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.

11
  • 2
    "I guess for performance reasons" - I haven't read your whole question, but I saw this. In c# structs vs classes have no general, easy to reason about rules regarding performance. See stackoverflow.com/a/3943154/8159, docs.microsoft.com/en-us/archive/blogs/ericlippert/… and docs.microsoft.com/en-us/archive/blogs/ericlippert/… Commented Sep 23, 2020 at 21:27
  • 1
    Structs can inherit, classes can implement interfaces. Your question seems to be about the interface/inheritance distinction. For the purpose of what's in your question, there is no meaningful distinction between structs and classes. What is your actual question? Are you asking what the difference between classes and structs are? Are you asking whether to use interfaces or inheritance? What goal are you trying to achieve with the choice you're trying to make? Commented Sep 23, 2020 at 23:07
  • Submitted to your consideration: no inheritance, no interfaces (and no boxing) on packets, have helper serializer and deserializer types that implement a generic interface (I consider those a different concern). You can do type discovery, or initialize with a hardcoded list (if using code generation, for example), or mark using an attribute. If some of them are server or client only, you can simply not have those helper types available on those builds. Have no NotImplementedException. Then struct vs class should be clearer (GC, Threading, Pooling). See also blittable types and marshalling. Commented Sep 24, 2020 at 3:24
  • @whatsisname In that sentence I was referencing what I'd mentioned just before (the particular implementation details using compiled lambda and casting). I'm familiar with the concepts mentioned in those articles but it was still a good read to refresh and remind myself of those things. Thanks. Commented Sep 24, 2020 at 4:47
  • @Flater "Structs can inherit".. is this true? From what I've read it seems only classes are designed to have inheritance. As for what I'm asking, I'd really just like to know if what I've thought up is a somewhat good design. At first it seemed like a good idea but now after implementing part of it and running into these non-ideal characteristics, it makes me think I've chosen the "wrong way" of doing things. I'm not sure what they teach in practice, so am curious if anything stands out as unconventional or "wrong. I tried to narrow it down to the things I'm uncertain or least confident in. Commented Sep 24, 2020 at 4:58

1 Answer 1

0

Where should your data end and your interface start? I think that's an interesting design question. Is your packet modeling data or an interface? There's no right and wrong answer. I'm just asking you. The right answer is your answer if your heart says it is so and you don't get a boatload of bugs and inefficiencies from it. I think it's "data" but that's just my opinion.

Lately, I always defaulted to the notion that something should just be data unless it is used very widely. Because most things are implementation details. We can talk about the mechanics of struct vs. class or whatnot but who gives AF. In C++ the only difference is the default access of members. I'm trying to communicate something and say something. So when I say struct, usually I'm saying this is data. This is not where the interface starts. This is what the interface uses. What are you trying to say? Yeah, you can put an interface over anything and maintain invariants. I've seen devs so zealous that they can't even be bothered to use a float without putting an interface like Speed over it. Thanks for making my debugging life harder than it needs to be.

So you say what you want. And I have found that saying what we want is kind of a problem. But it is not a problem with code usually if you do it right. Take these feminists saying they don't want to be "objectified". I am confused. Objectifying something is really paying attention to it and giving so much TLC. To objectify something in C++, I gotta give it a copy constructor, move constructor, various parametric constructors, default constructor, default constructor, maybe a swap method, copy assignment, and so forth. I put their privates into a place where no one else can access it without their permission and properly maintain vaginal/pregnancy invariants and now they think I'm some part of the patriarchy. I might as well just reduce them down to data at this point and structify them if they think "objectifying" is offensive rather than flattering. Ungrateful people -- I'll make them all into structs.

So what is worth your love, ignoring politics and whatnot? Is it worth giving love to this packet? Cause love always carries a cost. I would lean towards struct-ifying otherwise. Just make it a bundle of data, like a struct, an array, or an ArrayList`, or whatnot. Bundle of data. Save your interfaces for something really worthwhile.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.