mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-18 04:56:44 +00:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e3fe773634 | ||
|
17cac19c62 | ||
|
26bdd41285 | ||
|
6bf70463fa | ||
|
e3d844abad | ||
|
0373665382 | ||
|
3b0db64792 | ||
|
dd8b731ade | ||
|
c4489e0912 | ||
|
348c889e14 | ||
|
c6473db561 | ||
|
e165d49cb1 | ||
|
26370dce61 | ||
|
f3a12b2aa9 | ||
|
1f5802ee14 | ||
|
080a384ade | ||
|
9469b27348 |
@@ -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)_
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
@@ -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 (
|
||||
|
@@ -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 |
BIN
charts/smooth_melee_attack_spedup.gif
Normal file
BIN
charts/smooth_melee_attack_spedup.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 MiB |
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -440,7 +440,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
210.5241291124452,
|
||||
210.43753679824133,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@@ -454,7 +454,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
210.5241291124452,
|
||||
216.50635094610968,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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];
|
||||
},
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user