A little bee Advanced Version is currently extracting the frame synchronization logic into the SDK, please see the project https://github.com/dudu502/littlebee_libs This is a frame synchronization game example, synchronizing hundreds of objects and thousands of states in the game. The background of the game is a shooting game under a planetary system. The following is the video link.
[Watch playing the video (Youtube)]
[Watch replaying the video (Youtube)]
[Watch playing the video(bilibili)]
[Watch replaying the video(bilibili)]
Frame synchronization | State synchronization | |
---|---|---|
consistency | The design level determines the inevitable consistency | Can guarantee consistency |
Number of players | Limited multiplayer support | Multiple players have advantages |
Cross-platform | Need to consider the consistency of floating point operations | Since the main calculations are done on the server, there are no cross-platform issues. |
Anti-cheating | Easy to cheat, but can be optimized | Can be very good at preventing cheating |
Disconnect and reconnect | It’s difficult to implement, but it’s not impossible | You only need to resend the message once, easy to implement |
Playback requirements | can be realized perfectly | Unable to achieve |
pause game | Easy to implement | Not easy to implement |
Network transmission volume | relatively small | relatively large |
Development difficulty | relatively complex | relatively simple |
RTS games | Suitable | Not suitable |
fighting games | Suitable | Not suitable |
MOBA games | Suitable | Not suitable |
MMO games | Not suitable | Suitable |
After understanding the difficulties that need to be overcome in the frame synchronization development process, we will next consider choosing a better implementation method, or a development framework. Since frame synchronization development requires the separation of data and performance, to what extent should it be separated? The data calculation part can even be placed in a separate thread. The advantage of writing logic in this way is that it also allows the server to run to achieve the function of quickly replaying the game. I think only ECS can achieve this level of separation. Frame synchronization plus ECS is absolutely a perfect partner.
First, we need to introduce ECS. ECS is not a brand-new technology, nor was it first proposed by Unity. This term appeared very early, and it suddenly became popular in recent years because of Blizzard's "Overwatch". The server and client frameworks of "Overwatch" are completely built based on ECS, and have excellent performance in game mechanics, network, and rendering. Frankly speaking, ECS is not like a design pattern. The design patterns we used before were all discussed under object-oriented design, and ECS is not object-oriented. Unity also has ECS. In fact, Unity's own components are also a kind of ECS, but they are not pure enough. ECS is particularly suitable for Gameplay. There are many variants of ECS, and here is ECS with some slight modifications.
This is a feature of frame synchronization games. If a game has a replay system, then the game must be implemented through frame synchronization. Playback can also be called video recording, but it is very different from video recording. Playback using video files as the carrier usually takes up a huge file, and the window cannot be switched during the playback process. Videos are easily stolen, abused, maliciously modified, compressed, and degraded. quality, so video playback has a big disadvantage. Frame-synchronized playback can make the file extremely small and cannot be tampered with. Users can switch windows at will during the playback process. It can be said that the necessary system for frame synchronization games is the replay system.
RevenantX/LiteNetLib is recommended here. This library is very powerful and simple to use. It provides reliable UDP transmission, which is exactly what I want. There are many data protocols to choose from for network communication. I am using a self-made binary stream protocol. The main functions are serialization and deserialization. The fields in the structure are optional. Like this PtRoom structure:
//Template auto generator:[AutoGenPt] v1.0
//Creation time:2021/1/28 16:43:48
using System ;
using System . Collections ;
using System . Collections . Generic ;
namespace Net . Pt
{
public class PtRoom
{
public byte __tag__ { get ; private set ; }
public uint RoomId { get ; private set ; }
public byte Status { get ; private set ; }
public uint MapId { get ; private set ; }
public string RoomOwnerUserId { get ; private set ; }
public byte MaxPlayerCount { get ; private set ; }
public List < PtRoomPlayer > Players { get ; private set ; }
public PtRoom SetRoomId ( uint value ) { RoomId = value ; __tag__ |= 1 ; return this ; }
public PtRoom SetStatus ( byte value ) { Status = value ; __tag__ |= 2 ; return this ; }
public PtRoom SetMapId ( uint value ) { MapId = value ; __tag__ |= 4 ; return this ; }
public PtRoom SetRoomOwnerUserId ( string value ) { RoomOwnerUserId = value ; __tag__ |= 8 ; return this ; }
public PtRoom SetMaxPlayerCount ( byte value ) { MaxPlayerCount = value ; __tag__ |= 16 ; return this ; }
public PtRoom SetPlayers ( List < PtRoomPlayer > value ) { Players = value ; __tag__ |= 32 ; return this ; }
public bool HasRoomId ( ) { return ( __tag__ & 1 ) == 1 ; }
public bool HasStatus ( ) { return ( __tag__ & 2 ) == 2 ; }
public bool HasMapId ( ) { return ( __tag__ & 4 ) == 4 ; }
public bool HasRoomOwnerUserId ( ) { return ( __tag__ & 8 ) == 8 ; }
public bool HasMaxPlayerCount ( ) { return ( __tag__ & 16 ) == 16 ; }
public bool HasPlayers ( ) { return ( __tag__ & 32 ) == 32 ; }
public static byte [ ] Write ( PtRoom data )
{
using ( ByteBuffer buffer = new ByteBuffer ( ) )
{
buffer . WriteByte ( data . __tag__ ) ;
if ( data . HasRoomId ( ) ) buffer . WriteUInt32 ( data . RoomId ) ;
if ( data . HasStatus ( ) ) buffer . WriteByte ( data . Status ) ;
if ( data . HasMapId ( ) ) buffer . WriteUInt32 ( data . MapId ) ;
if ( data . HasRoomOwnerUserId ( ) ) buffer . WriteString ( data . RoomOwnerUserId ) ;
if ( data . HasMaxPlayerCount ( ) ) buffer . WriteByte ( data . MaxPlayerCount ) ;
if ( data . HasPlayers ( ) ) buffer . WriteCollection ( data . Players , ( element ) => PtRoomPlayer . Write ( element ) ) ;
return buffer . Getbuffer ( ) ;
}
}
public static PtRoom Read ( byte [ ] bytes )
{
using ( ByteBuffer buffer = new ByteBuffer ( bytes ) )
{
PtRoom data = new PtRoom ( ) ;
data . __tag__ = buffer . ReadByte ( ) ;
if ( data . HasRoomId ( ) ) data . RoomId = buffer . ReadUInt32 ( ) ;
if ( data . HasStatus ( ) ) data . Status = buffer . ReadByte ( ) ;
if ( data . HasMapId ( ) ) data . MapId = buffer . ReadUInt32 ( ) ;
if ( data . HasRoomOwnerUserId ( ) ) data . RoomOwnerUserId = buffer . ReadString ( ) ;
if ( data . HasMaxPlayerCount ( ) ) data . MaxPlayerCount = buffer . ReadByte ( ) ;
if ( data . HasPlayers ( ) ) data . Players = buffer . ReadCollection ( ( rBytes ) => PtRoomPlayer . Read ( rBytes ) ) ;
return data ;
}
}
}
}
This is a Unity project based on frame synchronization
Some tools: Pt structure generation tool, Excel2Json generation tool, General library project, ServerDll library project
Design documents: outline design documents, prototype design documents, configuration tables.
The following three figures describe the use of the frame synchronization simulator in three different scenarios.
The figure below shows the general behavior of the client and server at the same time, and the playback logic also corresponds to the same behavior.
This picture shows the client and server executing logic in each logical TICK. The upper part is the client. The logic that the client needs to execute includes the ECSR part, and the lower part is the server part.
The last picture describes each logical frame of playback.
Through these pictures and the specific types of games, we can set up custom System and Component to handle related logic.
This is a service collection project, including WebServer, GateServer, RoomServer, etc.