Compare commits

...

17 Commits

Author SHA1 Message Date
genxium
e3fe773634 Fixed (refRenderFrameId, snapshotStFrameId) pairing in "markConfirmationIfApplicable". 2022-12-05 21:17:18 +08:00
genxium
17cac19c62 Fix on backend "createInputsBufferSnapshot". 2022-12-05 17:27:44 +08:00
Wing
26bdd41285 Merge pull request #11 from genxium/backend_locking_improvement
Improved backend implementation.
2022-12-05 15:27:30 +08:00
genxium
6bf70463fa Improved backend implementation. 2022-12-05 15:23:56 +08:00
genxium
e3d844abad Improving backend use of room.InputsBufferLock. 2022-12-04 23:37:01 +08:00
genxium
0373665382 Minor updates on ringbuffer and formatting. 2022-12-02 10:20:58 +08:00
genxium
3b0db64792 Updated README. 2022-12-01 15:46:36 +08:00
Wing
dd8b731ade Merge pull request #10 from genxium/force_confirmation_isolation
Improvement on smooth synchronization.
2022-12-01 15:25:57 +08:00
genxium
c4489e0912 Fixed backend downsyncToAll battleState filtering. 2022-12-01 15:23:43 +08:00
genxium
348c889e14 Fixes on resync. 2022-12-01 11:53:37 +08:00
yflu
c6473db561 In progress for fixing recovery upon reconnection. 2022-12-01 00:30:35 +08:00
genxium
e165d49cb1 Further simplified signaling. 2022-11-30 21:51:06 +08:00
genxium
26370dce61 Added necessary locking for backend InputsBuffer. 2022-11-30 16:53:48 +08:00
yflu
f3a12b2aa9 A temp broken commit. 2022-11-30 00:04:52 +08:00
genxium
1f5802ee14 Fixed multiple error handling spots. 2022-11-29 21:32:18 +08:00
yflu
080a384ade Working on reduction of resync received in frontend. 2022-11-29 12:49:49 +08:00
genxium
9469b27348 Updated use of GOPROXY param in Makefile. 2022-11-28 23:49:52 +08:00
19 changed files with 684 additions and 465 deletions

View File

@@ -2,11 +2,11 @@
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
_(the following gif is sped up to ~1.5x for file size reduction, kindly note that around ~11s countdown, the attack animation is resumed from a partial progress)_
_(the following gif is sped up to ~1.33x for file size reduction, kindly note that around ~8s countdown, the attack animation is resumed from a partial progress)_
![gif_demo](./charts/melee_attack_fractional_anim_resume_spedup.gif)
![gif_demo](./charts/smooth_melee_attack_spedup.gif)
Please also checkout [this demo video](https://pan.baidu.com/s/1U1wb7KWyHorZElNWcS5HHA?pwd=30wh) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance.
Please also checkout [this demo video](https://pan.baidu.com/s/172AmIKxbFgGXZzWVqxNUPA?pwd=e2tp) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance.
The video mainly shows the following features.
- The backend receives inputs from frontend peers and broadcasts back for synchronization.

View File

@@ -1,5 +1,6 @@
PROJECTNAME=server.exe
ROOT_DIR=.
GOPROXY=https://mirrors.aliyun.com/goproxy
all: help
gen-constants:
@@ -13,13 +14,13 @@ run-test-and-hotreload:
ServerEnv=TEST CompileDaemon -log-prefix=false -build="go build" -command="./$(PROJECTNAME)"
build:
go build -o $(ROOT_DIR)/$(PROJECTNAME)
GOPROXY=$(GOPROXY) go build -o $(ROOT_DIR)/$(PROJECTNAME)
run-prod: build-prod
./$(PROJECTNAME)
build-prod:
go build -ldflags "-s -w -X main.VERSION=$(shell git rev-parse --short HEAD)-$(shell date "+%Y%m%d-%H:%M:%S")" -o $(ROOT_DIR)/$(PROJECTNAME)
GOPROXY=$(GOPROXY) go build -ldflags "-s -w -X main.VERSION=$(shell git rev-parse --short HEAD)-$(shell date "+%Y%m%d-%H:%M:%S")" -o $(ROOT_DIR)/$(PROJECTNAME)
.PHONY: help

View File

@@ -13,6 +13,7 @@ import (
type PlayerBattleState struct {
ADDED_PENDING_BATTLE_COLLIDER_ACK int32
READDED_PENDING_BATTLE_COLLIDER_ACK int32
READDED_BATTLE_COLLIDER_ACKED int32
ACTIVE int32
DISCONNECTED int32
LOST int32
@@ -26,11 +27,12 @@ func InitPlayerBattleStateIns() {
PlayerBattleStateIns = PlayerBattleState{
ADDED_PENDING_BATTLE_COLLIDER_ACK: 0,
READDED_PENDING_BATTLE_COLLIDER_ACK: 1,
ACTIVE: 2,
DISCONNECTED: 3,
LOST: 4,
EXPELLED_DURING_GAME: 5,
EXPELLED_IN_DISMISSAL: 6,
READDED_BATTLE_COLLIDER_ACKED: 2,
ACTIVE: 3,
DISCONNECTED: 4,
LOST: 5,
EXPELLED_DURING_GAME: 6,
EXPELLED_IN_DISMISSAL: 7,
}
}

View File

@@ -21,6 +21,10 @@ func NewRingBuffer(n int32) *RingBuffer {
}
func (rb *RingBuffer) Put(pItem interface{}) {
for 0 < rb.Cnt && rb.Cnt >= rb.N {
// Make room for the new element
rb.Pop()
}
rb.Eles[rb.Ed] = pItem
rb.EdFrameId++
rb.Cnt++
@@ -69,5 +73,8 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
}
func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} {
if frameId >= rb.EdFrameId || frameId < rb.StFrameId {
return nil
}
return rb.GetByOffset(frameId - rb.StFrameId)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1059,10 +1059,11 @@ type RoomDownsyncFrame struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Players map[int32]*PlayerDownsync `protobuf:"bytes,2,rep,name=players,proto3" json:"players,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
CountdownNanos int64 `protobuf:"varint,3,opt,name=countdownNanos,proto3" json:"countdownNanos,omitempty"`
MeleeBullets []*MeleeBullet `protobuf:"bytes,4,rep,name=meleeBullets,proto3" json:"meleeBullets,omitempty"` // I don't know how to mimic inheritance/composition in protobuf by far, thus using an array for each type of bullet as a compromise
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Players map[int32]*PlayerDownsync `protobuf:"bytes,2,rep,name=players,proto3" json:"players,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
CountdownNanos int64 `protobuf:"varint,3,opt,name=countdownNanos,proto3" json:"countdownNanos,omitempty"`
MeleeBullets []*MeleeBullet `protobuf:"bytes,4,rep,name=meleeBullets,proto3" json:"meleeBullets,omitempty"` // I don't know how to mimic inheritance/composition in protobuf by far, thus using an array for each type of bullet as a compromise
BackendUnconfirmedMask uint64 `protobuf:"varint,5,opt,name=backendUnconfirmedMask,proto3" json:"backendUnconfirmedMask,omitempty"` // Indexed by "joinIndex", same compression concern as stated in InputFrameDownsync
}
func (x *RoomDownsyncFrame) Reset() {
@@ -1125,6 +1126,13 @@ func (x *RoomDownsyncFrame) GetMeleeBullets() []*MeleeBullet {
return nil
}
func (x *RoomDownsyncFrame) GetBackendUnconfirmedMask() uint64 {
if x != nil {
return x.BackendUnconfirmedMask
}
return 0
}
var File_room_downsync_frame_proto protoreflect.FileDescriptor
var file_room_downsync_frame_proto_rawDesc = []byte{
@@ -1377,7 +1385,7 @@ var file_room_downsync_frame_proto_rawDesc = []byte{
0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x4d, 0x65, 0x6c, 0x65,
0x65, 0x42, 0x75, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
0x38, 0x01, 0x22, 0x9a, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x6f, 0x77, 0x6e, 0x73,
0x38, 0x01, 0x22, 0xd2, 0x02, 0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x44, 0x6f, 0x77, 0x6e, 0x73,
0x79, 0x6e, 0x63, 0x46, 0x72, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x40, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x79,
0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x70, 0x72, 0x6f, 0x74,
@@ -1389,14 +1397,18 @@ var file_room_downsync_frame_proto_rawDesc = []byte{
0x6f, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x6d, 0x65, 0x6c, 0x65, 0x65, 0x42, 0x75, 0x6c, 0x6c, 0x65,
0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x73, 0x2e, 0x4d, 0x65, 0x6c, 0x65, 0x65, 0x42, 0x75, 0x6c, 0x6c, 0x65, 0x74, 0x52, 0x0c, 0x6d,
0x65, 0x6c, 0x65, 0x65, 0x42, 0x75, 0x6c, 0x6c, 0x65, 0x74, 0x73, 0x1a, 0x52, 0x0a, 0x0c, 0x50,
0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x44, 0x6f, 0x77, 0x6e,
0x73, 0x79, 0x6e, 0x63, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42,
0x13, 0x5a, 0x11, 0x62, 0x61, 0x74, 0x74, 0x6c, 0x65, 0x5f, 0x73, 0x72, 0x76, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x6c, 0x65, 0x65, 0x42, 0x75, 0x6c, 0x6c, 0x65, 0x74, 0x73, 0x12, 0x36, 0x0a, 0x16, 0x62,
0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x55, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65,
0x64, 0x4d, 0x61, 0x73, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x16, 0x62, 0x61, 0x63,
0x6b, 0x65, 0x6e, 0x64, 0x55, 0x6e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x65, 0x64, 0x4d,
0x61, 0x73, 0x6b, 0x1a, 0x52, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x45, 0x6e,
0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x50, 0x6c,
0x61, 0x79, 0x65, 0x72, 0x44, 0x6f, 0x77, 0x6e, 0x73, 0x79, 0x6e, 0x63, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x13, 0x5a, 0x11, 0x62, 0x61, 0x74, 0x74, 0x6c,
0x65, 0x5f, 0x73, 0x72, 0x76, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@@ -365,7 +365,7 @@ func Serve(c *gin.Context) {
return nil
}
// Tries to receive from client-side in a non-blocking manner.
// TODO: Is there any potential edge-trigger improvement like the epoll approach mentioned above for the following statement? See discussion in https://github.com/gorilla/websocket/issues/122
_, bytes, err := conn.ReadMessage()
if nil != err {
Logger.Error("About to `signalToCloseConnOfThisPlayer`", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Error(err))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

View File

@@ -5,7 +5,7 @@ all: help
GOPROXY=https://mirrors.aliyun.com/goproxy
build:
go build -o $(ROOT_DIR)/$(PROJECTNAME)
GOPROXY=$(GOPROXY) go build -o $(ROOT_DIR)/$(PROJECTNAME)
run: build
./$(PROJECTNAME)

View File

@@ -134,4 +134,5 @@ message RoomDownsyncFrame {
map<int32, PlayerDownsync> players = 2;
int64 countdownNanos = 3;
repeated MeleeBullet meleeBullets = 4; // I don't know how to mimic inheritance/composition in protobuf by far, thus using an array for each type of bullet as a compromise
uint64 backendUnconfirmedMask = 5; // Indexed by "joinIndex", same compression concern as stated in InputFrameDownsync
}

View File

@@ -440,7 +440,7 @@
"array": [
0,
0,
210.5241291124452,
210.43753679824133,
0,
0,
0,

View File

@@ -454,7 +454,7 @@
"array": [
0,
0,
210.5241291124452,
216.50635094610968,
0,
0,
0,

View File

@@ -82,21 +82,21 @@ cc.Class({
// It turns out that "prevRdfPlayer.characterState" is not useful in this function :)
if (newAnimName == playingAnimName && window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
// No need to interrupt
// console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`);
// console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, to: ${JSON.stringify(rdfPlayer)}`);
return;
}
if (this.animComp instanceof dragonBones.ArmatureDisplay) {
this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl);
this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName);
} else {
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName);
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName);
}
},
_interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl) {
if (ATK_CHARACTER_STATE.Idle1[0] == newCharacterState || ATK_CHARACTER_STATE.Walking[0] == newCharacterState) {
_interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName) {
if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
// No "framesToRecover"
// console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${playAnimation}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`);
//console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`);
underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1);
} else {
const animationData = underlyingAnimationCtrl._animations[newAnimName];
@@ -109,10 +109,10 @@ cc.Class({
}
},
_interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName) {
_interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName) {
if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
// No "framesToRecover"
// console.warn(`JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${playAnimation}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, , to: ${JSON.stringify(rdfPlayer)}`);
//console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`);
this.animComp.play(newAnimName, 0);
return;
}

View File

@@ -104,32 +104,6 @@ cc.Class({
return (0 == inputFrameId % 10);
},
dumpToRenderCache: function(rdf) {
const self = this;
const minToKeepRenderFrameId = self.lastAllConfirmedRenderFrameId - 1; // Keep at least 1 prev render frame for anim triggering
while (0 < self.recentRenderCache.cnt && self.recentRenderCache.stFrameId < minToKeepRenderFrameId) {
self.recentRenderCache.pop();
}
const ret = self.recentRenderCache.setByFrameId(rdf, rdf.id);
return ret;
},
dumpToInputCache: function(inputFrameDownsync) {
const self = this;
let minToKeepInputFrameId = self._convertToInputFrameId(self.lastAllConfirmedRenderFrameId, self.inputDelayFrames) - self.spAtkLookupFrames; // [WARNING] This could be different from "self.lastAllConfirmedInputFrameId". We'd like to keep the corresponding delayedInputFrame for "self.lastAllConfirmedRenderFrameId" such that a rollback could place "self.chaserRenderFrameId = self.lastAllConfirmedRenderFrameId" for the worst case incorrect prediction.
if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) {
minToKeepInputFrameId = self.lastAllConfirmedInputFrameId;
}
while (0 < self.recentInputCache.cnt && self.recentInputCache.stFrameId < minToKeepInputFrameId) {
self.recentInputCache.pop();
}
const ret = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId);
if (-1 < self.lastAllConfirmedInputFrameId && self.recentInputCache.stFrameId > self.lastAllConfirmedInputFrameId) {
console.error("Invalid input cache dumped! lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
}
return ret;
},
_convertToInputFrameId(renderFrameId, inputDelayFrames) {
if (renderFrameId < inputDelayFrames) return 0;
return ((renderFrameId - inputDelayFrames) >> this.inputScaleFrames);
@@ -153,28 +127,35 @@ cc.Class({
null == self.ctrl ||
null == self.selfPlayerInfo
) {
return [null, null];
throw `noDelayInputFrameId=${inputFrameId} couldn't be generated: recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
let previousSelfInput = null,
currSelfInput = null;
const joinIndex = self.selfPlayerInfo.joinIndex;
const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId);
const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]);
// [WARNING] The while-loop here handles a situation where the "resync rdf & accompaniedInputFrameDownsyncBatch" mismatched and we have to predict some "gap-inputFrames"!
while (self.recentInputCache.edFrameId <= inputFrameId) {
// TODO: find some kind of synchronization mechanism against "onInputFrameDownsyncBatch"!
const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId - 1);
previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]);
// If "forceConfirmation" is active on backend, we shouldn't override the already downsynced "inputFrameDownsync"s.
const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId);
if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) {
return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]];
// If "forceConfirmation" is active on backend, there's a chance that the already downsynced "inputFrameDownsync"s are ahead of a locally generating inputFrameId, in this case we respect the downsynced one.
const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId);
if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) {
console.log(`noDelayInputFrameId=${inputFrameId} already exists in recentInputCache and is all-confirmed: recentInputCache=${self._stringifyRecentInputCache(false)}`);
return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]];
}
const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice());
currSelfInput = self.ctrl.getEncodedInput();
prefabbedInputList[(joinIndex - 1)] = currSelfInput;
const prefabbedInputFrameDownsync = window.pb.protos.InputFrameDownsync.create({
inputFrameId: self.recentInputCache.edFrameId,
inputList: prefabbedInputList,
confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1))
});
self.recentInputCache.put(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
}
const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice());
const currSelfInput = self.ctrl.getEncodedInput();
prefabbedInputList[(joinIndex - 1)] = currSelfInput;
const prefabbedInputFrameDownsync = {
inputFrameId: inputFrameId,
inputList: prefabbedInputList,
confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1))
};
self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
return [previousSelfInput, currSelfInput];
},
@@ -203,7 +184,7 @@ cc.Class({
for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.getByFrameId(i);
if (null == inputFrameDownsync) {
console.error("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, ": latestLocalInputFrameId=", latestLocalInputFrameId, ", recentInputCache=", self._stringifyRecentInputCache(false));
console.error(`sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=i: latestLocalInputFrameId=${latestLocalInputFrameId}, recentInputCache=${self._stringifyRecentInputCache(false)}`);
} else {
const inputFrameUpsync = {
inputFrameId: i,
@@ -225,6 +206,9 @@ cc.Class({
}).finish();
window.sendSafely(reqData);
self.lastUpsyncInputFrameId = latestLocalInputFrameId;
if (self.lastUpsyncInputFrameId >= self.recentInputCache.edFrameId) {
throw `noDelayInputFrameId=${self.lastUpsyncInputFrameId} == latestLocalInputFrameId=${latestLocalInputFrameId} seems not properly dumped #2: recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
},
onEnable() {
@@ -312,7 +296,7 @@ cc.Class({
self.recentRenderCache = new RingBuffer(self.renderCacheSize);
self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others".
self.recentInputCache = new RingBuffer((self.renderCacheSize >> 2) + 1);
self.recentInputCache = new RingBuffer((self.renderCacheSize >> 1) + 1);
self.collisionSys = new collisions.Collisions();
@@ -320,6 +304,8 @@ cc.Class({
self.collisionBulletIndexPrefix = (1 << 15); // For tracking the movements of bullets
self.collisionSysMap = new Map();
console.log(`collisionSys & collisionSysMap reset`);
self.transitToState(ALL_MAP_STATES.VISUAL);
self.battleState = ALL_BATTLE_STATES.WAITING;
@@ -413,7 +399,10 @@ cc.Class({
/** Init required prefab ended. */
window.handleBattleColliderInfo = function(parsedBattleColliderInfo) {
console.log(`Received parsedBattleColliderInfo via ws`);
// TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE".
Object.assign(self, parsedBattleColliderInfo);
self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis;
const tiledMapIns = self.node.getComponent(cc.TiledMap);
@@ -458,7 +447,7 @@ cc.Class({
const x0 = boundaryObj.anchor.x,
y0 = boundaryObj.anchor.y;
const newBarrier = self.collisionSys.createPolygon(x0, y0, Array.from(boundaryObj, p => {
const newBarrierCollider = self.collisionSys.createPolygon(x0, y0, Array.from(boundaryObj, p => {
return [p.x, p.y];
}));
@@ -493,10 +482,10 @@ cc.Class({
}
}
// console.log("Created barrier: ", newBarrier);
++barrierIdCounter;
const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter);
self.collisionSysMap.set(collisionBarrierIndex, newBarrier);
self.collisionSysMap.set(collisionBarrierIndex, newBarrierCollider);
// console.log(`Created new barrier collider: ${collisionBarrierIndex}`);
}
self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
@@ -509,6 +498,7 @@ cc.Class({
act: window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK,
}).finish();
window.sendSafely(reqData);
console.log(`Sent UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK via ws`);
});
};
@@ -571,17 +561,21 @@ cc.Class({
this._inputControlEnabled = false;
},
onRoomDownsyncFrame(rdf) {
onRoomDownsyncFrame(rdf, accompaniedInputFrameDownsyncBatch) {
// This function is also applicable to "re-joining".
const self = window.mapIns;
if (rdf.id < self.lastAllConfirmedRenderFrameId) {
return window.RING_BUFF_FAILED_TO_SET;
if (!self.recentRenderCache) {
return;
}
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
return;
}
const shouldForceDumping1 = (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id);
const shouldForceDumping2 = (rdf.id > self.renderFrameId + self.renderFrameIdLagTolerance);
const dumpRenderCacheRet = self.dumpToRenderCache(rdf);
const [dumpRenderCacheRet, oldStRenderFrameId, oldEdRenderFrameId] = (shouldForceDumping1 || shouldForceDumping2) ? self.recentRenderCache.setByFrameId(rdf, rdf.id) : [window.RING_BUFF_CONSECUTIVE_SET, null, null];
if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) {
console.error("Something is wrong while setting the RingBuffer by frameId!");
return dumpRenderCacheRet;
throw `Failed to dump render cache#1 (maybe recentRenderCache too small)! rdf.id=${rdf.id}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START < rdf.id && window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet) {
/*
@@ -592,13 +586,7 @@ cc.Class({
return dumpRenderCacheRet;
}
// The logic below applies to ( || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) {
console.log('On battle started! renderFrameId=', rdf.id);
} else {
console.log('On battle resynced! renderFrameId=', rdf.id);
}
// The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
const players = rdf.players;
self._initPlayerRichInfoDict(players);
@@ -612,11 +600,24 @@ cc.Class({
if (null == self.renderFrameId || self.renderFrameId <= rdf.id) {
// In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdf.id", but here we double check and log the anomaly
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) {
console.log('On battle started! renderFrameId=', rdf.id);
} else {
self.hideFindingPlayersGUI(rdf);
self.onInputFrameDownsyncBatch(accompaniedInputFrameDownsyncBatch); // Important to do this step before setting IN_BATTLE
console.warn(`Got resync@localRenderFrameId=${self.renderFrameId} -> rdf.id=${rdf.id} & rdf.backendUnconfirmedMask=${rdf.backendUnconfirmedMask}, @lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${self.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}`);
}
self.renderFrameId = rdf.id;
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId".
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id;
const candidateLastAllConfirmedInputFrame = self._convertToInputFrameId(rdf.id - 1, self.inputDelayFrames);
if (self.lastAllConfirmedInputFrame < candidateLastAllConfirmedInputFrame) {
self.lastAllConfirmedInputFrame = candidateLastAllConfirmedInputFrame;
}
const canvasNode = self.canvasNode;
self.ctrl = canvasNode.getComponent("TouchEventsManager");
@@ -650,9 +651,12 @@ cc.Class({
},
onInputFrameDownsyncBatch(batch) {
// TODO: find some kind of synchronization mechanism against "_generateInputFrameUpsync"!
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState
&& ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) {
if (!self.recentInputCache) {
return;
}
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
return;
}
@@ -663,6 +667,7 @@ cc.Class({
if (inputFrameDownsyncId < self.lastAllConfirmedInputFrameId) {
continue;
}
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId);
if (null != localInputFrame
&&
@@ -672,10 +677,12 @@ cc.Class({
) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1;
self.dumpToInputCache(inputFrameDownsync);
const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId);
if (window.RING_BUFF_FAILED_TO_SET == ret) {
throw `Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
}
if (null == firstPredictedYetIncorrectInputFrameId) return;
@@ -716,7 +723,7 @@ cc.Class({
logBattleStats() {
const self = this;
let s = [];
s.push(`Battle stats: renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}`);
s.push(`Battle stats: renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`);
for (let i = self.recentInputCache.stFrameId; i < self.recentInputCache.edFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.getByFrameId(i);
@@ -761,7 +768,8 @@ cc.Class({
const [wx, wy] = self.virtualGridToWorldPos(vx, vy);
newPlayerNode.setPosition(wx, wy);
playerScriptIns.mapNode = self.node;
const colliderWidth = playerDownsyncInfo.colliderRadius * 2, colliderHeight = playerDownsyncInfo.colliderRadius * 3;
const colliderWidth = playerDownsyncInfo.colliderRadius * 2,
colliderHeight = playerDownsyncInfo.colliderRadius * 3;
const [x0, y0] = self.virtualGridToPolygonColliderAnchorPos(vx, vy, colliderWidth, colliderHeight),
pts = [[0, 0], [colliderWidth, 0], [colliderWidth, colliderHeight], [0, colliderHeight]];
@@ -770,6 +778,8 @@ cc.Class({
newPlayerCollider.data = playerDownsyncInfo;
self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider);
console.log(`Created new player collider: joinIndex=${joinIndex}, colliderRadius=${playerDownsyncInfo.colliderRadius}`);
safelyAddChild(self.node, newPlayerNode);
setLocalZOrder(newPlayerNode, 5);
@@ -783,7 +793,8 @@ cc.Class({
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) {
const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt;
if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) {
if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) {
// [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience!
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
return;
}
@@ -822,17 +833,13 @@ cc.Class({
*/
// [WARNING] Don't try to get "prevRdf(i.e. renderFrameId == latest-1)" by "self.recentRenderCache.getByFrameId(...)" here, as the cache might have been updated by asynchronous "onRoomDownsyncFrame(...)" calls!
self.applyRoomDownsyncFrameDynamics(rdf, prevRdf);
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
let t3 = performance.now();
} catch (err) {
console.error("Error during Map.update", err);
self.onBattleStopped(); // TODO: Popup to ask player to refresh browser
} finally {
// Update countdown
self.countdownNanos = self.battleDurationNanos - self.renderFrameId * self.rollbackEstimatedDtNanos;
if (self.countdownNanos <= 0) {
self.onBattleStopped(self.playerRichInfoDict);
return;
}
const countdownSeconds = parseInt(self.countdownNanos / 1000000000);
if (isNaN(countdownSeconds)) {
console.warn(`countdownSeconds is NaN for countdownNanos == ${self.countdownNanos}.`);
@@ -840,8 +847,6 @@ cc.Class({
if (null != self.countdownLabel) {
self.countdownLabel.string = countdownSeconds;
}
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
}
}
},
@@ -921,9 +926,6 @@ cc.Class({
const self = this;
if (null == self.findingPlayerNode.parent) return;
self.findingPlayerNode.parent.removeChild(self.findingPlayerNode);
if (null != rdf) {
self._initPlayerRichInfoDict(rdf.players);
}
},
onBattleReadyToStart(rdf) {
@@ -967,15 +969,21 @@ cc.Class({
playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed);
playerRichInfo.scriptIns.updateCharacterAnim(immediatePlayerInfo, prevRdfPlayer, false);
}
// Update countdown
self.countdownNanos = self.battleDurationNanos - self.renderFrameId * self.rollbackEstimatedDtNanos;
if (self.countdownNanos <= 0) {
self.onBattleStopped(self.playerRichInfoDict);
}
},
getCachedInputFrameDownsyncWithPrediction(inputFrameId) {
const self = this;
let inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId);
if (null != inputFrameDownsync && -1 != self.lastAllConfirmedInputFrameId && inputFrameId > self.lastAllConfirmedInputFrameId) {
const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId);
const inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId);
const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId);
if (null != inputFrameDownsync && null != lastAllConfirmedInputFrame && inputFrameId > self.lastAllConfirmedInputFrameId) {
for (let i = 0; i < inputFrameDownsync.inputList.length; ++i) {
if (i == self.selfPlayerInfo.joinIndex - 1) continue;
if (i == (self.selfPlayerInfo.joinIndex - 1)) continue;
inputFrameDownsync.inputList[i] = (lastAllConfirmedInputFrame.inputList[i] & 15); // Don't predict attack input!
}
}
@@ -1007,11 +1015,7 @@ cc.Class({
};
}
const toRet = {
id: currRenderFrame.id + 1,
players: nextRenderFramePlayers,
meleeBullets: []
};
const nextRenderFrameMeleeBullets = [];
const bulletPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
@@ -1104,7 +1108,7 @@ cc.Class({
collisionSysMap.delete(collisionBulletIndex);
}
if (removedBulletsAtCurrFrame.has(collisionBulletIndex)) continue;
toRet.meleeBullets.push(meleeBullet);
nextRenderFrameMeleeBullets.push(meleeBullet);
}
// Process player inputs
@@ -1145,7 +1149,7 @@ cc.Class({
punch.offenderJoinIndex = joinIndex;
punch.offenderPlayerId = playerId;
punch.originatedRenderFrameId = currRenderFrame.id;
toRet.meleeBullets.push(punch);
nextRenderFrameMeleeBullets.push(punch);
// console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}: ${self._stringifyRecentInputCache(true)}`);
// console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
@@ -1198,7 +1202,11 @@ cc.Class({
}
return toRet;
return window.pb.protos.RoomDownsyncFrame.create({
id: currRenderFrame.id + 1,
players: nextRenderFramePlayers,
meleeBullets: nextRenderFrameMeleeBullets,
});
},
rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap, isChasing) {
@@ -1206,31 +1214,24 @@ cc.Class({
This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd" if not interruptted.
*/
const self = this;
let prevLatestRdf = null;
let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame"
if (null == latestRdf) {
console.error(`Couldn't find renderFrameId=${renderFrameIdSt}, to rollback, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`);
return [prevLatestRdf, latestRdf];
}
let i = renderFrameIdSt,
prevLatestRdf = null,
latestRdf = null;
if (renderFrameIdSt >= renderFrameIdEd) {
return [prevLatestRdf, latestRdf];
}
for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) {
const currRenderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"!
if (null == currRenderFrame) {
do {
latestRdf = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"!
if (null == latestRdf) {
console.warn(`Couldn't find renderFrame for i=${i} to rollback, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, might've been interruptted by onRoomDownsyncFrame`);
return [prevLatestRdf, latestRdf];
}
const j = self._convertToInputFrameId(i, self.inputDelayFrames);
const delayedInputFrame = self.getCachedInputFrameDownsyncWithPrediction(j);
if (null == delayedInputFrame) {
console.warn(`Failed to get cached delayedInputFrame for i=${i}, j=${j}, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}`);
return [prevLatestRdf, latestRdf];
// Shouldn't happen!
throw `Failed to get cached delayedInputFrame for i=${i}, j=${j}, renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
prevLatestRdf = latestRdf;
latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap);
latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, prevLatestRdf, collisionSys, collisionSysMap);
if (
self._allConfirmed(delayedInputFrame.confirmedList)
&&
@@ -1248,8 +1249,9 @@ cc.Class({
// Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
self.chaserRenderFrameId = latestRdf.id;
}
self.dumpToRenderCache(latestRdf);
}
self.recentRenderCache.setByFrameId(latestRdf, latestRdf.id);
++i;
} while (i < renderFrameIdEd);
return [prevLatestRdf, latestRdf];
},

View File

@@ -33,6 +33,7 @@ cc.Class({
self.inputScaleFrames = 2;
self.inputFrameUpsyncDelayTolerance = 2;
self.renderCacheSize = 1024;
self.rollbackEstimatedDt = 0.016667;
self.rollbackEstimatedDtMillis = 16.667;
self.rollbackEstimatedDtNanos = 16666666;

View File

@@ -13,6 +13,10 @@ var RingBuffer = function(capacity) {
};
RingBuffer.prototype.put = function(item) {
while (0 < this.cnt && this.cnt >= this.n) {
// Make room for the new element
this.pop();
}
this.eles[this.ed] = item
this.edFrameId++;
this.cnt++;
@@ -61,40 +65,41 @@ RingBuffer.prototype.getArrIdxByOffset = function(offsetFromSt) {
};
RingBuffer.prototype.getByFrameId = function(frameId) {
if (frameId >= this.edFrameId || frameId < this.stFrameId) return null;
const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
return (null == arrIdx ? null : this.eles[arrIdx]);
};
// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly.
RingBuffer.prototype.setByFrameId = function(item, frameId) {
if (frameId < this.stFrameId) {
console.error("Invalid putByFrameId#1: stFrameId=", this.stFrameId, ", edFrameId=", this.edFrameId, ", incoming item=", item);
return window.RING_BUFF_FAILED_TO_SET;
const oldStFrameId = this.stFrameId,
oldEdFrameId = this.edFrameId;
if (frameId < oldStFrameId) {
return [window.RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId];
}
const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
if (null != arrIdx) {
this.eles[arrIdx] = item;
return window.RING_BUFF_CONSECUTIVE_SET;
// By now "this.stFrameId <= frameId"
if (oldEdFrameId > frameId) {
const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
if (null != arrIdx) {
this.eles[arrIdx] = item;
return [window.RING_BUFF_CONSECUTIVE_SET, oldStFrameId, oldEdFrameId];
}
}
// When "null == arrIdx", should it still be deemed consecutive if "frameId == edFrameId" prior to the reset?
// By now "this.edFrameId <= frameId"
let ret = window.RING_BUFF_CONSECUTIVE_SET;
if (this.edFrameId < frameId) {
if (oldEdFrameId < frameId) {
this.st = this.ed = 0;
this.stFrameId = this.edFrameId = frameId;
this.cnt = 0;
ret = window.RING_BUFF_NON_CONSECUTIVE_SET;
}
this.eles[this.ed] = item
this.edFrameId++;
this.cnt++;
this.ed++;
if (this.ed >= this.n) {
this.ed -= this.n; // Deliberately not using "%" operator for performance concern
}
// By now "this.edFrameId == frameId"
this.put(item);
return ret;
return [ret, oldStFrameId, oldEdFrameId];
};
module.exports = RingBuffer;

View File

@@ -5,7 +5,6 @@ window.UPSYNC_MSG_ACT_PLAYER_CMD = 2;
window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = 3;
window.DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED = -98;
window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED = -97;
window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = -1;
window.DOWNSYNC_MSG_ACT_BATTLE_START = 0;
window.DOWNSYNC_MSG_ACT_HB_REQ = 1;
@@ -135,7 +134,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
clientSession.onopen = function(evt) {
console.log("The WS clientSession is opened. clientSession.id=", clientSession.id);
console.log("The WS clientSession is opened.");
window.clientSession = clientSession;
if (null == onopenCb) return;
onopenCb();
@@ -147,17 +146,14 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
}
try {
const resp = window.pb.protos.WsResp.decode(new Uint8Array(evt.data));
// console.log(`Got non-empty onmessage decoded: resp.act=${resp.act}`);
switch (resp.act) {
case window.DOWNSYNC_MSG_ACT_HB_REQ:
window.handleHbRequirements(resp); // 获取boundRoomId并存储到localStorage
window.handleHbRequirements(resp);
break;
case window.DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED:
mapIns.onPlayerAdded(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED:
// Deliberately left blank for now
mapIns.hideFindingPlayersGUI(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START:
mapIns.onBattleReadyToStart(resp.rdf);
break;
@@ -172,16 +168,10 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
break;
case window.DOWNSYNC_MSG_ACT_FORCED_RESYNC:
if (null == resp.inputFrameDownsyncBatch || 0 >= resp.inputFrameDownsyncBatch.length) {
console.error(`Got empty inputFrameDownsyncBatch upon resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, the incoming resp=
${JSON.stringify(resp, null, 2)}`);
console.error(`Got empty inputFrameDownsyncBatch upon resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, the incoming resp=${JSON.stringify(resp, null, 2)}`);
return;
}
const inputFrameIdConsecutive = (resp.inputFrameDownsyncBatch[0].inputFrameId == mapIns.lastAllConfirmedInputFrameId + 1);
const renderFrameIdConsecutive = (resp.rdf.id <= mapIns.renderFrameId + mapIns.renderFrameIdLagTolerance);
console.warn(`Got resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, inputFrameIdConsecutive=${inputFrameIdConsecutive}, renderFrameIdConsecutive=${renderFrameIdConsecutive}`);
// The following order of execution is important
mapIns.onRoomDownsyncFrame(resp.rdf);
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
mapIns.onRoomDownsyncFrame(resp.rdf, resp.inputFrameDownsyncBatch);
break;
default:
break;

View File

@@ -5082,6 +5082,7 @@ $root.protos = (function() {
* @property {Object.<string,protos.PlayerDownsync>|null} [players] RoomDownsyncFrame players
* @property {number|Long|null} [countdownNanos] RoomDownsyncFrame countdownNanos
* @property {Array.<protos.MeleeBullet>|null} [meleeBullets] RoomDownsyncFrame meleeBullets
* @property {number|Long|null} [backendUnconfirmedMask] RoomDownsyncFrame backendUnconfirmedMask
*/
/**
@@ -5133,6 +5134,14 @@ $root.protos = (function() {
*/
RoomDownsyncFrame.prototype.meleeBullets = $util.emptyArray;
/**
* RoomDownsyncFrame backendUnconfirmedMask.
* @member {number|Long} backendUnconfirmedMask
* @memberof protos.RoomDownsyncFrame
* @instance
*/
RoomDownsyncFrame.prototype.backendUnconfirmedMask = $util.Long ? $util.Long.fromBits(0,0,true) : 0;
/**
* Creates a new RoomDownsyncFrame instance using the specified properties.
* @function create
@@ -5169,6 +5178,8 @@ $root.protos = (function() {
if (message.meleeBullets != null && message.meleeBullets.length)
for (var i = 0; i < message.meleeBullets.length; ++i)
$root.protos.MeleeBullet.encode(message.meleeBullets[i], writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim();
if (message.backendUnconfirmedMask != null && Object.hasOwnProperty.call(message, "backendUnconfirmedMask"))
writer.uint32(/* id 5, wireType 0 =*/40).uint64(message.backendUnconfirmedMask);
return writer;
};
@@ -5240,6 +5251,10 @@ $root.protos = (function() {
message.meleeBullets.push($root.protos.MeleeBullet.decode(reader, reader.uint32()));
break;
}
case 5: {
message.backendUnconfirmedMask = reader.uint64();
break;
}
default:
reader.skipType(tag & 7);
break;
@@ -5304,6 +5319,9 @@ $root.protos = (function() {
return "meleeBullets." + error;
}
}
if (message.backendUnconfirmedMask != null && message.hasOwnProperty("backendUnconfirmedMask"))
if (!$util.isInteger(message.backendUnconfirmedMask) && !(message.backendUnconfirmedMask && $util.isInteger(message.backendUnconfirmedMask.low) && $util.isInteger(message.backendUnconfirmedMask.high)))
return "backendUnconfirmedMask: integer|Long expected";
return null;
};
@@ -5350,6 +5368,15 @@ $root.protos = (function() {
message.meleeBullets[i] = $root.protos.MeleeBullet.fromObject(object.meleeBullets[i]);
}
}
if (object.backendUnconfirmedMask != null)
if ($util.Long)
(message.backendUnconfirmedMask = $util.Long.fromValue(object.backendUnconfirmedMask)).unsigned = true;
else if (typeof object.backendUnconfirmedMask === "string")
message.backendUnconfirmedMask = parseInt(object.backendUnconfirmedMask, 10);
else if (typeof object.backendUnconfirmedMask === "number")
message.backendUnconfirmedMask = object.backendUnconfirmedMask;
else if (typeof object.backendUnconfirmedMask === "object")
message.backendUnconfirmedMask = new $util.LongBits(object.backendUnconfirmedMask.low >>> 0, object.backendUnconfirmedMask.high >>> 0).toNumber(true);
return message;
};
@@ -5377,6 +5404,11 @@ $root.protos = (function() {
object.countdownNanos = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long;
} else
object.countdownNanos = options.longs === String ? "0" : 0;
if ($util.Long) {
var long = new $util.Long(0, 0, true);
object.backendUnconfirmedMask = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long;
} else
object.backendUnconfirmedMask = options.longs === String ? "0" : 0;
}
if (message.id != null && message.hasOwnProperty("id"))
object.id = message.id;
@@ -5396,6 +5428,11 @@ $root.protos = (function() {
for (var j = 0; j < message.meleeBullets.length; ++j)
object.meleeBullets[j] = $root.protos.MeleeBullet.toObject(message.meleeBullets[j], options);
}
if (message.backendUnconfirmedMask != null && message.hasOwnProperty("backendUnconfirmedMask"))
if (typeof message.backendUnconfirmedMask === "number")
object.backendUnconfirmedMask = options.longs === String ? String(message.backendUnconfirmedMask) : message.backendUnconfirmedMask;
else
object.backendUnconfirmedMask = options.longs === String ? $util.Long.prototype.toString.call(message.backendUnconfirmedMask) : options.longs === Number ? new $util.LongBits(message.backendUnconfirmedMask.low >>> 0, message.backendUnconfirmedMask.high >>> 0).toNumber(true) : message.backendUnconfirmedMask;
return object;
};