đź““

NetworkEntityManager

public class SnapShot
{
    public SnapShot(int gameTick, byte[] packetData)
    {
        this.gameTick = gameTick;
        this.packetData = packetData;
    }

    public int gameTick;
    public byte[] packetData;
}

public class NetworkEntityManager : MonoBehaviour
{
    public static NetworkEntityManager networkEntityManager;


    public NetworkEntityPrefabs networkEntityPrefabs;

    //list of all gameobjects the network takes care of
    public Dictionary<int, NetworkEntity> networkEntities = new Dictionary<int, NetworkEntity>();

    public int packetGameTick = 0;
    public bool setFirstGameTime = false;
    public int gameTick = 0; //adds up in deltaTimes
    public int previousGameTick = 0; //last estimated gameTime client was running

    


    //we'll work out the lerp timer as te average of recent latency updates
    public List<float> mostRecentLatencies = new List<float>();
    public int maxRecentLatencies = 10;

    //list of snapshots for this game
    public List<SnapShot> snapshots = new List<SnapShot>();
    public bool isMaxNumSnapShots = true;
    public int maxNumSnapShots = 400; //I dunno, just work it out later for how many seconds of backlog you want
   //if client, then receive snapshots from server
    public SnapShot mostRecentlyReceivedSnapShot = null;


    //servers with ghostmanagers can send data about the world relevant to the players
    public GhostManager ghostManager;

    //if packets come in with a lower number, ditch em
    public int mostRecentSyncGameTick = 0;
    //for working out rollback
    public int previousSyncGameTick = 0;
    //keep track of inputs the server has done
    public int serverMostRecentPlayerInputID = 0;
    public int previousServerPlayerInputID = -1;//TODO use?

    public int currentNegativeEntityID = -1;
    public int maxNegativeEntityID = -100;

   public bool letsDoRollback = false;

    public static int numSyncPackets = 0;
//...

This class is the basically where the game loop lives during multiplayer NetworkEntity gameplay. I apologise for the size of it, it should be broken down into smaller chunks but here it is.

The main jobs of this class are as follows:

Plus other helpful functions to help out on various other smaller tasks. As there are a lot of methods in this class I will highlight the ones I feel are more important in orange.

What about sections in your game where there are no NetworkEntities? Like a lobby or something? Well you wont need one then in those scenes. You only need this class attached to a Unity game object somewhere in scenes where you want NetworkEntities to be doing stuff (players running around, monsters doing stuff, etc)

Snapshot Class

public class SnapShot
{
    public SnapShot(int gameTick, byte[] packetData)
    {
        this.gameTick = gameTick;
        this.packetData = packetData;
    }

    public int gameTick;
    public byte[] packetData;
}

This should have been pulled out into its own file, but since its here now I’ll write about it here. A SnapShot holds 2 important bits of information, a gameTick and packetData.

GameTick is basically the frame number this SnapShot was generated on. Every frame the game is calculated a gameTick variable is incremented, which is important for syncing things to the client.

PacketData is all of the compressed data about NetworkEntities from this frame. If there are a lot of NetworkEntities in the gameworld, then the packetData will be filled data from the closest NetworkEntities to the client we intend to send this to.

Main Attributes

There is a lot, so I will explain the main ones only here:

public static NetworkEntityManager networkEntityManager Is a static reference to itself so you can access it anywhere throughout the project. This class exists on the server and client at the same time, but may have methods only relevant to one side or the other, so be mindful how you use access to it.

public NetworkEntityPrefabs networkEntityPrefabs reference to the prefabs for your game. The NetworkEntityManager can only build and replicate NetworkEntities it knows definitions from this container of prefabs.

public Dictionary<int, NetworkEntity> networkEntities is the container of all known NetworkEntities in the game. This will be different for client and server. The server runs a full world simulation, so its container holds reference to all NetworkEntities in the game, whereas client only holds reference to those the server tells it about. Stored is using Dictionary for the container type, with entityID(int) being the lookup key to find entities quickly within the set if needed.

public int gameTick this variable counts every game frame thats been processed and used to ID packets to give an idea of when a packet of data was created during the games lifespan.

public List<SnapShot> snapshots on the server each time a game frame has been processed, its information gets saved to a SnapShot along with the frames GameTick and then stored in snapShots. Everytime a new SnapShot is added, the server will send it to clients. Its not feasible to store all SnapShots from a game as this will needlessly take up ram, so the variables around it help decide how many to keep around at max.

public GhostManager ghostManager I wrote the code for this project to make using a ghostManager optional, but HIGHLY RECOMMENDED. When processing what data to send to clients a GhostManager will only grab NetworkEntity data closest to the player and keeps filling the packet with data until filled and no more. Without the ghostManager, all NetworkEntity data is sent to clients all the time, which is ok for games with minimal NetworkEntities, but if you have a lot and exceeds the packet size, it will be fragmented and this may lead to more packet drops.

The rest of the attributes help manage the above mentioned attributes.Methods

private void Start()

This is a Unity function, so when the Game Object hits its start phase, this function sets static NetworkEntityManager networkEntityManager to equal this(itself). Its a bit of lazy singleton work, but it also lets the other network systems in place know it exists so they can route NetworkEntity packet data to it when it comes in

private void Update()

Another Unity function. We’re just using it to call UpdateRecentLatenciesList . Do not add game logic into here.

private void FixedUpdate()

Another Unity function, but one we’re going to rely on. This function calls our GameLoop method which is what pushes our game simulation forward 1 step at a time. We need roughly fixed steps between game frames and the best we have with Unity is FixedUpdate. Its mostly good, though apparently not always accurate but seems good enough from what I’ve tested.

public void GameLoop()

Each time this method runs, we update our entire game world 1 frame. Looking through you can get the idea of what it does because it mostly calls other methods. UpdateFrame basically physics, inputs, animation first. Then onto doing rollback for clients if we need to re-predict their client side prediction as new server data comes in. LerpClients helps smoothly move other entities in the world to their new server positions for clients viewing them. If we’re on the server a snapshot is saved now, then finally if any entities marked themselves for deletion for end of frame, this is where that is done.

public void UpdateFrame()

Every frame follows these exact steps:

  1. ProcessInputs - reads inputs from user and actions them on the PlayerNetworkEntity that they own
  1. ProcessRemoteInputs - if we’re the server and have inputs sent to us from clients, then we process those inputs on the PlayerNetworkEntities that they belong to
  1. UpdateEntities - Up to each entity, but this could be changing state, calculate movement via physics, updating their animation, using AI to determine something or whatever else a NetworkEntity needs to do in this frame
  1. UpdateHits - If your game has hitboxes and damage boxes, this step is where we check if any NetworkEntity is hitting any other NetworkEntity. If your game doesn’t have any kind of use for this function, you can delete this step to save some frame processing time.

public void SetEntitiesForDeletionIfNotUpdatedByServer()

This is a sweeping function used on the client side that marks all known NetworkEntities it can as deleteIfNotUpdatedByServer. That way if we receive a packet from the server and DO NOT get some info on some NetworkEntities we did know about, then those will be deleted to save us from the awkward experience of seeing that NetworkEntity standing around doing nothing until updated from the server.

NOTE: I had to make a choice on how to deal with this and this is what I decided on, for better or for worse. In the book I had read, it said send packets of all of the most recent NetworkEntity entityIDs who were deleted on the server, but thinking about the types of games I like to make, that would create a lot of extra packets (lots of explosions and bullets etc that have short lifespans). But I also opted to use a GhostManager to only send closest data to a player and only fill a Packet per frame, not go over. So from testing if your game has lots of players, monsters, bullets, explosions all on screen at once, then you will see them pop in and out of existence for the client based on distance which can look dumb but it does make sure the game doesn’t break across the network. In a game where you have lots of NetworkEntities but they are spaced out well, then the clients wont notice the ones being destroyed as they’ll likely be off screen. If that doesn’t work for you game, maybe remove this approach to the logic in the game and apply your own.

public void DeleteEntitiesEndOfFrame()

We all gotta go sometime. Maybe a monster died, or an explosion is finishing up or an item is picked up, either way we need to delete NetworkEntities from time to time and they are killed off in this method.

private bool ShouldDeleteEntityEndOfFrame(KeyValuePair<int, NetworkEntity> g)

A method that gives back true or false on whether we should delete this NetworkEntity passed to it

public void BackupEntityPhysicData()

Backing up physics data usually helpful on client side before reading server packet, that way we can lerp or compare between on and new values

public void LerpClients()

Client side method, calls all NetworkEntities DoLerp() method. NOTE: the smoother lerp methods are called within each NetworkEntities Unity Update function, so this DoLerp may not be used by all of your entities

public void UpdateUnityPositions()

All of the math and physics in this project happens in a way that doesn’t impact on the actual Unity Transforms, but we also would like to see the game world update visually so this method moves all NetworkEntities to any new calculated locations

public void DoRollback()

This one is a doozy and kind of crucial. When a client is pressing buttons we’re “predicting” what’s going to happen by actioning those user inputs immediately on their PlayerNetworkEntity and storing those inputs against the GameTick they happened. The server has authority, so when a packet comes in we need to use that data instead of our own prediction.

HOWEVER that data is technically in the past as the packet took some time to get to our client machine. So, we “roll back” the world to the point in time the packet was generated for using all of its data, then roll forwards multiple frames in an instance replaying any user inputs we recorded on our clients end the server had not processed yet so that our client side prediction still feels good.

This is the best way to maintain accurate client side prediction and why I also opted for full physics determinism. If I didn’t go for determinism, then I would have to deal with client side prediction physics jitter

public void UpdateEntities()

Gets all NetworkEntities to call their UpdateEntity method

public void UpdateHits()

This method deals with checking if NetworkEntities are hitting other NetworkEntities e.g player hits a monster with a sword, or a bullet hits a player etc. At the moment it can only handle rectangular or circular hit boxes. From memory, at the moment if 2 things should be hitting each other, its only really the first one who gets checked will deal the hit. If you want to control this section further, I recommend making changes here.

private static void ProcessRemoteInputs()

Server sends full packets of data about the game world to the clients, but the clients are only allowed to send their inputs to the server for their characters. This prevents all sorts of dumb cheating (some games let the player send their own XYZ data etc, imagine how much you could cheat with that). SO, this method is on the server side and processes those inputs sent in from clients and processes them on the PlayerNetworkEntities they relate to.

public void ProcessInputs()

Ran on both server and client (because servers can be players too in this design), checks to see if there is a PlayerInputManager and if so, get it to action any user inputs on the PlayerNetworkEntity they control

private void ProcessReceivedSnapShots()

Client side method for dealing with a received SnapShot from the server. It will pass the SnapShot data over to ReadWorldDataPacket which will read the packet and feed the data to the correct NetworkEntities

private void UpdateRecentLatenciesList()

Collects latencies of attached connections to a server so we can see the average latency for each client

public SnapShot SaveWorldSnapShot()

Create a new SnapShot with current GameTick and then write all NetworkEntity data to it.

public void ReadWorldDataPacket(NetDataReader reader)

Read SnapShot and get NetworkEntities to read the parts they need from this snapshot. If a NetworkEntity is referenced in the snapshot, but does not exist in the world, then one of that type is built quickly before getting it to read the next that part.

public void LoadWorldData(NetDataReader reader, bool destroyTempEntities = true)

Currently not in use, but technically you can use the Snapshot read and write data to save and load an entire world of NetworkEntities. I think this is here because I was going to work out lag compensation but didn’t get around to it.

public GameObject BuildNetworkEntity(int typeID, bool addToList = false)

Builds a NetworkEntity based on type ID. Optional if you add it to the NetworkManager’s networkEntities list.

public GameObject BuildNetworkEntity(NetworkEntityType type, bool addToList = false)

Some as above, but uses the NetworkEntityType enum instead of typeID

public void RegisterNetworkEntity(NetworkEntity networkEntity)

Unregistered entities can call this to be officially registered, good use case is for entities put into the scene directly from editor

public GameObject BuildNetworkEntityWithID(int typeID, int id, bool addToList = false)

Similar to GameObject BuildNetworkEntity(int typeID, bool addToList = false) but also sets the generated NetworkEntities entityID too

public void AddNetworkEntity(NetworkEntity n)

Adds a NetworkEntity to the NetworkEntityManager’s networkEntities list

public GameObject BuildClientLocalNetworkEntityTypeID(int typeID)

This is for building entities on the client side that use client side prediction instead. Use these sparingly. The example project uses this when a player shoots, the client builds a bullet locally with this method and is used mostly as an an estimation as to what the bullet is really doing. The server generates it on its end the normal way and other clients will see the bullet lerped similar to other things clients may see in the world

private int FindUnusedNegativeNetworkEntityIDInDictionary()

When creating NetworkEntities on the client side, they wont be built with a valid positive entityID, so for management sake they are given negative entityID’s. This method tries to get the next negative ID to use for any new networkEntities you are building locally as they will still need a unique ID to fit into the networkEntities dictionary. When getting the next negative ID, if an entry for it already exists, we destroy that game object so the new one can take its place. This is necessary in the event that our negative IDs exceed their minimum value and loop back around to -1.

public NetworkEntity GetEntityByID(int id)

Find a NetworkEntity in the networkEntities dictionary by its entityID, else return null

public NetworkEntity GetEntityByPlayerID(int id)

Find a NetworkEntity that is of type PlayerNetworkEntity and playerID matches id, else return null

public PlayerNetworkEntity GetPlayerNetworkEntityByPlayerID(int id)

Find a PlayerNetworkEntity and playerID matches id, else return null. Actually does the exact same thing as the above one, just returns it as a PlayerNetworkEntity rather then NetworkEntity

public void KillAllPlayers()

I added this one for fun more then anything. Reduces all NetworkEntitites of type PlayerNetworkEntity to 0 hp

public void DestroyDisconnectedPlayerEntity(int playerID)

On the server side, when a client disconnects this method will remove their PlayerNetworkEntity from the game and from the networkEntities dictionary

public List<NetworkEntity> GetAllNetworkEntitiesWhoAreNotMe(NetworkEntity me)

Example is currently not using this anymore, but a handy method to keep around. You pass it a NetworkEntity and it will make a list of all the NetworkEntities who do not match the one passed in

public List<NetworkEntity> GetAllSolidNetworkEntitiesWhoAreNotMe(PhysicsComponent me)

Also not used in the example, but may be useful. You pass it a PhysicsComponent belonging to a NetworkEntity, and it will get you a list of all other NetworkEntities that are set as solid

public List<SRect> GetAllSolidNetworkEntityRectsWhoAreNotMe(PhysicsComponent me)

This one is in use in the physics calculations. You pass it a PhysicsComponent belonging to a NetworkEntity, and it will get you a list of rectangles(SRect) representing the collision boxes of all other NetworkEntities

public static bool IsRollbacking()

Returns true if the game is currently doing a rollback else false. Useful for situations where we need to know whether to run certain logic or not. For example updating timers or animations should only happen when the game is doing a full update for some NetworkEntities and not change things during a rollback