mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-18 13:06:29 +00:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ec2a21dbe7 | ||
|
dc6402c2b7 | ||
|
8038b393e0 |
@@ -3,9 +3,12 @@
|
|||||||
This project is a demo for a websocket-based input synchronization method inspired by [GGPO](https://www.ggpo.net/).
|
This project is a demo for a websocket-based input synchronization method inspired by [GGPO](https://www.ggpo.net/).
|
||||||

|

|
||||||
|
|
||||||
Please checkout [this demo video](https://pan.baidu.com/s/1aM6e8IWaJszFCYAsRjt19g?pwd=z02c) to see whether the source codes are doing what you expect for synchronization.
|
Please checkout [this demo video](https://pan.baidu.com/s/123LlWcT9X-wbcYybqYnvmA?pwd=qrlw) to see whether the source codes are doing what you expect for synchronization.
|
||||||
|
|
||||||
The video mainly shows the following feature (yet I'm not surprised if they're not obvious): when a player didn't have its input arrived at the backend in time (e.g. due to local lag, network delay or reconnection), backend forces confirmation of a prediction of its own and sends the confirmed input together w/ a reference render frame to that player.
|
The video mainly shows the following features.
|
||||||
|
- The backend receives inputs from frontend peers and [by a GGPO-alike manner](https://github.com/pond3r/ggpo/blob/master/doc/README.md) broadcasts back for synchronization.
|
||||||
|
- The game is recovered for a player upon reconnection.
|
||||||
|
- Both backend(Golang) and frontend(JavaScript) execute collision detection and handle collision contacts by the same algorithm. The backend dynamics can be toggled off by [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.5.2/battle_srv/models/room.go#L813), but **when turned off the game couldn't support recovery upon reconnection**.
|
||||||
|
|
||||||
# 1. Building & running
|
# 1. Building & running
|
||||||
|
|
||||||
|
@@ -2,12 +2,17 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
. "dnmshared"
|
. "dnmshared"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/solarlune/resolv"
|
"github.com/solarlune/resolv"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
. "server/common"
|
. "server/common"
|
||||||
"server/common/utils"
|
"server/common/utils"
|
||||||
pb "server/pb_output"
|
pb "server/pb_output"
|
||||||
@@ -15,11 +20,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"encoding/xml"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -459,9 +459,9 @@ func (pR *Room) StartBattle() {
|
|||||||
pR.prefabInputFrameDownsync(noDelayInputFrameId)
|
pR.prefabInputFrameDownsync(noDelayInputFrameId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force setting all-confirmed of buffered inputFrames periodically
|
|
||||||
unconfirmedMask := uint64(0)
|
unconfirmedMask := uint64(0)
|
||||||
if pR.BackendDynamicsEnabled {
|
if pR.BackendDynamicsEnabled {
|
||||||
|
// Force setting all-confirmed of buffered inputFrames periodically
|
||||||
unconfirmedMask = pR.forceConfirmationIfApplicable()
|
unconfirmedMask = pR.forceConfirmationIfApplicable()
|
||||||
} else {
|
} else {
|
||||||
pR.markConfirmationIfApplicable()
|
pR.markConfirmationIfApplicable()
|
||||||
@@ -474,7 +474,7 @@ func (pR *Room) StartBattle() {
|
|||||||
|
|
||||||
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more.
|
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more.
|
||||||
|
|
||||||
Hence even upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId".
|
Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed.
|
||||||
*/
|
*/
|
||||||
refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1
|
refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1
|
||||||
if refRenderFrameId > pR.RenderFrameId {
|
if refRenderFrameId > pR.RenderFrameId {
|
||||||
@@ -539,7 +539,7 @@ func (pR *Room) StartBattle() {
|
|||||||
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"!
|
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"!
|
||||||
|
|
||||||
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId {
|
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId {
|
||||||
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, candidateToSendInputFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, candidateToSendInputFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
|
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -567,6 +567,17 @@ func (pR *Room) StartBattle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames)
|
toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames)
|
||||||
|
if false == pR.BackendDynamicsEnabled {
|
||||||
|
// When "false == pR.BackendDynamicsEnabled", the variable "refRenderFrameId" is not well defined
|
||||||
|
minLastSentInputFrameId := int32(math.MaxInt32)
|
||||||
|
for _, player := range pR.Players {
|
||||||
|
if player.LastSentInputFrameId >= minLastSentInputFrameId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minLastSentInputFrameId = player.LastSentInputFrameId
|
||||||
|
}
|
||||||
|
toApplyInputFrameId = minLastSentInputFrameId
|
||||||
|
}
|
||||||
for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) {
|
for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) {
|
||||||
f := pR.InputsBuffer.Pop().(*pb.InputFrameDownsync)
|
f := pR.InputsBuffer.Pop().(*pb.InputFrameDownsync)
|
||||||
if pR.inputFrameIdDebuggable(f.InputFrameId) {
|
if pR.inputFrameIdDebuggable(f.InputFrameId) {
|
||||||
|
@@ -440,7 +440,7 @@
|
|||||||
"array": [
|
"array": [
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
344.75930058781137,
|
216.05530045313827,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
@@ -382,7 +382,8 @@ cc.Class({
|
|||||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||||
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
||||||
};
|
};
|
||||||
resultPanelScriptIns.onCloseDelegate = () => {};
|
resultPanelScriptIns.onCloseDelegate = () => {
|
||||||
|
};
|
||||||
|
|
||||||
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
|
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
|
||||||
self.gameRuleNode.width = self.canvasNode.width;
|
self.gameRuleNode.width = self.canvasNode.width;
|
||||||
@@ -740,9 +741,11 @@ cc.Class({
|
|||||||
newPlayerNode.setPosition(cc.v2(x, y));
|
newPlayerNode.setPosition(cc.v2(x, y));
|
||||||
newPlayerNode.getComponent("SelfPlayer").mapNode = self.node;
|
newPlayerNode.getComponent("SelfPlayer").mapNode = self.node;
|
||||||
const currentSelfColliderCircle = newPlayerNode.getComponent(cc.CircleCollider);
|
const currentSelfColliderCircle = newPlayerNode.getComponent(cc.CircleCollider);
|
||||||
const r = currentSelfColliderCircle.radius, d = 2*r;
|
const r = currentSelfColliderCircle.radius,
|
||||||
|
d = 2 * r;
|
||||||
// The collision box of an individual player is a polygon instead of a circle, because the backend collision engine doesn't handle circle alignment well.
|
// The collision box of an individual player is a polygon instead of a circle, because the backend collision engine doesn't handle circle alignment well.
|
||||||
const x0 = x-r, y0 = y-r;
|
const x0 = x - r,
|
||||||
|
y0 = y - r;
|
||||||
let pts = [[0, 0], [d, 0], [d, d], [0, d]];
|
let pts = [[0, 0], [d, 0], [d, d], [0, d]];
|
||||||
|
|
||||||
const newPlayerColliderLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
|
const newPlayerColliderLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
|
||||||
|
Reference in New Issue
Block a user