Tallowmere 2
Work in progress. Subject to change. More fun than you can shake a kitten at.
Tallowmere 2

Lady Tallowmere has begun expanding her empire.

Tallowmere 2 is an upcoming 2D dungeon platformer game, currently in its early stages, being developed by Chris McFarland in Auckland, New Zealand.

Delve Further
Blog Post

Networking with TNet

25 April 2016  •  Chris McFarland

I've ditched Unity's built-in UNet networking in favour of Tasharen Entertainment's TNet.

  • TNet gives me full source code that I can modify easily.
  • TNet gives me the choice between UDP and TCP.
  • TNet gives me a small message server I can host anywhere and let players connect to that.
  • TNet opens up users' ports on their routers using UPnP automatically.
  • TNet is pretty awesome.

Stress-test

Below is a video of me stress-testing 20 mobs on 3 instances of Tallowmere 2, with TNet's message server being hosted on a server in California.


For the most part, things are synced excellently. I need to improve some of my code for the AI and position syncing, but to use TNet and get this far is truly great.

What needs to be synced?

My creatures share a common base Creature class. Creatures are either controlled by a Player or a CreatureAI module. In a sense, the creature is just a dumb host, and is brought to life by a parasite, a controller, eg either the Player or the AI.

After three rewrites of how to best code the network functionality for both Players and AI, I've devised to have Creatures use the following info/states for the needs of my two-dimensional world:

  • Does this creature belong to a Player? (boolean)
  • Does this creature belong to an AI module? (boolean)
  • What is the creature's movement state (standing still, moving west, or moving east)? (MovementState byte enum)
  • Should the creature be facing east? (boolean)
  • Should the creature start attacking with its weapon? (boolean)
  • Should the creature have its shield raised? (boolean)
  • Should the creature initiate a jump? (boolean)
  • What is the creature's X & Y position? (Vector2)
  • What is the creature's velocity? (Vector2)

Creatures are then manipulated by setting the bools, states, and position and velocity data.

I've largely settled for using TNet's DataNode class to transmit this info.

DataNodes act like a generic XML or JSON type of Dictionary, with strings for keys for any type of data I wish to include. This means I don't have to write methods with super long parameters; just a single DataNode parameter is sufficient in most cases. This means I can include multiple sets of info into a single message, which I believe feels pretty efficient, and also makes my life easier as I write the code.

Connection flow

Rough overview of how this all works:

  1. I, or potentially a customer, hosts a TNet server somewhere. (TNet also allows you to host the game yourself without the need for the message server to be used)

  2. Player connects to the server using TNManager.Connect.

  3. When OnConnect is called, player calls JoinChannel and joins a channel for a dungeon. Channels are merely message passers that are only sent to whoever's in the channel. In theory, you could have multiple dungeons running across multiple clients using multiple channels.

  4. When OnJoinChannel is called, player calls TNManager.Instantiate which requests that a Player class is created for all current and future players that join. Note that if this player leaves, this command is removed from the buffer.

  5. When the Player object (which is just an invisible logic class) is spawned, the Player object calls TNManager.Instantiate to create a Creature object that will be controlled by the Player. This Creature object starts disabled and hidden.

  6. Once the Creature object for the Player is created, the Creature object asks the server/host for the latest info using TNObject.Send, such as where are we positioned, what class type are we, what equipment are we using, how much health do we have, etc. (It's worth noting at this point that both my Player and Creature classes have a TNObject component attached to them)

  7. The server/host receives the request from the Creature, creates a DataNode with the latest info, and uses TNObject.Send to send the DataNode to the Creature. The Creature receives this DataNode, parses and applies it, and finally enables itself to be a living thing within the dungeon.

TNet's default code for TNManager.Instantiate had the newly-instantiated object be active from the start, but I modified the code to ensure it's disabled to start with, as I need the latest Creature data to be synced and parsed before enabling the Creature.

With this connection flow, when any new player joins the game, they are able to have all Players and Creatures be instantiated and synced, which is awesome.

As I develop this further, I plan to use object pooling instead of destroying Creatures when they die. So it's possible that a Creature might actually be dead, so a new player would receive data to create a Creature but wouldn't need to be activated.

Syncing Creature states

When it comes to telling a Creature to do something, I have my Player's Update method listen for input, and then sets the bools and states on its Creature as required.

Or if the Creature has its AI module enabled, its AI behaviour is processed during FixedUpdate, which again sets bools and states as required. AI behaviour is only handled by the host; it merely sets the values and sends the values to the other clients to interpret.

When it comes to juggling a Creature's movement state, facing east state, start-attacking state, shield state, start-jumping state, position, and velocity... creating methods for each of these things sounded like a lot of work.

Instead, I devised a simple SendDataToOthers method that gathers up the Creature's info, creates a DataNode, and sends the DataNode along. SendDataToOthers has no parameters. And while it's assembling the DataNode to create, if the previous DataNode it sent along contains the same state for something, it doesn't get included. For usage, I just set the bools and states and position as required before calling SendDataToOthers(), and then it does its thing. By not having any parameters, it feels very clean and simple to call and transmit what's needed.

Method simplicity

The real beauty with this simple state manipulation is that I don't have to have to change code for both my Player and AI classes when it comes to making the Creature do something new.

Each Creature has a ProcessUpdate method. Players set bools and states when needed, and then calls its Creature's ProcessUpdate. AI does the same.

ProcessUpdate checks each state and reacts accordingly.

Efficiency

Is this the most efficient way of doing things? Maybe, or maybe not. But it works for me. Perhaps the actions for wanting to jump or start attacking could be put into their own remote function calls without using a DataNode, but the message has to be transmitted one way or another regardless.

Network bandwidth is minimal when there's not too much happening, but it currently shoots up between 20-30 KB/s during really extreme situations with lots of Creatures doing lots of things.

In comparison, Counter-Strike: Global Offensive uses roughly 40 KB/s for about 12 players, and this is the highest I've seen a game use, so I'm using 40 KB/s as my peak limit to try and ensure I stay under.

Instant feedback vs delayed feedback

First-person shooters and third-person action games tend to let the client move around immediately. The client then sends its movement commands to the server, and the server sends the commands to everyone else. This allows the local player to move instantly on their screen, which feels nice and responsive.

In contrast, with real-time strategy games and most online fighting/platforming games, movement is delayed, as you're forced wait for the server to receive your command to move, and then you move when the command is sent back to you.

For a platformer, if you're playing over the Internet with a non-instant ping, this means you're constantly having to fight the lag. It never feels smooth because there's always a delay.

So what I'm trying with Tallowmere 2 is to have your local movement be instant, giving the client the benefit of the doubt.

When it comes to attacking and using items though, I have two options (using an axe as an example):

  1. Let the player swing the axe, but wait for the server to say if you hit anything or not; you'd swing instantly but actually hitting a mob and dealing damage would be delayed.

  2. Have the player send the command to swing but be forced to wait for the server to give you the response to swing; this results in delayed-feeling action, but the impact is instant when it happens.

But what if you had some grenades equipped instead? Using option 1, would your hands go through the animation of throwing, but you wouldn't actually throw a grenade until the server told you to instantiate a grenade and throw it? Or would you use option 2, where you'd only carry out the throwing animation once the server said so? I'm leaning towards the latter.

And what about raising your shield? Should the client do this instantly and have the final say if you've gotten hit or not? Or should it be delayed and let the server figure it out? I will need to experiment.

Ultimately, I will side with whatever feels most fun, responsive, and practical. You can never have perfect 1:1 syncing across the Internet, so it's a matter of getting it good enough. Still, the act of playing long-distance in real-time with other players is part of the magic with online multiplayer, so I'm sure it will feel fun in any case!

‒ Chris