Search Unity

Game networking system

Discussion in 'Scripting' started by RickP, Jan 16, 2020.

  1. RickP

    RickP

    Joined:
    Apr 4, 2010
    Posts:
    262
    I'm creating a turn based kind of game. You can do a bunch of actions like move character units around, attack, etc then end your turn. I'm normally a big "system" guy in that I like to not really brute force things. I'm using Unity, but I created my own state machine system and I created a more event driven entity component system. I like systems.

    I've used UDP libraries before, but am not all that experienced in networking libraries, and I am able to get communication working, but it feels very brute force right now.

    I already had the code to run the game on 1 PC then I added networking. So I have a network code class and hook into the state machine and abilities when needed to get information. I send message ID's and the data needed across. It's not much of a system and couldn't really be reused. Now I get the idea behind "as long as it works", etc. but I'm a system guy and I like using more of a system that is more configured than programmed (of course I program it).

    Any ideas on how to make networking a game like this more of a system? I mean at the end of the day I'm sort of syncing values and kicking off tasks and having the server validate and relay all of that to the opponent. There has to be a better way for that kind of thing.

    Right now I have a dictionary of Actions where the key is a string (I'll probably change to enum later) message. So a message comes in and it calls a function passing all the relevant networking data. That function can read the data sent and then gets references to a gameplay type class and state machines that control things. All networking messages are coming into 1 NetworkCode class I have. I don't like that idea as this class could get pretty big then. The biggest problem I generally have with any system is data accessibility. Having some method, like a network message method, somehow able to manipulate another part of the game without having everything be public.

    The kinds of things that come to mind are, can you use annotations on certain things to get a more configurable way to sync things? Stuff like that.

    Any thoughts/ideas/tips on stuff like this?
     
  2. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    Well you can switch to a ready made high level network API, which generally have features like syncing variables, remote RPC's, instantiating/destroying GameObjects across all connected clients, etc. You could also consider building your own such system as well, basically your own network API. It can be built to fit your specific game, or built more general so you could use it across any number of future games.
     
  3. RickP

    RickP

    Joined:
    Apr 4, 2010
    Posts:
    262
    Thanks for the reply. The post was meant to go the route of creating my own and how to go about it. I've got a start on it now and curious of any thoughts/opinions.


    What I had was 1 giant Network class that received the messages (let's use client side for now). The server sends a message and it would call a method inside this Network class. I'd read the data that came along this message and then I'd have to inform whatever objects needed to be informed for this network message. That could be a state in my state machine or a given game object, a chat class, whatever. This obviously means my Network class was getting large and it had it's hands into every other area/object in the game which I don't like as it's very coupled which makes it fragile.

    So my initial thought here is that any class should be able to define method(s) that can be called for a given network message and they can do with the data as they see fit. The Network class basically has a dictionary of network messages as keys and list of delegates as values. My Gameplay class basically creates all game objects so it's in the Start() method that I use the Network class. For every gameplay object it loops through the methods looking for anything with a given attribute (NetworkMethod) and subscribes that delegate to that network message. In the Network class when it receives a message that function then loops over all the delegates for that message and calls them (it fills in the argument parameter for that given message as well). Now all those classes can do what they want with the data for a given network message.

    From a usage standpoint it ends up being something like:

    Code (CSharp):
    1. class OpponentTeam
    2.     {
    3.         private int mana = 0;
    4.  
    5.         public OpponentTeam()
    6.         {
    7.  
    8.         }
    9.  
    10.         [NetworkMethod(NetworkMessages.OPPONENT_MOVE)]
    11.         public void UpdateMana(OpponentMoveMessage args)
    12.         {
    13.             mana = args.Mana;
    14.         }
    15.     }
    16.  
    17.  
    18. // the creation part
    19. OpponentTeam opponentTeam = new OpponentTeam();
    20.  
    21. // the registration part
    22. RegisterNetworkMethods(network, opponentTeam);

    I don't like having to explicitly call RegisterNetworkMethods(). It would be nice if there was a way to just get all objects (MonoBehavior or otherwise) and loop over them passing them into RegisterNetworkMethods(). That method uses reflection to check if a NetworkMethod attribute exists and if so store it as a delegate to be called with the message parameter in that attribute. I guess ideally it would be handy to have some kind of hook whenever ANY object is created to run this on. That might slow things down though for object creation I suppose. Just thinking out loud.
     
  4. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847
    You can just split the monolithic network class into multiple classes, each handling its own area of the system, and you can build it to be more modular so it is easier to expand on with other scripts without editing the core network scripts.

    On that last part, my system implements different message type codes. Some of them are automatically handled by the network system, like where the message needs to be delivered to a specific GameObject/Component, but I have other codes that I handle using a 'handler' script I think I call it. That's just a script I write specific to messages for the specific project, and I just drag it over into one of the main networking scripts' public reference for a handler script. That way I keep everything that is project specific outside of the actual network system scripts. I can also use different handler scripts for client and server in the same project.
     
  5. RickP

    RickP

    Joined:
    Apr 4, 2010
    Posts:
    262
    I don't think splitting a "network" class up into multiple network classes solves the monolithic idea. In my view the idea of that is still very monolithic. I'm working on making my network class very generic and really it's simply just a routing class that routes network messages to where they need to go (calling other class functions that care about the network message in question where an attribute on that method tells me they are interested in that network message). Sounds similar to the first part of what you're saying and really even the second part of what you said sounds like your networking class really just routes to those specific scripts per message.

    There are probably some semantics here but if there are multiple scripts that are heavy network related (know about any sort of network structures) then it's really part of the overall network class creating a very monolithic idea of handling network traffic. I wanted a way to generically route any network message to any object method that the coder decides needs to know about it while keeping that object as decoupled to anything network related.

    What I wanted to avoid, and I started it out this way, is inside a network message function (that is highly network coupled, say has a net data reader in it) read the data, then tries to access the objects it needs and then directly updates those objects (calling methods and passing the network data to them). The network methods were needing access to all sorts of game objects to call methods to update data in them and it was just getting messy. Felt way to coupled together even if that msg was game specific access a game specific game object. I didn't want my network class methods needing all these references to game objects.It was turning into a spider web mess of dependencies. I just didn't feel any network class should need to know about all these other objects in such an intimate way.

    I think what I have now is working well. The example I gave in the above post shows this. The OpponentTeam object will need it's Mana variable updated by the server. That's the business case. And the design starts to build around how can we do that.The brute force design is of course in a network class when packets come in you check the msg packet and do a switch case on it. Inside the the case statement to the msg you care about you get a reference to the OpponentTeam object that was created somewhere and update it's mana. Right to the point! Of course the case statement grows out of control so I change it to a dictionary where the key is the network msg and the value is an Action method. Now I've removed the case statement which is good, but that network callback method still needs a reference to the OpponentTeam at the time the msg happens to update the data. Still not ideal. That network class is now intimately coupled with the OpponentTeam class.

    So instead, wherever an object is created I can call a register method from the Network class passing in any object. It'll look for the custom method attribute I created and if it exists, it'll read it's parameter to get the network message in question and add it as a delegate to the network dictionary. Then when that network message comes in we find all delegates we've registered to that msg and call them. We do some tricks for the argument so we get actual types for arguments and different ones per network msg and now it's more loosely coupled and the objects that cares about the network message gets informed of the network message when it happens via the delegate callback.

    Wall of text I know :). I just like talking design.
     
    Joe-Censored likes this.
  6. Joe-Censored

    Joe-Censored

    Joined:
    Mar 26, 2013
    Posts:
    11,847

    Now I'm not saying my system is any better than yours, or is in any way ideal, but I'm just going to describe how it works and how my network system can sync variables and call remote methods without actually knowing anything about the individual scripts on networked objects or having to maintain a spider web of references.

    If I had a networked GameObject called OpponentTeam it would have a script attached I call JCGIdentity, which every networked object in my system has. The server instantiates the object and the JCGIdentity component is assigned a unique ID, then the GameObject is spawned on all clients and keeps that same unique ID. All network aware components capable of sending/receiving RPC's and syncing SyncVars (yes I took a bit of inspiration from Unet) inherit from JCGNetworkBehaviour instead of MonoBehaviour. On instantiation in Awake the JCGIdentity finds all attached JCGNetworkBehaviour scripts and adds them to a list it maintains.

    When a script attached to OpponentTeam on the server updates its Mana SyncVar I would also set a variable SyncDirty to true. The next time Update is called on JCGIdentity it calls Sync() on all the JCGNetworkBehaviour scripts in its list. JCGNetworkBehaviour has virtual methods Serialize and Deserialize which I implement on any script which inherits from it to serialize/deserialize all of its SyncVars to and from a byte array. In Sync() if SyncDirty is true, its Serialize() method is called. The resulting byte array is then packaged into a network message identifying it as a SyncVar message with the index of its component in the JCGIdentity list of networked components, and with the unique ID of the JCGIdentity. The message is then sent into core of the network system and sent to all the connected clients who should receive the message (might be everyone, might be just connections who are subscribed to the object, or might just be the owner, depending on what is selected on the component in the inspector).

    When any client receives the message, the process is basically reversed. The client sees it as a message directed to a specific GameObject so finds its JCGIdentity in its list based on the included unique ID. It then passes the message off to that JCGIdentity component. The JCGIdentity component then sees it is a SyncVar message and sends the message to the JCGNetworkBehaviour script based on index number included in the message. That JCGNetworkBehaviour script then passes off the byte array payload of the message to that script's implemented Deserialize method and the SyncVars are then updated.

    I do something somewhat similar for RPC's (or my version of RPC's which are actually just pairs of separate sending and receiving methods, so not real RPC's I guess). Each JCGNetworkBehaviour script contains a list of delegates, which are methods for receiving RPC messages. In Awake any JCGNetworkBehaviour which implements RPC's adds the receiving methods to that delegate list. When sending an RPC it includes the index number of the method needed to be called to receive it. It includes who should receive it (again everyone, subscribed only, or an individual connection), that it is an RPC message, and the rest is basically the same as it sends it to the JCGIdentity script. On the other side it reverses the process the same as the SyncVars except it also finds the correct delegate method in the delegate list and calls that method passing off the RPC message. The RPC message could be its own class for more complicated data which includes its own serialize/deserialize methods, or if simple I just deserialize the message directly in the receiving RPC method.

    So in this way the network system actually doesn't know anything about the actual scripts on the networked objects, doesn't need to maintain any references other than the list of JCGIdentity components and the internal references the JCGIdentity component itself keeps between the JCGNetworkBehaviour components, and communication is just done through the index numbers in all these lists without having to know anything about the scripts themselves. Again not saying this is a better way or even a good way, but it does eliminate the networking system itself needing to know anything about what the objects themselves do or what the network aware components are for.

    Up in comment number #4 the 'handler' script I was referring to is just used for network messages which aren't directed to any specific object.
     
    Last edited: Jan 18, 2020
  7. RickP

    RickP

    Joined:
    Apr 4, 2010
    Posts:
    262
    No need to disclaimer :). I like hearing different ideas about systems people make!

    How are you defining sync vars? Do you put attributes above the variables or generics? SyncVar<int> Mana?

    "When any client receives the message, the process is basically reversed. The client sees it as a message directed to a specific GameObject so finds its JCGIdentity in its list based on the included unique ID"

    So the network "onReceive()" is being called somewhere on the client side when the packets come in. How is that getting a list of all networked objects so it can find that networked game object by unique ID in the message? Some kind of static/singleton holding this list that you're adding to inside Awake() in JCGIdentity?

    A lot to digest in your post and I really do want to thank you for explaining things. I'm sure I'll have a couple more question tomorrow as I digest this.