❓ Frame synchronization and state synchronization can be used together? ❓ 200ms millisecond delay can also achieve the silky smooth single-player game? ❓ How to achieve skill determination in the case of delay?

See Demo: tsrpc.cn/fight/index… You can invite friends or browser to experience the effect of multiplayer

Writing in the front

Multiplayer real-time combat has always been a hard nut to crack in game development. As difficult as it sounds, getting it right is even harder. But times are moving on, technology is evolving. Just as Cocos Creator has made game development easier, the bar for multiplayer real-time battles has been lowered with the latest technology stacks and understandings.

On December 4, 2021, I was honored to attend the offline communication Meeting of Cocos Star Meeting in Shenzhen station as a guest and shared some information about the field of “real-time multiplayer battle” combined with TSRPC, the open source framework that has been accumulated for 5 years.

The following is a transcript of this shared content.

To introduce myself

Hello, everyone. First, let me introduce myself. I’m King Wang Zhongyang and my Github ID is K8W.

Tencent interactive entertainment used to be an old goose, is also an old full stack. I discovered TypeScript in 2016 and started using it for full-stack development, and it’s really taken off. TSRPC 1.0 made its first appearance on Github in 2017. After 5 years of development, it is now in version 3.x.

Now I spend most of my time on the development and maintenance of the open source project TSRPC, and also provide some technical consulting services. Welcome to pay attention to my official number/B site/Zhihu column/digging gold – TypeScript full stack development.

directory

Then enter today’s theme – TSRPC + Cocos, multiplayer real-time battle So Easy!

I will mainly introduce it into three parts:

  • Synchronization strategies
    • Describes how to optimize the real-time battle experience with network latency
  • Network communication
    • Details the pain points of network communication, and introduces the ultimate solution – TSRPC
  • The project of actual combat
    • Start from scratch to achieve a crazy fight group multiplayer version, see if it is So Easy
  • Additional content
    • Add some useful content that you didn’t mention when sharing offline

Synchronization strategies

When you think of “multiplayer in real time”, what comes to mind when you think of “synchronization”?

Frame synchronization vs state synchronization?

Yeah, a lot of people think frame synchronization and state synchronization. You can search for numerous descriptions of them, such as:

Transmission content Logic calculation Break line reconnection Replay/watch the game
Frame synchronization operation The client After the historical frame Natural support
State synchronization The results of The service side The next time synchronization Another implementation

But first, I want to correct a common misconception.

A lot of the time we talk about “frame sync or state sync” and it seems like it’s become an either-or question. But in fact, frame synchronization and state synchronization are not an either/or relationship, and can even be interchangeable and mixed.

Frame synchronization and state synchronization are ultimately synchronous states.

Let’s say we want to implement a bunch of people running around a room, and whether you use state synchronization, which sends your coordinates directly, or frame synchronization, which sends the movement operation and the client calculates the final coordinates, for the presentation layer component, all you need is your final state, the position coordinates. So really frame synchronization and state synchronization are more about what goes over the network and where the state is calculated — it seems more like a cost issue.

Just make sure your state calculation logic works on both the front and back ends, so frame synchronization or state synchronization can be used concurrently or switched at any time.

sync

Frame synchronization and state synchronization are synchronous states.

Let’s start by looking at how the simplest state can be synchronized without any optimization strategy.

  1. Local press the key to issue operation instructions.
  2. The instructions are sent to the server, which gets the latest status and broadcasts it to everyone.
    • Frame synchronization broadcast operation instructions, status synchronization broadcast results
  3. The front end refreshes the interface after receiving the new status from the server.

The result is shown below. (Your view above, server in the middle, other player’s view below)

Obviously, there are several problems:

  1. Operation delay
    • Due to network latency, it takes a while to receive the return from the server after pressing the left and right buttons, resulting in delayed operation and poor experience
  2. caton
    • Because the server synchronizes the logical frame rate (3 frames per second) slower than the display frame rate (60 frames per second), the displacement is slow and not smooth

However, network latency must exist objectively, and the frame rate of synchronization and display may not be consistent. So we needed a way to have a smooth, smooth experience with no lag, with lag. This is the magic of synchronization — synchronization strategy.

Synchronization strategies

Network latency is an objective reality, so synchronization strategy is essentially a magic trick to achieve the illusion of no latency when there is latency. There are several main types, depending on the project.

First of all, according to the synchronous rhythm, it is divided into fast and slow.

slow

Synchronization interval of 1~2 seconds or more, such as playing cards, chess, etc. It’s as simple as optimizing on the performance level for immediate feedback. For example, when a player clicks a mouse button, instead of waiting for the server to return to display the move, the move is immediately displayed with a clear “pop” echo, giving the player an immediate feedback on the action. The server may actually be delayed by a second, but the player will have no idea.

The fast pace

The synchronization interval is shorter, for example, to enable us to run around the room. So there are cases, conflict-free and conflicted.

If we’re all ghosts, then there’s no conflict. Because our bodies are void and can penetrate each other. Your position is controlled only by you and not by any other factors. This is called conflict-free. The solution is simple — treat yourself differently from everyone else. You implement it yourself as a single player game, and any movement you make is immediately applied to the presentation layer, just sending that information to the server in sync. Others simply receive messages from the server and animate their changes. Of course, because of network latency, you may be looking at someone else’s status five or ten seconds ago. But what does that matter? With ghosts being conflict-free, there’s no impact, and everyone gets a silky, dray-free experience like a console game, and everyone is happy.

But on the other hand, if we were all flesh and blood, there would be an actual physical collision, and I would stand here and you wouldn’t be able to stand in the same position. This situation is called conflict. Fast paced, conflicting synchronization strategies are more complex, and are described below.

Fast paced conflicting synchronization strategies

The core of the synchronization strategy to solve the fast pace conflict is three key words: prediction, reconciliation and interpolation. With these three concepts in mind, synchronization in any situation should be easy for you. But before we do that, let’s look at the architecture of the separation of logic and presentation.

Separation of logic and presentation

Multiplayer real-time games are usually divided into presentation and logic layers. Presentation layer refers to the display of the game screen and the acquisition of user input; The logic layer refers to the gameplay logic that is rendered irrelevant and only focuses on state changes and calculations.

The logical and presentation layers are ultimately data-oriented, such as the player’s position, health, etc., which we collectively refer to as states. We call all factors that affect state change inputs, such as player actions (movement), system events (thunder in the sky), the passage of time, and so on.

The logical layer is the algorithm that defines all the states and inputs and then implements state changes:

New state = Old state + Enter 1 +... N + inputCopy the code

The critical logic layer is essentially a state machine.

There are several important points to focus on when implementing the logical layer:

  • No input, no change: The state changes only at the input time. The state does not change without input
  • No external dependencies: State calculations should be free of any external dependencies, for exampleDate.now(),Math.random()All of these should be explicitly part of the input
  • Consistency of results: Given the same state and input, the resulting new state should be consistent

Scenarios such as random numbers can be implemented using a pseudorandom number generator, where random results should be consistent with the same seed input.

Prediction, reconciliation, and interpolation are easier to understand on the basis of state calculations at the logical layer.

To predict

Prediction is the immediate application of the player’s input to the local state without waiting for the server to return.

If every action the player takes takes effect until the server confirms it, then delays are inevitable. The solution: immediately after the player does anything, apply the input to the local state and refresh the presentation layer display. For example, if you press “Right”, you immediately move to the right without waiting for the server to return.

Now, the latency of the operation is gone. You can press “left” or “right” to get immediate feedback.

But the problem doesn’t seem to be completely solved, and you can always feel a “tug” or position wobble back and forth during movement. This is because when you perform local predictions, you are also receiving synchronization from the server, and the status from the server is always lagging.

Such as:

  1. Your coordinates are(0, 0)
  2. You sent out twoMoves to the rightCommand (move 1 unit to the right at a time), the server has not returned, after performing a local prediction,Coordinates into(2, 0)
  3. You sent out two moreMoves to the rightCommand, the server has not yet returned, after performing a local prediction,Coordinates into(4, 0)
  4. The server sent back your first two right shift commands: from(0, 0)Perform 2 right shifts,Coordinates into(2, 0)“Is pulled back to its previous position

Because of the latency, synchronization on the server always lags, so you are always pulled back to where you were before. Back and forth, that’s the shaking and pulling you see in the picture.

Ultimately, the state synchronized from the server is inconsistent with the state predicted locally, so we need to “reconcile” them.

The settlement

The settlement is a formula: predicted state = authoritative state + predicted input

The concept of important reconciliation is one of the most difficult to understand, but one of the most important steps in achieving a delay-free experience. You can simply memorize the above formula and try it on your project.

Authority and prediction

Generally speaking, the server is always considered authoritative. The input received from the server is called authoritative input, and the state calculated by authoritative input is called authoritative state. Similarly, when we send an input that has not been confirmed by the server, the input is called an unauthoritative input, also known as a predictive input.

In the case of an open network, predictive inputs will sooner or later become authoritative in the order they are sent. We need to know which inputs are being sent, which have become authoritative inputs and which are still predictive inputs. With reliable transport protocols (such as WebSocket) you don’t have to worry about packet loss and order, so you can simply compare message numbers.

Reconciliation process

Based on the foregoing predictions, reconciliation is how we handle the state of server-side synchronization. If state synchronization is used, the process is as follows:

  1. Received authoritative status from server synchronization
  2. Set the local state to the authoritative state immediately
  3. Apply all the current predictive inputs based on the authoritative state

If frame synchronization is used, the process is as follows:

  1. Received authoritative input from server synchronization
  2. The local state is immediately rolled back to the previous authoritative state
  3. The authority state is obtained by applying the authority input to the current state
  4. Apply all the current predictive inputs based on the authoritative state

Thus, state synchronization and frame synchronization are just different things transmitted over the network, but they are completely interchangeable – the ultimate goal is to synchronize authoritative state.

example

Does it work? Let’s go back to the example of the prediction above, and see what happens with the settlement:

  1. Your coordinates are(0, 0)
  2. You sent out twoMoves to the rightCommand (move one unit to the right at a time), the server has not returned
    • Authority state:(0, 0)
    • Predicted input:Moves to the right # 1 Moves to the right # 2
    • Forecast status:(2, 0)(Authoritative state + predictive input)
  3. You sent out two moreMoves to the rightCommand, the server has not returned
    • Authority state:(0, 0)(No server synchronization received, unchanged)
    • Predicted input:Moves to the right # 1 Moves to the right # 2 Moves to the right # 3 Moves to the right # 4
    • Forecast status:(4, 0)(Authoritative state + predictive input)
  4. The server sent back your first two right shift commands (frame sync)
    • Previous authority status:(0, 0)
    • Authoritative input:Moves to the right # 1 Moves to the right # 2
    • Authority state:(2, 0)(Previous authority status + authority input)
    • Predicted input:Moves to the right # 3 Moves to the right # 4# 1,# 2Becomes authoritative input)
    • Forecast status:(4, 0)(Authoritative state + predictive input,The previous pull is gone)

Look! Although the authority state from server synchronization is “past”, with reconciliation, the pull problem is resolved, as shown in the following figure:

Prediction + reconciliation is a very common approach to handling local input. You’ll find that when there are no collisions, network latency doesn’t affect operation latency at all, just like in a console game! For example, in the move example above, if there is no collision (such as a collision with another person), even if the network delay is 10 seconds, you can move without delay and smoothly. This is the magic of a delay-free experience with delay.

conflict

What about the conflict situation? For example, in the example above, you send the move command four times, but on the server, after the second move command, the server inserts a new input — “You were stunned by someone’s brick”. This means that your last two moves to the right will not work (because you are dizzy). The process would look like this:

  1. Your coordinates are(0, 0)
  2. You sent out twoMoves to the rightCommand (move one unit to the right at a time), the server has not returned
    • Authority state:(0, 0)
    • Predicted input:Moves to the right # 1 Moves to the right # 2
    • Forecast status:(2, 0)
  3. You sent out two moreMoves to the rightCommand, the server has not returned
    • Authority state:(0, 0)
    • Predicted input:Moves to the right # 1 Moves to the right # 2 Moves to the right # 3 Moves to the right # 4
    • Forecast status:(4, 0)
  4. The server sent back your first two right shift commands
    • Authority state:(2, 0)
    • Predicted input:Moves to the right # 3 Moves to the right # 4# 1,# 2Becomes authoritative input)
    • Forecast status:(4, 0)
  5. The server sent back new input that conflicted with expectations
    • Previous authority status:(2, 0)
    • Authoritative input:You're stunned Moves to the right # 3 Moves to the right # 4
    • Authority state:(2, 0)(The last two right move commands are invalid because I was stunned by the clap first)
    • Predictive inputs: None (All predictive inputs have become authoritative)
    • Forecast status:(2, 0)

At this point, the previous forecast state (4,0) conflicts with the latest forecast state (2,0). Of course, the client is dominated by the latest state, so you are pulled back to (2,0) and appear dizzy. This is the cost of network latency — probability of conflict.

The interpolation

Interpolation refers to the use of interpolation animations to smooth transitions as the presentation layer updates state changes for “other people”.

So far, we’ve had our own delay-free local silkboarding experience. But in the eyes of other players, we’re still stuck. This is due to the mismatch between the synchronized frame rate and the display frame rate, so instead of updating someone else’s state in one step, we use interpolating animations to smooth the transition.

Important prediction + reconciliation is to solve their own problems that occur at the logical level; Interpolation is solving other people’s problems and occurs at the presentation level.

For example, in the example above, the frame rate is 30fps and the server synchronization frame rate is 3 FPS. Instead of setting Node. position immediately after receiving the status of other players synchronized from the server, Tween it to move smoothly from the current position to the new position within a short period of time. This way, we look smooth to other players:

Prediction, reconciliation and interpolation are the three core ideas to solve the fast-paced and conflicting synchronization, and you should be able to learn from them and deal with various scenarios with ease.

Network communication

With that in mind, we are now going to start writing a multiplayer real-time battle project. What is the first problem that stands in our way at the hands-on level – network communication? In the field of Internet communication, we’ve always had a hard time actually, because there’s always a lot of pain points, but we’re probably used to it.

The pain of defining an agreement

To communicate, you need to define the protocol, which is what you want to send between the server and the client. There are several common ways.

1. Defined in documents

Many project teams define protocols through documentation, and the problem is obvious. Because documents are not strongly typed, spelling errors, field type errors and other low-level errors occur frequently. When the protocol changes, inconsistency between the documentation and the code often occurs. Have to spend a lot of time, but the solution is only these low-level mistakes, high risk, low efficiency.

2. Use the Protobuf

Protobuf is a common tool in the game industry that can be used for runtime type detection and binary serialization. The downside is that its types are defined in a separate language, which can add a lot of extra learning costs. Because language is different, Protobuf cannot fully exert TypeScript type features, such as A & B | C) that can’t use the common type of advanced characteristics.

3. Use the TypeScript

Using TypeScript types directly to define protocols is not only convenient, but also can be shared on the front and back end to facilitate code prompting. But TypeScript’s type system only works at compile time and has no runtime support, which can be a security risk for unreliable user input. It also doesn’t do binary serialization like Protobuf does.

Multiple communication model

In network communication in a multiplayer real-time game, we handle network requests in a number of ways.

For example, call the API interface, which is based on the request/response model usage. Interfaces such as login and registration are often implemented using HTTP short connections in Web applications.

But there are also uses based on the publish/subscribe model, such as server-side push and streaming. Such as frame synchronization broadcast, chat message broadcast, often using WebSocket long connection to achieve.

HTTP uses frameworks such as ExpressJS, while WebSocket uses frameworks such as SocketIO. They have different frameworks, apis, and technical solutions, and often have to be split into different projects. But in fact, their business logic is highly similar, which makes unified maintenance difficult and learning cost high.

Safety safety safety

Important things three times: Safety! Safety! Safety! What is the game industry most afraid of? Plugins.

Caught breaking

Currently, Most Web applications are delivered via JSON strings. Plaintext JSON is too easy to capture and crack, which is disastrous for the game. The string encryption algorithm itself is very limited, many project teams choose to switch to Base64 strings, but this will significantly increase the package body.

Low-level mistakes

100 + '100' === '100100'
Copy the code

Should pass numbers but accidentally pass strings? Small type errors can have serious consequences. Murphy’s Law tells us that anything that can go wrong will go wrong. Relying solely on humans for type safety is not reliable.

Safe hidden trouble

User input is always unreliable! Illegal request parameter types and lax field filtering can lead to serious security risks! For example, if there is an interface user/Update that updates user information, the request format is defined as:

export interface ReqUpdate {
    id: number.update: { nickname? :string, avatar? :string}}Copy the code

If the client constructs a malicious request, the update contains a sensitive role field that should not appear:

{
    "id": 123."update": {
        "nickname": "test"."role": "Super Administrator"    // Sensitive field, not in protocol, not allowed to update!}}Copy the code

The back end is likely to be lax, resulting in security risks!

so

We couldn’t find an off-the-shelf framework that perfectly solved these problems, so we redesigned and created TSRPC. So far, it has lasted for 5 years and has been verified by more than 10 million user projects.

TSRPC

Let’s take a look at TSRPC, a MORE coCOs-friendly RPC framework designed for TypeScript.

  • Liverpoolfc.tv: TSRPC. Cn
  • Documents: TSRPC. Cn/docs/introd…
  • Example: github.com/k8w/tsrpc-e…
  • Github: github.com/k8w/tsrpc

Designed for TypeScript

TSRPC is designed specifically for TypeScript, so it naturally fits Cocos.

  • 🔥 uses TypeScript types directly to define protocols
    • No decorators, no annotations, no Protobuf
    • Supports TypeScript advanced type features such asA & (B | C),Pick,Omit, complex nested references, etc

  • 🔥 Runtime type safety
    • Request and response types are automatically validated at runtime based on TypeScript type definitions
    • Automatically intercepts requests of invalid type
  • 🔥 supports binary serialization
    • TypeScript types can be encoded directly to binary
    • Coding efficiency is similar to Protobuf and supports TypeScript advanced types
  • 🔥 Front-end and back-end full code prompt
    • A full-stack architecture that reuses code and type definitions at the front and back ends
    • Full code prompt, avoid low-level errors

Transport protocol-independent architecture

TSRPC was designed from the outset to be transport protocol-independent.

This means that you can write a single set of code that runs on both HTTP short connections and WebSocket long connections. Instead of splitting projects, you can use both short and long connections in one project. At the same time, TSRPC can be easily extended to UDP, IPC, even Web Worker and other arbitrary channels.

TSRPC also supports a variety of transport formats, and you can choose whether to use binary encoded transport (which is smaller) or JSON transport (which is more general). TSRPC also allows you to directly use types such as ArrayBuffer, Date, and ObjectId in the protocol, even if you choose to transfer them using JSON. The framework automatically converts the types before and after the transfer, making it easier to send and receive binary data.

Other features

  • cross-platform
    • Support browsers, small programs, small games, App and other platforms
    • NodeJS pure back-end microservice calls are supported
  • Serverless cloud function deployment is supported
  • One-click generation of Swagger/OpenAPI/Markdown format interface documents
  • Mature, reliable and high performance
    • Multiple tens of millions of users online project verification

Links to resources

To learn more about the features and how to use them, see the

  • Liverpoolfc.tv: TSRPC. Cn
  • Documents: TSRPC. Cn/docs/introd…
  • Example: github.com/k8w/tsrpc-e…
  • Github: github.com/k8w/tsrpc

The project of actual combat

With ideas, but also to solve the problem of network communication, next we start from scratch, from the front end to the back end, to complete the implementation of a multiplayer game example, see if it is So Easy.

The Demo presentation

First let’s take a look at the finished product. It took me two and a half days to create this example. Github has records to check.

  • Experience address: tsrpc.cn/fight/index…
  • Source code: github.com/k8w/tsrpc-e…

Does that look familiar? That’s right, the new “Crazy Fight” that came out of the Cocos store a few days ago, and I snapped it up right away. Then this Demo is to take the resources of the crazy fight group to change a multiplayer version, it is mainly 2 simple gameplay:

  1. A group of people are running around the scene
  2. You can shoot an arrow and the target is stunned in place for 1 second, unable to move

After applying the above prediction + reconciliation + interpolation, let’s see what happens.

Local no delay running

As in the video above, the device in the middle has a 200ms network delay, and you can clearly see that its picture is out of sync with the left and right sides. However, for him, there is no delay at all, he does not feel the existence of the 200ms, this is the magic of prediction + reconciliation.

Where conflict

Archery + stun is a point that can lead to conflict.

In the video above, for example, the device in the middle still has a 200ms delay. As he maneuvered out, an arrow landed at his starting position. Of course, due to network latency, the arrow didn’t shoot when he started running, so he ran out based on local predictions. When a delayed synchronization is received, the server tells it that you were stunned 200ms ago and can’t get out. So his position was suddenly pulled back and he became dizzy, and at this time he could clearly feel as if he was stuck.

Frame synchronization and state synchronization are used together

This is a typical fast-paced, conflicting synchronization that can be properly resolved using prediction + reconciliation + interpolation, but there is another requirement.

I want to be able to start the game as soon as I enter the room, rather than having to replay frames from the beginning like King of Glory does, which means I have to use state sync as soon as I enter the game.

However, I wanted to keep the packet size of the network transmission as small as possible, so if I wanted to transmit actions during the game rather than complete state, I would need to use frame synchronization during the game after entering the room.

It’s totally doable.

The whole stack architecture

The key to multiplayer is the separation of logic and presentation. Logic is pure TypeScript code that should be platform-independent and reusable across ends. Frame synchronization is used with state synchronization, which is equivalent to calculating the game state logic on both the server (state synchronization) and client (frame synchronization). Therefore, the full stack architecture should look like this.

This full stack diagram is the core of what I’m sharing today. If you understand the process, you can easily achieve the same effect.

It is mainly divided into several pieces (the English name is arbitrary), in fact, you can look at each piece is very simple.

Cross-end multiplexing part

  • State calculation GameSystem
    • Define state and input
    • implementationOld state + input = New stateThe algorithm of

The service side

  • Game Room
    • Receiving player input
    • Regular radio
    • Synchronizing the computed state (call GameSystem)

The client

  • Logical layer (prediction + reconciliation) GameManager
    • Compute authority status through server input
    • Authoritative state + forecast input = local forecast state
  • The presentation layer GameScene
    • Take the state from the logical layer and update the render display
      • Update yourself directly
      • Smooth interpolation others
    • Receives user input and sends it to the logical layer

TSRPC full stack project structure

As mentioned above, the GameSystem part of the code is intended to be reused on the front and back ends. In addition, there are other code and type definitions that we might want to share on the front and back ends.

TSRPC was designed from the beginning to be full-stack oriented, so it already has a built-in solution for sharing code across ends and projects. The default is Symlink, which is similar to shortcuts in Windows. For example, the Demo project has two directories, backend and frontend. The backend project has a directory named shared, which is shared across the backend project. There is also a shared in the front-end project, but it is a Symlink that points to the shared directory in the back-end, which is like a shortcut. You can think of them as a directory. When one side adds/modifies a file, the other side also changes synchronously.

Write state calculation

Let’s start writing the code, starting with GameSystem, which consists of three steps:

  1. Define state
  2. Define the input
  3. Implement state calculation

Define state

In this Demo, there are three main states:

  • The current timenow
    • The gameplay logic of many games is all about time
    • In this Demo, the arrow landing judgment needs to rely on it
  • All playersplayers
    • Including the locationpos
    • And the stun state, which I’m going to indicate with a stun end time
  • All the arrows in flightarrows
    • Including landing time and landing position, through them to complete the judgment of hit

Simply use TypeScript types.

Define the input

Next, we need to define all the inputs that can affect state changes as follows:

  • User action class
    • mobile
    • To attack (with arrows)
  • System event class
    • Players to join
    • Players leave
  • Time goes by
    • The synchronization is subject to server synchronization

You can define them separately, add a mutually exclusive field such as Type, and combine them into a TypeScript Union type.

Tips The types you define in your code can be used directly for TSRPC’s network communication ~ with no extra cost to enjoy runtime type safety and binary serialization features.

Implement state calculation

Finally, to implement the state calculation algorithm, called GameSystem, you can encapsulate it with a simple class:

  • A member variablestateIs used to store the current state
  • aapplyInputMethod, which takes the input and changes the state
    • For example, if the input is “new player joins”, thestate.playersAdd an item to
    • If the input is “move”, the corresponding player’s is updatedposPosition state

It’s that simple ~ Remember to make sure the principles are:

  • No external dependencies
    • All factors that affect state changes should be defined as inputs, including random numbers, time lapse, and so on
  • No input, no change
    • Only when theapplyInputThe state changes only when there is input

Write the back-end

The main task of the back end is to receive input from the player and then complete the synchronization.

You can choose to synchronize as soon as you receive input, or LockStep at a fixed frequency. I chose the latter and set the sync frequency to 10fps (100ms interval) because the 100ms delay is a perfectly acceptable collision probability for this gameplay.

Because it’s fixed frequency sync, we don’t do anything when we receive player input, just save it temporarily:

Then I have a method called sync, which the server calls every 100ms. It does two things:

  • Calculation of state
    • Referring to GameSystem, a copy of state is also computed synchronously on the server
    • When a new player joins, the current state is sent once to complete the initial state synchronization
  • Radio input
    • All inputs during this frame are broadcast to everyone

Don’t doubt it, there’s so much code, simple ~

Write the front-end logic layer

Next, write the front-end logic layer. Isn’t the state calculation both front and back end multiplexed? Why is there another logical layer on the front end? This is because the state displayed on the front end is not the state directly sent by the server (that would be state synchronization). Because the front end needs to do prediction + reconciliation processing, there is also a front end logic layer between the state calculation and performance layer.

The logic layer of the front end is designed to do the authoritative state + predicted input = local predicted state, essentially processing input from the front end and the back end.

When receiving front-end input:

  1. Sends the input synchronously to the server
  2. Calling GameSystem immediately applies the input to the local state

When receiving back-end input:

  1. Calculates the current state of authority
    • Roll back to the previous authority state
    • Then input the authority of the application calculation, get the authority state
  2. The local forecast input is applied to the calculation to obtain the forecast state

The concept of reconciliation is not Easy to understand, but it is So Easy to implement

Write the front-end presentation layer

Finally, the implementation of the front-end presentation layer, whose job is to take the current state from the GameManager, and then display it.

For yourself, the state is updated through prediction + reconciliation, without interpolation, so you can directly reset the state in one step, for example:

For others, interpolation is needed to smooth the transition, and in Cocos we can do that by Tween. Just remember, don’t forget to clean up before Tween each time, because network jitter, the last interpolation may not be finished when the new interpolation starts.

In addition, there may be some information that is not reflected in the two frame state changes. A chicken bullet, for example, is created instantly, hits an enemy and then disappears. So in a state comparison of two frames, you just know that the enemy is taking damage, but you can’t see the bullet. If you need to use the bullet’s information to, say, plot a trajectory, there are two ways to think about it:

  1. Record bullet information to state as well, such as only bullets that occurred during the previous frame
  2. Implemented as an event in GameSystem, ephemeral information is transmitted as an event

New arrows in the Demo, for example, are passed to the presentation layer as events. The presentation layer receives the “shoot a new arrow” event and initializes a new arrow component once and for all, its flight animation in the air and so on is entirely the presentation layer’s job, and the arrow does not need to update its state from GameSystem once it is created.

So far, all the work is done ~ TSRPC + Cocos, multiplayer real-time battle is not So Easy? Come and experience it

Demo Experience Address:

  • TSRPC. Cn/fight/index…

Demo source code address:

  • Github.com/k8w/tsrpc-e…
  • Store.cocos.com/app/detail/…

Bonus: Handling latency

When it comes to multiplayer real-time games, the most important thing players care about is “lag”, and this lag is often referred to as “network lag”. We actually have some misunderstandings about it.

Delay does not affect operations

From the above examples, we can draw several important conclusions:

  • When there is no conflict, network latency does not affect operation latency, and prediction + reconciliation can achieve local zero latency operation experience
  • When a conflict occurs, the local state is immediately reset to the latest state, the screen jumps, and only then can the player clearly feel “stuck”.
  • The network delay affects the conflict probability: the greater the network delay, the greater the possibility of conflict

When prediction + reconciliation is used, our previous belief that “the greater the network delay, the greater the operation delay” becomes a misconception.

Even in a MOBA game, you’re playing wild and another player is grinding — there’s no possibility of “conflict” between you. Even if there is a lot of latency on the network, you should both experience the same game as a single player with zero latency! Only when you are in a team battle can there be conflicts such as skill determination due to network lag; And it’s only when conflict occurs that you get an intuitive sense of the delay.

Is the delay as small as possible

The server can broadcast the input from the client immediately or use LockStep to fix the synchronization frequency. In addition to the network, synchronization frequency also affects latency. For example, the server’s logical frame rate synchronizes 10 times per second, which means 100ms latency can occur even within the LAN.

But is network latency really as low as possible? In fact, the small delay also has a side effect: the interpolation is not smooth.

If you move uniformly from point A to point B in 1 second, if the synchronization frequency is exactly 1 time per second, then by interpolation, the other players should see A perfectly uniform movement. But what if the synchronization rate is 60 times per second? In theory you would receive a new state every 16ms, and then update the interpolation animation every 16ms. But just like latency, network jitter is an objective thing. Instead of getting a message every 16ms, you’re likely to get one message every 200ms, or N messages in 20ms. In this way, other players will see the movement as fast and slow, and this uneven animation will give an intuitive sense of stuttering.

So, less delay is not always better, and it’s a tradeoff:

  • Large delay: Smoother interpolation and higher conflict probability
  • Small delay: the interpolation is not smooth, and the conflict probability is smaller

What is the best frequency for delay and synchronization? There is no right answer to this question and it should be decided based on the pros and cons of the actual gameplay needs.

There is a decision under delay

In the case of delay, who should be the judge of skill hit? Let’s look at a simple example.

Scenario Example In an open field, you pick up a sniper rifle and aim it at a moving enemy’s head. Click the mouse, a ballistic flash – you are very sure, hit! However, due to the presence of network latency, the enemy you see is actually 200ms ago in location. From the server’s point of view, the enemy is gone by the time you shoot — you’re empty. So at this point, how should we decide? Let’s take a look at each.

If we choose to go with the judgment of the server, you’re going to be upset. Because the way you see it, if the enemy doesn’t bleed, the other side must be open. In theory, the opposite side should be happy because the server protects him from injury. But the truth is that he has nothing to be happy about, because he has no idea what the server has done for him. He just thinks it’s “real food”.

What if we go with the client’s judgment? Of course you’ll be happy, because the result is exactly what you expected, and you feel that the game is smooth and smooth with no lag, which is awesome. In theory, the other person will be upset because from the server’s point of view, you missed him. But the truth is, he doesn’t really know what happened, he just thinks that you were a good shot and hit him. Despite getting hit, the experience was smooth and predictable for him, with no discomfort.

So it looks like everyone is happy listening to the client, so is it safe? There are exceptions.

Suppose, instead of running in the open, the opposite ran behind a wall. At this point, he thinks he’s safe, but because of network latency, you still think you hit him. Now behind the wall he is still hurt, he must be very upset, either the network card or you open through the wall hanging. Therefore, there is no 100% perfect solution, and if you feel that the probability of such a situation is low and acceptable after weighing the pros and cons, you can choose to use the judgment of the client for a better game experience.

Tips You can also include the game time when the client sends the input, and the server decides who will determine the actual delay. For example, if the delay is less than 200ms, it is determined by the client; otherwise, it is determined by the server.

Concerns about cheating

As mentioned above, in some cases decisions can be made by the client for a better game experience. Is there a risk of cheating and plugins?

Conclusion first, no.

First, to prevent cheating, you must encrypt the transmission. If you’re using plaintext like JSON, no matter how you do it, even with server-side validation, it’s easy to cheat, isn’t it? So the premise is to prevent cheating, transmission layer is encrypted, at least there is a certain crack threshold.

On the basis that the encryption at the transport layer is not cracked, there is no significant difference between the client and the server in determining the security risks. It is only necessary to weigh the advantages and disadvantages at the experience level and choose a more suitable solution. As mentioned earlier, the gameplay logic is universal on the front and back ends, and the client is free to send any input to the server. Therefore, whether the client sends the calculation result to the server, or the client sends the input and the server completes the calculation, the process is the same. You’re worried that the client might send you a cheating calculation, but the client might also send you a cheating input. So the root cause is transmission encryption, not server determination is fine.

(End of text)

Welcome to TypeScript full stack development and stay up to date with TSRPC.