Search Unity

Showcase Creating an Isometric 2D RPG/MMO (Ongoing Series)

Discussion in 'Multiplayer' started by qbvbsite, Nov 2, 2020.

  1. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    After working on making an Isometric 2D RPG/MMO over the past 2-3 weeks and reading hundreds of forum posts, blogs, websites, and tutorials. I figured I would document my quest here with the hope to help jump-start other's projects. Let's start off that this will not be a tutorial series giving you step by step instructions on how to create an RPG/MMO but more providing you the information you will need to get it done. A few reasons for this are that every project is different and I personally find it's more beneficial for people to code something them selfs then just downloading a package and tweaking it to their needs.

    So with that said what will this post be about? Well as I go through the motions of building my RPG/MMO I'll include any resources I find helpful, game architecture, game mechanics, and maybe some snippets of code here and there. I would also like to mention I'm not a pro game developer by any means and relatively new to Unity. What I do have is 16+ years of experience as a computer programmer and have been coding C# for 6-7 years.

    The Start
    Before I started my current RPG/MMO journey I had attempted this before but 3D 4-5 years ago and did a lot of reading about networked games and more specifically Authoritative Servers. I even followed an older tutorial by Christian Richards (https://www.youtube.com/channel/UCUy5QMxQDVlYMSM4Zt483BA) which laid out a framework for an authoritative server outside of Unity (server running as a console application). Personally, I found it pretty good, and gave me a pretty good understanding of how things worked/are laid out. Now it wasn't perfect and there were a few design decisions I didn't like but overall got me on the right track.

    Now you might be wondering what is an authoritative server and it pretty much boils down to the server have authority over the player's movements/actions. This is a must in my opinion for an MMO type game to help limit cheating. At the start of your project, you're going to want to figure out which direction you would like to go and you have 2 main options: Build your server component in Unity or build a standalone console application for your server. For my project, I choose to go for a console server to remove the overhead of Unity, ease of testing (can build mini-console applications to test functionality), and figured it would be easier/more flexibility to run different instances of regions. If you were to use Unity you would want to run it in headless mode and probably use a HLAPI for networking like Mirror (https://mirror-networking.com/). Personally, I haven't used it but seems to be widely used by the community and is a fork of Unity's UNET.

    Now that I have chosen to create the server entirely separate from Unity I laid out the following design elements for my server architecture:
    • Make it flexible and extendable - Abstract out things like physics libraries, network libraries, databases, etc. So that any component can be easily replaced without touching the core code.
    • follow SOILD principals and keep my classes small and specific
    • Focus on clean code and functionally rather than fret about optimization early on. This keeps the project moving forward and if the code is clean and classes small optimization should be easy to apply without recoding the whole pieces of the server.
    I think that's all for now, next post will be about how I came about choosing my network library, how I handled server-side 2D physics and smooth client-side movement. Here are a few nice series of articles/resource that helped me along the way with networking, client-side movement, and various other odds/ends:

    https://www.gabrielgambetta.com/client-server-game-architecture.html
    https://forum.unity.com/threads/une...nt-side-prediction-and-reconciliation.349929/
    https://developer.valvesoftware.com...rver_In-game_Protocol_Design_and_Optimization
    https://gafferongames.com/post/deterministic_lockstep/
    https://github.com/GenaSG/UnityUnetMovement/blob/master/Scripts/NetworkMovement.cs
     
    Last edited: Nov 2, 2020
  2. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Starting The Server Architecture
    The first thing I did when designing the server architecture was started by making a set of interfaces that would make up the network portion of the server. This consisted of 4 interfaces:
    • IServerNetworkManager - Used to listening for incoming requests and handling connected peers
    • IServerNetworkSender - Used to send packets
    • IServerNetworkReciever - Used to manage received packets
    • IPacket - Common interface to pack/unpack our data
    You may wonder why even bother creating interfaces and not just create classing instead. the reason because is that instead of using the concrete classes everywhere in my code I use their interface equivalent. This decouples my code so at any point I can create a whole new set of classes that implement the same interfaces and my core code that uses them doesn't need to change. If one day I decided to switch out my Network Library I could easily do so by making 3 new classes that implement IServerNetworkManager, IServerNetworkSender, and IServerNetworkReciever using the new library. Here is what I landed on for each interface:

    IServerNetworkManager

    Code (CSharp):
    1.     public delegate void ServerStarted();
    2.     public delegate void ServerStopped();
    3.     public delegate void ReceivedPacket(int peerId, IPacket packet);
    4.     public delegate void NetworkError(IPEndPoint endpoint, SocketError socketError);
    5.     public delegate void PeerConnected(int peerId);
    6.     public delegate bool PeerConnectionRequest(IPEndPoint endPoint, string key);
    7.     public delegate void PeerDisconnected(int peerId);
    8.  
    9.     public interface IServerNetworkManager
    10.     {
    11.         void StartServer(int port);
    12.         void PollEvents();
    13.         void StopServer();
    14.  
    15.         event ServerStarted ServerStarted;
    16.         event ServerStopped ServerStopped;
    17.         event ReceivedPacket ReceivedPacket;
    18.         event NetworkError NetworkError;
    19.         event PeerConnected PeerConnected;
    20.         event PeerConnectionRequest PeerConnectionRequest;
    21.         event PeerDisconnected PeerDisconnected;
    22.     }
    IServerNetworkSender
    Code (CSharp):
    1.     public interface IServerNetworkSender
    2.     {
    3.         void SendPacket(int peerId, IPacket packet, PacketDeliveryMethod packetDeliveryMethod = PacketDeliveryMethod.Unreliable);
    4.         void SendBatchPacket(int[] peerId, IPacket packet, PacketDeliveryMethod packetDeliveryMethod = PacketDeliveryMethod.Unreliable);
    5.     }
    IServerNetworkReciever
    Code (CSharp):
    1.     public interface IServerNetworkReceiver
    2.     {
    3.         void HandlePacket(int peerId, IPacket packet);
    4.     }
    IPacket

    Code (CSharp):
    1.     public interface IPacket
    2.     {
    3.         byte OpCode { get; set; }
    4.  
    5.         int SubCode { get; set; }
    6.  
    7.         byte[] PacketData { get; set; }
    8.     }
    Think these are pretty much self-explanatory, in the IServerNetworkManager I do all the heavy lifting with starting/stopping listening for requests and implement events for all major actions. In IPacket is pretty light with having OpCode/SubCode which can be used for packet routing/actions and PacketData which is the data you are sending. The idea because packet data is the packet you create would be responsible for packing/unpacking its own data.

    Finding A Network Library
    Early on with this new project, I knew I wanted to say away from any third-party services like PUN. I think these are great for someone looking to quickly do a P2P setup but doing a full authoritative server would just add unneeded latency adding PUN as a middle man. After looking at a few candidates such as DarkRift 2, LiteNetLib, and Lidgren I choose to use LiteNetLib. The reasons for this choice was because it has a good track record with the community, updated frequently, and had a good set of features such as
    • Lightweight
    • Packet loss and latency simulation
    • Easy to use
    • Many supported platforms such as:
      • Windows/Mac/Linux (.NET Framework, Mono, .NET Core)
      • Android (Unity)
      • iOS (Unity)
      • UWP Windows 10 including phones
      • Lumin OS (Magic Leap)
    With my interfaces created and network library chosen I went ahead and created the implementing classes with LiteNetLib and by the end of the night, I had a functioning server. For the client-side, I created similar interfaces IClientNetworkManager, IClientNetworkSender, and reused IPacket. These interfaces will be used on the Unity side of things to handle connecting to the server.

    IClientNetworkManager - Handle connecting/disconnecting from the server
    Code (CSharp):
    1.     public delegate void ReceivedServerPacket(int serverId, IPacket packet);
    2.     public delegate void NetworkServerError(IPEndPoint endpoint, SocketError socketError);
    3.     public delegate void ServerConnected();
    4.     public delegate void ServerDisconnected();
    5.  
    6.     public interface IClientNetworkManager : IClientNetworkSender
    7.     {
    8.         void Connect(string address, int port, string key);
    9.         void PollEvents();
    10.         bool IsConnected();
    11.         void Disconnect();
    12.  
    13.         event ReceivedServerPacket ReceivedServerPacket;
    14.         event NetworkServerError NetworkServerError;
    15.     }
    IClientNetworkSender - Used to send packets to the server
    Code (CSharp):
    1.     public interface IClientNetworkSender
    2.     {
    3.         void SendPacket(IPacket packet, PacketDeliveryMethod packetDeliveryMethod = PacketDeliveryMethod.Unreliable);
    4.     }
    I think that's pretty good for this post and hope you found it useful. Next post I give a little more detail about Unity interacts with the server and the plans for player movement on the server.

    Links to other networking solutions/libraries:
    https://www.darkriftnetworking.com/darkrift2
    https://github.com/RevenantX/LiteNetLib
    https://mirror-networking.com/
    https://www.photonengine.com/

    --James
     
    Last edited: Nov 4, 2020
  3. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Sending Information From Unity to the Server
    Now that we have your networking architecture laid out how do we go about sending data back and forth from the client/server. As you may have guessed this will be done by sending the data store in your packet implementation of IPacket. Here is a sample packet that is built upon BasePacket (which implements IPacket) for when a Player joins the gaming world:

    BasePacket
    Code (CSharp):
    1.     public class BasePacket : IPacket
    2.     {
    3.         public BasePacket(byte opCode, int subCode, byte[] packetData)
    4.         {
    5.             OpCode = opCode;
    6.             SubCode = subCode;
    7.             PacketData = packetData;
    8.         }
    9.  
    10.         public byte OpCode { get; set; }
    11.         public int SubCode { get; set; }
    12.  
    13.         [SerializedData]
    14.         public byte[] PacketData { get; set; }
    15.     }
    PlayerJoinPacket
    Code (CSharp):
    1.     public class PlayerJoinPacket : BasePacket
    2.     {
    3.         public PlayerJoinPacket(IPacket basePacket)
    4.             : base(basePacket.OpCode, basePacket.SubCode, basePacket.PacketData)
    5.         {
    6.         }
    7.  
    8.         public PlayerJoinPacket(string username)
    9.             : base((byte) PacketOpCode.Region, (int) PacketSubCode.PlayerJoin)
    10.         {
    11.             Username = username;
    12.         }
    13.  
    14.         [Serialize]
    15.         public string Username { get; set; }
    16.     }
    A few things to note about the packet, one is the [Serialize] [SerializedData] tags which I use to dynamically pack/unpack the packet in the network layer. This is done by a packet serializer I wrote that uses reflection/generics to look for these tags and dynamically serialize/unserialize the packets. The other is you can see I set the OpCode/SubCode to PacketOpCode.Region/PacketSubCode.PlayerJoin. These are used to help direct the packet on both the client/server. I use the OpCode to tell the system where it is going (in this instance the Region server) and the SubCode the action I wish to perform. Here are some example classes of OpCode/SubCodes.

    PacketOpCode
    Code (CSharp):
    1.     public enum PacketOpCode : byte
    2.     {
    3.         Client = 0x01,
    4.         Chat = 0x02,
    5.         Login = 0x04,
    6.         Region = 0x08
    7.     }
    PacketSubCode
    Code (CSharp):
    1.     public enum PacketSubCode : int
    2.     {
    3.         //Client Sub Codes
    4.         PlayerJoin,
    5.         PlayerLeave,
    6.         PlayerMove,
    7.  
    8.         //Login Sub Codes
    9.  
    10.         //Chat Sub Codes
    11.  
    12.         //Region Sub Codes
    13.         PlayerJoined,
    14.         PlayerMoved,
    15.         PlayerForget,
    16.         PlayerInput,
    17.         CharacterKnown,
    18.         CharacterMoved,
    19.         CharacterForget,
    20.         RemoveGameObject,
    21.         GameObjectKnown,
    22.  
    23.         //Other Sub Codes
    24.         Error
    25.     }
    So the idea here is that you would register packet handlers are the client/server and when you receive a certain packet you would forward it to the appropriate handler. With the packet/packet handler written to join a player in the game world, we now have to plan out how you are going to handle player physics, collisions, and sending movement updates to other players.

    Hope you have enjoyed the third entry in this series and If you have any questions feel free to ask.

    --James
     
    ali12000, ModLunar and Erveon like this.
  4. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Planning Out The Server Details
    With our network infrastructure in place and the client/server now talking to each other it is time to start making some architecture decisions regarding physics, collisions, and sending updates to players. With game physics and collisions in mind, I did make a game decision early on that players/NPCs would not impede each other's movement, meaning that can walk through each other. This was done for a few reasons, one being I didn't want players to block other players from areas in the game world, and second, collisions become much harder to implement due to latency between players.

    A fairly common task for the server is querying for objects in the game world so it can perform tasks such as sending network updates, testing collisions, and what game objects are near a certain player. This can be down with spatial partitioning using spatial databases (https://en.wikipedia.org/wiki/Spatial_database#Spatial_index). These are used to store information related to objects in space such as our players, NPCs, and other game objects. There are many different types of spatial databases each having their strengths and weaknesses. After much research, I landed on 2 different ones: QuadTree (https://en.wikipedia.org/wiki/Quadtree) and Grid (https://en.wikipedia.org/wiki/Grid_(spatial_index)).

    The reason for choosing 2 is so that I can play to each's strength and handling both moving objects (players/NPCs) and static objects (that players can't walk through) separately. I decided to choose Quadtree because I have used it before in another project and there are plenty of C# implementations of it. Even though the Quadtree is slower at additions it's quite speedy at executing queries which is perfect for static gameworld objects since they will not move and change. That now leaves us with our dynamic objects such as players/NPCs and for these objects, I choose to do a Grid implementation. Grid is faster and removing/adding objects than Quadtree and is also pretty simple to query. The overall idea is that any objects that can impede the player's movement would be placed in the quadtree and other objects that are constantly moving are stored in the grid. Keeping with my design pattern I created the following Interfaces to give me flexibility down the line if I chose to change out any of these 2 implementations:

    IGameGrid - Used for Dynamic Objects like Players/NPC's/Resources
    Code (CSharp):
    1.     public interface IGameGrid<T>
    2.     {
    3.         void Add(T obj);
    4.         void Remove(T obj);
    5.         void Adjust(T obj, Position oldPosition);
    6.         T[] Retrieve(T obj);
    7.         T[] Retrieve(Position position, SizeF bounds);
    8.     }
    IWorldCollisionManager - Used for anything the player can collide with such as collision tiles
    Code (CSharp):
    1.     public interface IWorldCollisionManager
    2.     {
    3.         void LoadColliders(string colliderJson);
    4.         void LoadTileColliders(string tileColliderJson);
    5.         void Add(IColliderObject colliderObject);
    6.         void Remove(IColliderObject colliderObject);
    7.         IColliderObject[] GetNearestObjects(IColliderObject colliderObject);
    8.         ICollisionResult[] CheckForCollisions(IColliderObject colliderObject, IColliderObject[] possibleCollisionObjects);
    9.         bool RayForCollisions(Vector2 start, Vector2 end, IColliderObject[] possibleCollisionObjects);
    10.     }
    These are pretty self-explanatory which each allowing you to add/remove the objects as well as query for objects near another object. You may of noticed a few each different functions in the IWorldCollisionManager which are responsible for Loading Colliders from a JSON string (exported from Unity, we will get to that later) and CheckForCollisions which will let you know if an object collides with any objects in an array.

    As a final discussion point for this post, how do we determine if one object is colliding with another object?. The Grid/Quadtree will give us a list of objects near/in a region of space but doesn't necessarily mean 2 objects are actually colliding. After some research on different algorithms, I ended up making a few design decisions and went with SAT or Separating Axis Theorem (http://www.dyn4j.org/2010/01/sat/#sat-inter). This algorithm allows you to quickly determine if 2 convex polygons overlap with each other. With that being said this is something we have to be aware of when creating colliders as they can not be concave. To save a little time I also found a C# port of Differ (https://github.com/bfollington/differ-cs) which saves me some time in having to roll my own. This implementation also handles 2 colliding spheres as well as a sphere with a polygon.

    I think that's it for this post, next post I'll focus on how we are going to handle player movement both on the server/client.

    Useful links (some included in the post already):
    https://en.wikipedia.org/wiki/Spatial_database#Spatial_index
    https://en.wikipedia.org/wiki/Quadtree
    https://en.wikipedia.org/wiki/Grid_(spatial_index)
    https://www.habrador.com/tutorials/programming-patterns/19-spatial-partition-pattern/
    http://www.dyn4j.org/2010/01/sat/#sat-inter
    https://badai.xyz/2019-10-07-sat-collision-detection-polygon-and-circle/
    https://github.com/pothonprogrammin...tent/bouncing-polygons/bouncing-polygons.html
    https://pothonprogramming.github.io/content/bouncing-polygons/bouncing-polygons.html
    https://github.com/bfollington/differ-cs
     
    Last edited: Nov 6, 2020
    ModLunar and Erveon like this.
  5. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Smooth Player Movement
    Now that we have ideas of how we are going to handle the players/collisions server-side let's work on getting some movement going. This probably took the most time researching/tweaking to achieve smooth player movement with no delay/rubberbanding/popping. To do this I employed client-side prediction which allows the player to move before receiving confirmation from the server. I few goods article that got me headed on the right path is one from valve (https://developer.valvesoftware.com...rver_In-game_Protocol_Design_and_Optimization) and another from Gabriel Gambetta (https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html). These pretty much explain the idea of storing all the player inputs client-side stamped with a unique identifier (such as a timestamp/frame number) that is sent to the server. Once the server processes the request it sends back the player's location along with the input that was processed. When the client receives the result from the server it then goes through its list of stored inputs and replays all future inputs from the server result. If you had correctly simulated movement client-side the new simulated position should be exactly the same. If there is a difference then you have to make a choice to either slowly Lerp the difference (smoothly work it in as the player moves) or pop the character to the new simulated position. At first, I tried both approaches with the Lerping probably working the best of the 2 but I still wasn't satisfied with the occasional sliding motion when stopping. So instead of tried to bandaid a solution I spent the next few days perfecting my movement both client and server-side. Here are some of the solutions I came up with:
    • Have all client-side movement updates done in FixedUpdate (most know this). This allows you to easily make the same movement both client/server-side. At first, I was moving the player on the server based on Time.delta between updates which caused slight movement drift. Once I changed it to use the exact time FixedUpdate was set to movements became exactly correct.
    • In C# using DateTime.Now to generate a timestamp is not accurate as it's only updates about every 15ms which is no good if you using timestamps for your frames. Instead, if you running on a Windows machine you can use the lower-level function GetSystemTimePreciseAsFileTime (https://stackoverflow.com/questions/16032451/get-datetime-now-with-milliseconds-precision). For my implementation, I actually removed timestamps and used an input frame counter but it's still good to know when sleeping background threads on the server.
    • Always use MovePosition when working with a RidgedBody to make sure client-side physics is applied correctly.
    • Make sure collisions on both client/server-side work the same way. This was probably the hardest one to figure out since client-side colliders like to slide off of colliders. Instead of writing code on the server to duplicate this slide, I decided that if the player collided with an object it would not move from its current position. This seems to work well since the amount of movement a player can do in 16ms is minimal and it can easily be checked on the client/server.
    Once all these solutions were in place I was left with incredibly smooth player movement with no poping/rubberbanding insight. Here is a snippet of my reconciliation code that searches the inputs for the result input frame for the server then replays all future inputs. The code was highly derived from the following GitHub link: https://github.com/GenaSG/UnityUnetMovement/blob/master/Scripts/NetworkMovement.cs

    Code (CSharp):
    1.         //Player Move Update From Server
    2.         public void UpdatePlayerMovement(Vector3 position, float movementSpeed, long lastInputFrame, long serverTimestamp)
    3.         {
    4.             //Update Movement Speed
    5.             _movementSpeed = movementSpeed;
    6.  
    7.             //Store Server Position
    8.             var serverResult = new PositionResult()
    9.             {
    10.                 position = position,
    11.                 inputFrame = lastInputFrame
    12.             };
    13.  
    14.             //Discard If Older Then Last Processed Step
    15.             if (serverResult.inputFrame <= _lastProcessedInputFrame)
    16.                 return;
    17.  
    18.             //Store Last Input Frame
    19.             _lastProcessedInputFrame = serverResult.inputFrame;
    20.  
    21.             //Update Player Position Based On Server
    22.             _positionResult.position = serverResult.position;
    23.  
    24.             //Find All Inputs After Input Frtame
    25.             var foundInputIndex = -1;
    26.             for (var i = 0; i < _playerInputList.Count; i++)
    27.             {
    28.                 if (_playerInputList[i].inputFrame <= serverResult.inputFrame)
    29.                     continue;
    30.  
    31.                 //Found Future Inputs
    32.                 foundInputIndex = i;
    33.                 break;
    34.             }
    35.  
    36.             //Clear Input List If Input Frame Not Found
    37.             if (foundInputIndex == -1)
    38.             {
    39.                 //Clear Inputs list if no needed records found
    40.                 while (_playerInputList.Count != 0)
    41.                 {
    42.                     _playerInputList.RemoveAt(0);
    43.                 }
    44.  
    45.                 //No Inputs To Reprocess
    46.                 return;
    47.             }
    48.  
    49.             //Reply Future Inputs
    50.             for (var i = foundInputIndex; i < _playerInputList.Count; i++)
    51.             {
    52.                 //Execute Future Move
    53.                 _positionResult.position = InterpolatePosition(_playerInputList[i], _positionResult);
    54.             }
    55.  
    56.             //Remove All Inputs Before Processed Input Frame
    57.             var targetInputIndex = _playerInputList.Count - foundInputIndex;
    58.             while (_playerInputList.Count > targetInputIndex)
    59.             {
    60.                 _playerInputList.RemoveAt(0);
    61.             }
    62.         }
    Now, this all well and good on a perfect network but what happens which we start adding latency/dropped packets to the mix? As you probably have guessed it's not pretty as with the current architecture we are assuming the server is receiving each one of our packets successfully. In the next post, I'll go through how I solved this issue so we can still have smooth movement even with 40%+ packet drop and 300ms +of latency

    Hope you have enjoyed the series so far and if you have any questions feel free to ask them here or in PM.
    --James
     
    Last edited: Nov 10, 2020
    obonnate and Erveon like this.
  6. obonnate

    obonnate

    Joined:
    Jan 16, 2021
    Posts:
    1
    Very useful information, thanks for sharing it.
    The way you studied, discarded and selected among the different options is very enlightening.
    Do you plan to keep on posting on the subject ? I would love to read about the workaround you used to mitigate packet drops and network latency. Having a glance on how you wired thing together on the client side (Unity) would be very interesting too.
     
  7. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Hey,
    I plan on picking this back up shortly, life got in the way and took a little break :).
    --James
     
  8. ModLunar

    ModLunar

    Joined:
    Oct 16, 2016
    Posts:
    374
    Jesus, thank you for describing this, this is amazing!
    After all, my favorite games are multiplayer RPGs by far!
     
  9. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    No problem, I'll be adding to it shortly as I'm picking the project back up :)
     
    ModLunar likes this.
  10. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Handling Latency and Packet Loss
    Now that we have the client/server talking to each other and the players movement smooth its time to tackle our next issue Latency and Packet Loss. Running the current project locally runs flawlessly when opening two clients with movement of one player shown on the second players screen and vise versa. The problem here is your running in the idle scenario with no latency and no lost packets. Now lets dive into each issue and how you can solve/mitigate the them.

    Latency
    Lets say we used out current code with users from different house holds what would we see? Well we can expect 30-50ms of latency between the players which means the other players on your screen you would be 30-50ms in the past and not there current server location. This becomes an issue because you are doing actions based on where the other players are on your screen and not there current location on the server. So when you attack another player it's possible on the server they are not there and you would miss. This would mean the servers response of your missing is not what you are seeing on your screen. One solution to this is have the server store the past actions of all players with a timestamp and the latency of the players packet. The reason for this is that we can use this data to roll back events to see where the player was at a given point in time. Using the players latency and when we received an action of a player we can rewind all the near by players to the state that the actioning player is seeing on his screen. We then use these rewound positions to determine if the action hit any of the players or missed. Now this isn't a silver bullet and does cause another small issue with the player being hit possibly seeing that from his screen it's not a hit. So depending on your game and its mechanics you may want to apply this a little differently. Personally I rather have the actioning player showing the hit and do some UI tricks on the player being hit to show he was hit. With this change in our code we do add one small field to out IPacket so that the network layer can store the current ping when the packet is received.

    IPacket
    Code (CSharp):
    1.     public interface IPacket
    2.     {
    3.         byte OpCode { get; set; }
    4.  
    5.         int SubCode { get; set; }
    6.         int Ping { get; set; }
    7.  
    8.         [SerializedData]
    9.         byte[] PacketData { get; set; }
    10.     }

    Packet Loss
    Currently the way our code is written its assuming all packets that we sent are getting the server and are in order. As we know this is not true and running the game like this would cause the client/server to get out of sync and you would notice tons of banding/popping on the client-side when losses occur. This could maybe be mitigated with some Lerp code but this isn't the idle solution as you are likely not able to fully compensate and will still see issues of popping/banding. Now the solution to this is actually quite simple: we need the client to send the current input action as well as input actions from the past. So this means on the client we need to keep track of all our inputs (we already did this when we added reconciliation in the Smooth Movement post) and give them a unique identifier (I used a rolling counter to save bandwidth) when sending them to the server. With sending the server also past inputs if by chance there was a packet lost the server can still process all the inputs on order since each input is in multiple packets. Now that the server can handle loss packets how does the client know which past inputs to send? Well this is pretty simple, since we already trim the history during the position reconciliation we just send the full history every time. This guarantees the server we get all inputs we haven't received a response from. Again this will handle most situations fairly well and keep the game totally playable when packet loss happens but if users are having 25%+ packet loss you can expect to see popping/delayed movement of characters.

    PlayerInputPacket
    Code (CSharp):
    1.     public class PlayerInputPacket : BasePacket
    2.     {
    3.         public PlayerInputPacket(IPacket basePacket)
    4.             : base(basePacket.OpCode, basePacket.SubCode, basePacket.PacketData, basePacket.Ping)
    5.         {
    6.         }
    7.  
    8.         public PlayerInputPacket(long startInputFrame, int numberOfInputFrames, int[] moveDirections, int[] playerActions)
    9.             : base((byte)PacketOpCode.Region, (int)PacketSubCode.PlayerInput)
    10.         {
    11.             StartInputFrame = startInputFrame;
    12.             NumberOfInputFrames = numberOfInputFrames;
    13.             MoveDirections = moveDirections;
    14.             PlayerActions = playerActions;
    15.             ClientInputTimestamp = Timestamp.GetTimestamp();
    16.         }
    17.  
    18.         //Input Frame of First Item
    19.         [Serialize]
    20.         public long StartInputFrame { get; set; }
    21.  
    22.         [Serialize]
    23.         public int NumberOfInputFrames { get; set; }
    24.  
    25.         //Move Direction
    26.         [Serialize]
    27.         public int[] MoveDirections { get; set; }
    28.  
    29.         //Action (Melee, Spell, Etc)
    30.         [Serialize]
    31.         public int[] PlayerActions { get; set; }
    32.  
    33.         //Timestamp of Packet
    34.         [Serialize]
    35.         public long ClientInputTimestamp { get; set; }
    PlayerMovedPacket
    Code (CSharp):
    1.     public class PlayerMovedPacket : BasePacket
    2.     {
    3.         public PlayerMovedPacket(IPacket basePacket)
    4.             : base(basePacket.OpCode, basePacket.SubCode, basePacket.PacketData)
    5.         {
    6.         }
    7.  
    8.         public PlayerMovedPacket(Guid objectId, float x, float y, float z, Direction facingDirection, Direction moveDirection, float moveSpeed, long lastReceivedInputTimestamp)
    9.             : base((byte)PacketOpCode.Client, (int)PacketSubCode.PlayerMoved)
    10.         {
    11.             ObjectId = objectId;
    12.             PositionX = x;
    13.             PositionY = y;
    14.             PositionZ = z;
    15.             FacingDirection = facingDirection;
    16.             MoveDirection = moveDirection;
    17.             MoveSpeed = moveSpeed;
    18.             LastReceivedInputTimestamp = lastReceivedInputTimestamp;
    19.             ServerTimestamp = Timestamp.GetTimestamp();
    20.         }
    21.  
    22.         [Serialize]
    23.         public Guid ObjectId { get; set; }
    24.  
    25.         [Serialize]
    26.         public float PositionX { get; set; }
    27.  
    28.         [Serialize]
    29.         public float PositionY { get; set; }
    30.  
    31.         [Serialize]
    32.         public float PositionZ { get; set; }
    33.  
    34.         [Serialize]
    35.         public Direction FacingDirection { get; set; }
    36.  
    37.         [Serialize]
    38.         public Direction MoveDirection { get; set; }
    39.  
    40.         [Serialize]
    41.         public float MoveSpeed { get; set; }
    42.  
    43.         [Serialize]
    44.         public long LastReceivedInputTimestamp { get; set; }
    45.  
    46.         [Serialize]
    47.         public long ServerTimestamp { get; set; }
    48.     }
    By implementing these changes into my code and using the latency/packet loss simulation included with LiteNetLib it ran extremely smooth with 50-200ms latency and 10% packet loss. The only noticeable difference was the character on the other screen was a little delayed. Even increasing the latency to 300-500ms and 40% packet loss it was still somewhat playable with a bigger delay and the occasional freeze/pop which I think was quite impressive.

    Sorry about the delay on this write-up but will try to add to this every other week as the project progresses.

    Thanks for reading,

    --James

    Resources
    https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
     
    Last edited: Sep 10, 2021
    ModLunar likes this.
  11. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Smoothing Other Players Movements
    Now that we have our player moving smoothly and are able to handle latency/packet loss what's next? Well testing my code with multiple players I did notice something: well my player movement was smooth the movement of other players was just a little jittery. They we not rubber banding or popping but rather just not moving at a steady pace and at times there animation would jitter. After doing research and lots of testing over the course of a week I found 2 issues in my code that caused this issue:
    • My threads were not firing as accurately as I thought.
    • Thanks to variable latency in receiving packets there was times I would not have a movement for a player when processing player's movement/actions
    Before I go on explain the two issues let me fill you in on what I mean by threads. On my server I have various processes that run independently that handle things like: NPC Physics, Player Physics, NPC Spawning, NPC Updates, Player Updates. Each of these threads runs on a timer to do X actions every X milliseconds. The important time sensitive threads are the Physics threads which need to execute in time with the FixedUpdate function on the client. This is needed to keep both the client/server moving at the same speed and that each action is processed at the same time interval.

    So what was the problem with my threads? Currently I have my FixedUpdate running every 16 milliseconds (60 times a second) and since my thread only took 1-2 millisecond to execute I had to sleep the thread for the remainder of the time. To do this I took the start time of the thread and the end time of the thread to figure out how many milliseconds I needed to sleep. Since Thread.Sleep only takes an int there as some rounding issues where I could lose/gain up to 0.5 a millisecond which at teh time I didn't think would be much of an issue. Another problem I found with Thread.Sleep is that it is not exact and only guarantees that the thread will sleep for ATLEAST the time given. This meant sometimes it would sleep for the correct time but others would be longer. The combination of both these issues caused the simulated physics of other players to be far out of sync with both the FixedUpdate as well as the thread that sends network updates to all the other clients. Since I'm programming the server in C# and have found out that Thread.Sleep is inherently inaccurate I needs to find a solution that can execute code every x millisecond fairly accurately. After some reach I found that there is a Multimedia API that has a timer that does just that. Since it's not built into .NET you have to actually access the DLL directly but I found a nice implementation on GitHub that created a HighPrecisionTimer class that uses it (https://github.com/mzboray/HighPrecisionTimer). Once I implemented this timer I found that my code was now executing within 0.01 milliseconds which was a huge improvement over Thread.Sleep.

    IBackgroundThread.cs
    Code (CSharp):
    1.     public interface IBackgroundGameThread
    2.     {
    3.         void Run(object threadContext);
    4.         void Stop();
    5.     }
    NpcPhysicsThread.cs
    Code (CSharp):
    1.     public class NpcPhysicsThread : IBackgroundGameThread
    2.     {
    3.         private IRegion _region;
    4.         private int _tickTime;
    5.         private int _numberOfGameStates;
    6.         private HighPrecisionTimer _tickTimer;
    7.  
    8.         public NpcPhysicsThread(IRegion region, int tickTime, int numberOfGameStates)
    9.         {
    10.             _region = region;
    11.             _tickTime = tickTime;
    12.             _numberOfGameStates = numberOfGameStates;
    13.  
    14.             _tickTimer = new HighPrecisionTimer();
    15.             _tickTimer.Elapsed += (sender, e) => ProcessNpcs(_region, _tickTime, _numberOfGameStates);
    16.             _tickTimer.Interval = tickTime;
    17.         }
    18.  
    19.         public void Run(object threadContext)
    20.         {
    21.             _tickTimer.Start();
    22.         }
    23.  
    24.         private static void ProcessNpcs(IRegion region, int tickTime, int numberOfGameStates)
    25.         {
    26.             //Process All NPC Movements
    27.         }
    28.  
    29.         public void Stop()
    30.         {
    31.             _tickTimer.Stop();
    32.         }
    33.     }

    Region.cs (Example of Starting Threads)
    Code (CSharp):
    1.             //Start Up Background Threads
    2.             _backgroundThreads = new List<IBackgroundGameThread>();
    3.  
    4.             //Start Physics Thread (Move/Player Actions)
    5.             var playerPhysicsThread = new PlayerPhysicsThread(this, 16, 2, 50, 0.125f);
    6.             ThreadPool.QueueUserWorkItem(playerPhysicsThread.Run);
    7.             _backgroundThreads.Add(playerPhysicsThread);
    8.  
    9.             //Start Player Update Thread (Send Player State Updates)
    10.             var playerUpdateThread = new PlayerUpdateThread(this, 8);
    11.             ThreadPool.QueueUserWorkItem(playerUpdateThread.Run);
    12.             _backgroundThreads.Add(playerUpdateThread);
    13.  
    14.             //Start Known Lists Update Thread (Keeps KnownLists Updated)
    15.             var knownListsUpdateThread = new KnownListsUpdateThread(this, new Size(30, 30), 120);
    16.             ThreadPool.QueueUserWorkItem(knownListsUpdateThread.Run);
    17.             _backgroundThreads.Add(knownListsUpdateThread);
    18.  
    19.             //Start Known NPC Spawning Thread (Spawns NPCs if There is a Player Near By)
    20.             var npcSpawningThread = new NpcSpawningThread(this, new Size(40, 40), 120);
    21.             ThreadPool.QueueUserWorkItem(npcSpawningThread.Run);
    22.             _backgroundThreads.Add(npcSpawningThread);
    23.  
    24.             //Start NPC Physics Thread (Move/NPC Actions)
    25.             var npcPhysicsThread = new NpcPhysicsThread(this, 16, 50);
    26.             ThreadPool.QueueUserWorkItem(npcPhysicsThread.Run);
    27.             _backgroundThreads.Add(npcPhysicsThread);
    28.  
    29.             //Start NPC Update Thread (Send Player NPC Updates)
    30.             var npcUpdateThread = new NpcUpdateThread(this, 8);
    31.             ThreadPool.QueueUserWorkItem(npcUpdateThread.Run);
    32.             _backgroundThreads.Add(npcUpdateThread);
    Appling this thread fix made my movement of other player 10x better but there was still occasions were they would jitter ever so slightly. I found out this was due to latency being variable and that at times there wouldn't be any input for a moving character when the player physic thread fired. This caused the character to miss a frame of movement and visually would seem to stop then start again. To solve this issue I applied a server buffer for players which would only send updates of player if there was still X amount of moves in the buffer. By buffering the movements I knew I always had a move to send for the next update tick. Now this wouldn't solve extreme latency issues as you only want to buffer a few frames, so if your latency is greater than the number of buffered frames x 16 milliseconds this jitter movement would start again.

    I hope this post was informative and maybe save you some time in debugging your project. Next up we will move over to the Unity side of things and go through the process of export collision data to be able to use it on our authoritative server to validate movement.

    --James
     
    Last edited: Sep 13, 2021
    ModLunar likes this.
  12. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    After a 2-year break going to start making more progress on this. If there is any topics you would like to see covered just let me know. I should have the post about how I export the collision data from Unity to use for the server physics done in the next few days.
     
    Tom-Kazansky likes this.
  13. qbvbsite

    qbvbsite

    Joined:
    Feb 19, 2013
    Posts:
    83
    Now that we have a character smoothly walking around our area with fellow players the next thing we need to tackle is world collisions. Since this is an authoritative server we can't trust the characters clients position so we have to mimic the clients world server side. To simplify things I made the decision when the character collides with an object it just stops all movement. This was done to simplify the re-creation of the physics on the server and eliminated the need to emulate the Unity slide off physics. Later on I may adjust the moving to create something similar for for know the simple approach is working well. To be able to re-create client side movements server side I need to be able to export all the map tile colliders as well as any objects that have attached 2D colliders. To do this I need to create some Unity buttons and scripts to export the needed data to a format that I can use on the server side of things.

    World Map Colliders
    To create area's of the map where the Hero is unable to walk through I create multiple layers for the world map tile which include 2 layers for collision tiles.

    upload_2023-10-31_16-21-34.png

    Now we create/add a tile palette and make some tile spites that will generate a collider mesh. This is done by adding a physics shape to the tile spite and assign a "Tilemap Collider 2D/Composite Collider 2D" to the GameObject that is acting as our collision layer for the map. Now this works great for getting client side collisions but how to we replicate this on the server? Will the answer is quite easy, I created a Unity UI button called "Export Colliders" and wrote a little script to export all the physics data of the tiles. Once the button and script is created all I need to do to export the data is click the "Grid" Game Object (Parent Object of all my World Tile Layers) and then click "Export Colliders" in the Inspector window.

    Export Tile Colliders Script
    Code (CSharp):
    1.  
    2.     [CustomEditor(typeof(Grid))]
    3.     public class ExportTileEditor : Editor
    4.     {
    5.         public override void OnInspectorGUI()
    6.         {
    7.             DrawDefaultInspector();
    8.  
    9.             if (GUILayout.Button("Export Tiles"))
    10.             {
    11.                 var tilemapExport = new StringBuilder("");
    12.                 tilemapExport.AppendLine("{");
    13.                 tilemapExport.AppendLine("tiles: [");
    14.  
    15.                 //Cast Grid
    16.                 var grid = (Grid)target;
    17.  
    18.                 //Get All Tilemaps In Grid
    19.                 var tilemaps = grid.GetComponentsInChildren<Tilemap>();
    20.  
    21.                 foreach (var tilemap in tilemaps)
    22.                 {
    23.                     //Check To See If Tilemap Has a Collider
    24.                     if (tilemap.GetComponentInParent<TilemapCollider2D>() == null)
    25.                         continue;
    26.  
    27.                     //Loop Through All Tiles of Tilemap with Coillider
    28.                     for (var x = tilemap.cellBounds.xMin; x < tilemap.cellBounds.xMax; x++)
    29.                     {
    30.                         for (var y = tilemap.cellBounds.yMin; y < tilemap.cellBounds.yMax; y++)
    31.                         {
    32.                             for (var z = tilemap.cellBounds.zMin; z < tilemap.cellBounds.zMax; z++)
    33.                             {
    34.                                 //Get Local Vector
    35.                                 var localPlace = new Vector3Int(x, y, z);
    36.  
    37.                                 //Check To See If It Has A Tile
    38.                                 if (!tilemap.HasTile(localPlace))
    39.                                     continue;
    40.  
    41.                                 //Get Tile
    42.                                 var tile = tilemap.GetTile(localPlace);
    43.  
    44.                                 //Get Tile Sprite
    45.                                 var tileSprite = tilemap.GetSprite(localPlace);
    46.  
    47.                                 //Get Physic Object of Sprite
    48.                                 var tilePhysicsShapeVertices = new List<Vector2>();
    49.                                 tileSprite.GetPhysicsShape(0, tilePhysicsShapeVertices);
    50.  
    51.                                 //Get World Location Of Tile
    52.                                 var tileToWorld = tilemap.CellToWorld(localPlace);
    53.  
    54.                                 //Get Tile Rotation
    55.                                 var tileRotation = tilemap.GetTransformMatrix(localPlace).rotation.eulerAngles;
    56.  
    57.                                 //Debug.Log("Local: x:" + localPlace.x + ", y:" + localPlace.y + ", z:" + localPlace.z);
    58.                                 //Export Tile with 0.25 y offset.
    59.                                 tilemapExport.AppendLine(JsonUtility.ToJson(new ExportTile(tile.name, tileToWorld.x + tilemap.tileAnchor.x / 2, tileToWorld.y + tilemap.tileAnchor.y / 2, tileToWorld.z, tileRotation, tilePhysicsShapeVertices.ToArray())) + ",");
    60.                             }
    61.                         }
    62.                     }
    63.                 }
    64.  
    65.                 tilemapExport.Remove(tilemapExport.Length - 3, 3).AppendLine("");
    66.  
    67.                 tilemapExport.AppendLine("]");
    68.                 tilemapExport.AppendLine("}");
    69.  
    70.                 //Save JSON Data
    71.                 File.WriteAllText(Application.dataPath + "/Exports/Tilemap.json", tilemapExport.ToString());
    72.  
    73.                 Debug.Log("Tilemap Exported!");
    74.             }
    75.         }
    76.     }
    Now that we have a JSON export of all the map tile colliders I wrote a little import script on the server. This script uses all the exported vertices, creates a PolygonCollider (the SAT collision routine uses this), and store it in the worlds QuadTree.

    Tile Collider Server Import
    Code (CSharp):
    1.         public void LoadTileColliders(string tilemapJson)
    2.         {
    3.             //Parse Collider Json Export
    4.             dynamic parsedJson = JObject.Parse(tilemapJson);
    5.  
    6.             //Create Collider Objects
    7.             foreach (var collider in parsedJson.tiles)
    8.             {
    9.                 //Get Name/Position
    10.                 var name = (string)collider.Name;
    11.                 var position = new Position(float.Parse(FormatFloat((string) collider.X)), float.Parse(FormatFloat((string)collider.Y)), 0);
    12.                 var rotation = new Position(float.Parse(FormatFloat((string)collider.Rotation.x)), float.Parse(FormatFloat((string)collider.Rotation.y)), float.Parse(FormatFloat((string)collider.Rotation.z)));
    13.  
    14.                 //Get Vertices
    15.                 var vertices = new List<Vector>();
    16.  
    17.                 foreach (var point in collider.Vertices)
    18.                 {
    19.                     var x = float.Parse(FormatFloat((string)point.x));
    20.                     var y = float.Parse(FormatFloat((string)point.y));
    21.  
    22.                     vertices.Add(new Vector(x, y));
    23.                 }
    24.  
    25.                 _mapQuadTree.Insert(new PolygonCollider(position.X, position.Y, vertices));
    26.             }
    27.         }
    World Objects
    For world objects it works very similar, I put all my objects into a Parent "Decorations" GameObject and attach the needed collider to them (Polygon, Box, Circle). Then I created a Unity Button and attached a script that export all the Colliders to a JSON file. On the server I import this JSON file and create the matching collider (Polygon, Box, Circle) and store it in the world QuadTree.

    Export World Object Colliders
    Code (CSharp):
    1.             if (GUILayout.Button("Export Colliders"))
    2.             {
    3.                 //Cast GameObject
    4.                 var gameObject = (GameObject)target;
    5.  
    6.                 var colliderExport = new StringBuilder("");
    7.                 colliderExport.AppendLine("{");
    8.                 colliderExport.AppendLine("colliders: [");
    9.  
    10.                 //Check For Circle Colliders
    11.                 foreach (var circleCollider2D in gameObject.GetComponentsInChildren<CircleCollider2D>())
    12.                 {
    13.                     colliderExport.AppendLine(JsonUtility.ToJson(new ExportCircleCollider(circleCollider2D.name, 1, circleCollider2D)) + ",");
    14.                 }
    15.  
    16.                 //Check For Box Colliders
    17.                 foreach (var boxCollider2D in gameObject.GetComponentsInChildren<BoxCollider2D>())
    18.                 {
    19.                     colliderExport.AppendLine(JsonUtility.ToJson(new ExportBoxCollider(boxCollider2D.name, 2, boxCollider2D)) + ",");
    20.                 }
    21.  
    22.                 //Check For Polygon Colliders
    23.                 foreach (var polygonCollider2D in gameObject.GetComponentsInChildren<PolygonCollider2D>())
    24.                 {
    25.                     colliderExport.AppendLine(JsonUtility.ToJson(new ExportPolygonCollider(polygonCollider2D.name, 3, polygonCollider2D)) + ",");
    26.                 }
    27.  
    28.                 //Add To String Builder
    29.                 colliderExport.Remove(colliderExport.Length - 3, 3).AppendLine("");
    30.                 colliderExport.AppendLine("]");
    31.                 colliderExport.AppendLine("}");
    32.  
    33.                 //Save JSON Data
    34.                 File.WriteAllText(Application.dataPath + "/Exports/GameObjectColliders.json", colliderExport.ToString());
    35.  
    36.                 Debug.Log("Colliders Exported!");
    37.             }
    Import World Collider Objects

    Code (CSharp):
    1.        public void LoadColliders(string colliderJson)
    2.        {
    3.            //Parse Collider Json Export
    4.            dynamic parsedJson = JObject.Parse(colliderJson);
    5.  
    6.            //Create Collider Objects
    7.            foreach (var collider in parsedJson.colliders)
    8.            {
    9.                //Get Name/Position
    10.                var name = (string)collider.Name;
    11.                var position = new Position(float.Parse(FormatFloat((string)collider.X)), float.Parse(FormatFloat((string)collider.Y)), 0);
    12.  
    13.                switch ((ColliderType)collider.Type)
    14.                {
    15.                    case ColliderType.Circle:
    16.                        var radius = float.Parse(FormatFloat((string)collider.Radius));
    17.  
    18.                        _mapQuadTree.Insert(new CircleCollider(position.X, position.Y, radius));
    19.                        break;
    20.  
    21.                    case ColliderType.Box:
    22.                        var size = new SizeF(float.Parse(FormatFloat((string)collider.Length)), float.Parse(FormatFloat((string)collider.Height)));
    23.  
    24.                        _mapQuadTree.Insert(PolygonCollider.Rectangle(position.X, position.Y, size.Width, size.Height));
    25.                        break;
    26.  
    27.                    case ColliderType.Polygon:
    28.                        var vertices = new List<Vector>();
    29.  
    30.                        foreach (var point in collider.Points)
    31.                        {
    32.                            var x = float.Parse(FormatFloat((string)point.x));
    33.                            var y = float.Parse(FormatFloat((string)point.y));
    34.  
    35.                            vertices.Add(new Vector(x, y));
    36.                        }
    37.  
    38.                        _mapQuadTree.Insert(new PolygonCollider(position.X, position.Y, vertices));
    39.                        break;
    40.  
    41.                    default:
    42.                        break;
    43.                }
    44.            }
    45.        }
    Now the server has an exact replica of the client's world map that we can now test for collisions to validate players movements. You just have to remember every time you adjust your map in Unity that you have to re-export the colliders for the server. I hope this helps anyone trying to make an authoritative server without using Unity as a backend. Next post I'll go about how I'm going to handle adding NPCs to my game along with some simple patrolling code.
     
    Last edited: Nov 7, 2023
    HellRayzor likes this.