Through years of accumulation of card games, frame synchronization is the most commonly used multiplayer game synchronization scheme for this kind of games. If it is a round game, then state synchronization is the first choice with high probability, but its implementation complexity is not as good as the frame synchronization technology in this kind of game. So this chapter records the frame synchronization scheme and details of several projects I developed.
We unified all floating-point types in the project into fixed points, thus ensuring that our logical operations will not lead to differences in calculation results due to different platforms. The reason why different platforms operate floating-point numbers differently is because the underlying hardware of different manufacturers operates floating-point numbers differently. Although everyone's calculation results are similar, there will always be differences in the last few decimal places. If double is used to calculate, although the precision of double is higher, the last few digits are inevitably inconsistent, but it is already twenty or thirty digits after the decimal point.
It is for this reason that we first use double as the floating-point data type, and then replace it with a fixed point. Because the effective precision of my fixed-point number can only reach 5 decimal places, even if the underlying hardware makes mistakes in floating-point number operation, it will still be forcibly unified by the fixed-point number, without worrying about the problems caused by inconsistent operation with smaller precision.
Many fixed-point schemes can be found on the internet, and the advantages and disadvantages of fixed-point schemes are analyzed in many aspects. I won't go into details here. Online 10000 is used as the magnification of the fixed point, which is the difference between my fixed-point implementation and the online one.
In my fixed point, I adopted a special magnification, that is, 2 16. The biggest advantage of such a magnification is that most operations between fixed points can be enlarged and reduced in the form of bit operations, which is more suitable in performance. In addition, taking the power of 16 of 2 as the magnification, the significant digits of the decimal can be retained to 5 decimal places. Judging from the calculation accuracy, such a range can also meet the needs of our project development. As for the magnification of different projects, it can be adjusted to other powers of 2 as needed.
Using the integer power of 2 will improve the calculation performance in multiplication, division, square root and fixed-point comparison, which is far more efficient than the multiplication and division of 10000. In particular, our root here directly calls the root operation in the C# math library, but the difference is that the number to be root needs to be shifted to the left by 16 bits before root, so that the result obtained by root is a value that meets the fixed-point magnification.
Based on the above points, we will use long as the data storage type inside the fixed point, and the range of numbers represented by the fixed point is consistent with the int type. If a number greater than the maximum value of int is stored by using a fixed number of points, a calculation error is likely to occur due to the shift in the subsequent calculation.
When we implement our own fixed-point scheme, we need to generate trigonometric function query table according to its accuracy. Only the sin lookup table is generated here. When calculating other trigonometric functions, specific values can be obtained according to the sin value.
In the process of realizing the fixed number of points, there are still many contents worthy of reference:
After adopting the frame synchronization scheme, the network scheme is a problem that needs attention.
Under the frame synchronization scheme, both the client and the server need to use the network frequently for data interaction, whether it is synchronization information every frame or synchronization information when data changes. Under the traditional TCP scheme, the transmission mechanism of TCP itself is delayed, and KCP is needed to achieve high real-time performance, but the cost (according to the official statement) is extra bandwidth consumption of 10%-20%.
However, it should be noted that when the packet loss rate rises, KCP will lead to more traffic due to the organization and sending form of its underlying packets, which is not as good as the comprehensive efficiency of TCP. This is a defect in choosing KCP scheme.
In my project, I finally designed two sets of network structures, KCP and TCP. Here, the client needs to collect the network state data (ping value) of the server. When the packet loss rate and delay are not serious (the threshold can be set according to the actual situation of the project), the network adopts KCP;; ; Once the network state exceeds the threshold, the underlying network scheme switches to TCP. The design purpose of this scheme is mainly to solve the problem of excessive KCP traffic when the packet loss rate rises, but the specific situation needs to be determined according to the project type and specific needs.
In the combat logic department, we have talked about the concept of framework, but the framework there is mainly aimed at the fixed execution of logic.
Frame synchronization also needs the concept of frame. Only when the frame rates of all clients and servers are unified can we restrain everyone's behavior to be consistent. Therefore, we need to distinguish between frame synchronization and logical framing. I will use "logical frame" to refer to logical frame and "service frame" to refer to frame synchronization.
The simplest scheme is to unify the frame rates of logical frames and service frames at the beginning of design. The advantage of this is to avoid the matching problem caused by different frame rates. Every time the server drives a frame, the logical frame runs once, which is simple and convenient to realize.
However, the design of business frames is generally based on communication performance, and the natural frame rate will be lower, while the logical frames will be unstable due to different project requirements, with a high probability. Therefore, it is normal that the frame rates of business frames and logical frames do not match, which requires some adapters to meet our needs.
Remember the interface ILogicMgr I mentioned in the first chapter? One of the functions of its derived classes is to provide a "frame rate difference" so that game logic can run smoothly at a frame rate different from that of logical frames. The frame rate difference is based on the speed of the party with the fastest frame rate, and each heartbeat returns the heartbeat result. Frame rate difference has three heartbeat results:
Because our project is a frame locking scheme, the service frame takes priority, so the differential heartbeat flow chart is as follows:
Difference is a key point of frame locking mode, because of its existence, logical frames and service frames can be executed sequentially. Because there is time management inside the differential, the service frame locking function can be realized. This part depends on the current maximum number of frames data synchronized by the server, limited to maximum number of frames. If the network fluctuates, the client logic will be stuck at the corresponding time point in maximum number of frames until it receives the latest maximum number of frames data from the server.
Frame chasing is also done here, that is, if the client finds that there are many frames behind the server, it needs to design an algorithm here to speed up the operation of logic, so that a heartbeat can execute more logical frames. However, it should be noted that you should not run all logical frames at once, which will cause the display effect of the client to be torn.
When talking about frame rate difference, we have explained the concepts of service frame and logical frame, and we have also made it clear that difference needs to be used in the derived class of interface ILogicMgr, so the logical part is naturally integrated here.
When the result of every heartbeat is a logical frame, it is time for us to call the logical heartbeat. In this way, the logical integration under the frame synchronization scheme is completed. Because logical heartbeat is driven in ILogicMgr, you can develop frame tracking and heartbeat time control here as needed.
The frame synchronization scheme I adopted in the project has been described in detail above.
It should be pointed out that under the concept of service framework, our scheme is to take the server as the leading client to achieve the effect of framework locking. This implementation scheme is the simplest and most effective. When we are faced with a project that does not require high real-time performance, this frame locking scheme can simply and effectively meet our needs, and will not cause too many negative effects on the player's feelings.
What if the real-time requirement of the project is high?
Similar to ACT or MOBA games, players need to be able to get feedback on skill release in time, otherwise the delayed skill release will tear the fast-paced game experience. In this case, we need to design new ideas. Before that, we need to clarify the concept of snapshot.
A snapshot is a copy of the current data. We need snapshots, because we will need to save all the data at a certain point in time, and then restore the battlefield data through the saved data at another point in time.
Based on the operational framework, we only need the logical data snapshot of the snapshot, so that even if the logic rolls back the data through the snapshot, the data can be synchronized to the display layer for restoration. ECS architecture itself completely separates data from logic, so the implementation scheme of snapshot and data rollback can refer to Chapter 2, which is not repeated here.
Next we will introduce an improved frame synchronization strategy, which requires data snapshot and rollback.
There is also a frame synchronization strategy, that is, under the concept of service frame, the client time comes before the server time. In this case, all the logic of the client can be regarded as a preview, and the information of the past frame is sent by the server.
Suppose the client is the n+2 th frame and the server sends the n th frame.
The above is the client-first frame synchronization strategy, but there are still several implementation issues to consider:
The above is my record and thinking about frame synchronization, and the next article focuses on the development tools.