mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-17 04:29:00 +00:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d38d4b4ec9 | ||
|
03828db6ff | ||
|
917fca2bcd | ||
|
680e4f1f59 | ||
|
f367609276 |
17
README.md
17
README.md
@@ -2,20 +2,15 @@
|
||||
|
||||
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
|
||||
|
||||
[Demo recorded over INTERNET (Phone-Wifi v.s. PC-Wifi UDP holepunched) using an input delay of 6 frames](https://pan.baidu.com/s/1UArwqDShLoPjYppjjqsTqQ?pwd=10wc), and it feels SMOOTH when playing!
|
||||
|
||||

|
||||
|
||||
As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- **since v0.9.25, the project is actually equipped with UDP capabilities as follows**.
|
||||
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone.
|
||||
- If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for a [Phone-4G v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1IZVa5wVgAdeH6D-xsZYFUw?pwd=dgkj).
|
||||
- When using the so called `native apps` on `Android` and `Windows` (I'm working casually hard to support `iOS` next), the frontends will try to use UDP hole-punching w/ the help of backend as a registry. If UDP hole-punching is working, the rollback is often less than `turn-around frames to recover` and thus not noticeable, being much better than using websocket alone. This video shows how the UDP holepunched p2p performs for [Phone-Wifi v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1K6704bJKlrSBTVqGcXhajA?pwd=l7ok).
|
||||
- If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for [Phone-4G v.s. PC-Wifi (viewed by PC side)](https://pan.baidu.com/s/1IZVa5wVgAdeH6D-xsZYFUw?pwd=dgkj).
|
||||
- Browser vs `native app` is possible but in that case only websocket is used.
|
||||
|
||||
The following video is recorded over INTERNET using an input delay of 4 frames and it feels SMOOTH when playing! Please also checkout these demo videos
|
||||
- [source video of the first gif (earlier version)](https://pan.baidu.com/s/1ML6hNupaPHPJRd5rcTvQvw?pwd=8ruc)
|
||||
- [source video of the second gif (added turn-around optimization & dashing)](https://pan.baidu.com/s/1isMcLvxax4NNkDgitV_FDg?pwd=s1i6)
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# Notable Features
|
||||
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786)
|
||||
|
BIN
charts/Merged_cut_annotated_spedup.gif
Normal file
BIN
charts/Merged_cut_annotated_spedup.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 MiB |
Binary file not shown.
Before Width: | Height: | Size: 6.7 MiB |
Binary file not shown.
Before Width: | Height: | Size: 3.7 MiB |
@@ -547,7 +547,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
209.73151519075364,
|
||||
210.27555739078596,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -2313,7 +2313,7 @@
|
||||
"mapNode": {
|
||||
"__id__": 3
|
||||
},
|
||||
"speed": 5000,
|
||||
"speed": 500,
|
||||
"_id": "76ImpM7XtPSbiLHDXdsJa+"
|
||||
},
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ cc.Class({
|
||||
},
|
||||
speed: {
|
||||
type: cc.Float,
|
||||
default: 500
|
||||
default: 100
|
||||
},
|
||||
},
|
||||
|
||||
|
@@ -394,6 +394,8 @@ cc.Class({
|
||||
self.networkDoctor = new NetworkDoctor(20);
|
||||
self.skipRenderFrameFlag = false;
|
||||
|
||||
self.allowRollbackOnPeerUpsync = true;
|
||||
|
||||
self.countdownNanos = null;
|
||||
if (self.countdownLabel) {
|
||||
self.countdownLabel.string = "";
|
||||
@@ -950,18 +952,8 @@ fromUDP=${fromUDP}`);
|
||||
const inputFrame = batch[k]; // could be either "pb.InputFrameDownsync" or "pb.InputFrameUpsync", depending on "fromUDP"
|
||||
const inputFrameId = inputFrame.inputFrameId;
|
||||
const peerEncodedInput = (true == fromUDP ? inputFrame.encoded : inputFrame.inputList[peerJoinIndex - 1]);
|
||||
if (inputFrameId <= renderedInputFrameIdUpper) {
|
||||
if (false == self.allowRollbackOnPeerUpsync && inputFrameId <= renderedInputFrameIdUpper) {
|
||||
// [WARNING] Avoid obfuscating already rendered history, even at "inputFrameId == renderedInputFrameIdUpper", due to the use of "INPUT_SCALE_FRAMES" some previous render frames might already be rendered with "inputFrameId"!
|
||||
// TODO: Shall we update the "chaserRenderFrameId" if the rendered history was wrong? It doesn't seem to impact eventual correctness if we allow the update of "chaserRenderFrameId" upon "inputFrameId <= renderedInputFrameIdUpper" here, however UDP upsync doesn't reserve order from a same sender and there might be multiple other senders, hence it might result in unnecessarily frequent chasing.
|
||||
const localInputFrame = self.recentInputCache.GetByFrameId(inputFrameId);
|
||||
if (null != localInputFrame
|
||||
&&
|
||||
null == firstPredictedYetIncorrectInputFrameId
|
||||
&&
|
||||
localInputFrame.InputList[peerJoinIndex - 1] != peerEncodedInput
|
||||
) {
|
||||
firstPredictedYetIncorrectInputFrameId = inputFrameId;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inputFrameId <= self.lastAllConfirmedInputFrameId) {
|
||||
@@ -986,12 +978,26 @@ fromUDP=${fromUDP}`);
|
||||
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList);
|
||||
//console.log(`Updated encoded input of peerJoinIndex=${peerJoinIndex} to ${peerEncodedInput} for inputFrameId=${inputFrameId}/renderedInputFrameIdUpper=${renderedInputFrameIdUpper} from ${JSON.stringify(inputFrame)}; newInputFrameDownsyncLocal=${self.gopkgsInputFrameDownsyncStr(newInputFrameDownsyncLocal)}; existingInputFrame=${self.gopkgsInputFrameDownsyncStr(existingInputFrame)}`);
|
||||
self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameId);
|
||||
|
||||
if (self.allowRollbackOnPeerUpsync) {
|
||||
// Reaching here implies that "true == self.allowRollbackOnPeerUpsync".
|
||||
// Shall we update the "chaserRenderFrameId" if the rendered history was wrong? It doesn't seem to impact eventual correctness if we allow the update of "chaserRenderFrameId" upon "inputFrameId <= renderedInputFrameIdUpper" here, however UDP upsync doesn't reserve order from a same sender and there might be multiple other senders, hence it might result in unnecessarily frequent chasing.
|
||||
if (
|
||||
null == firstPredictedYetIncorrectInputFrameId
|
||||
&&
|
||||
existingInputFrame.InputList[peerJoinIndex - 1] != peerEncodedInput
|
||||
) {
|
||||
firstPredictedYetIncorrectInputFrameId = inputFrameId;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (0 < effCnt) {
|
||||
//self._markConfirmationIfApplicable();
|
||||
self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
|
||||
}
|
||||
self._handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP);
|
||||
if (true == self.allowRollbackOnPeerUpsync) {
|
||||
self._handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP);
|
||||
}
|
||||
},
|
||||
|
||||
onPlayerAdded(rdf /* pb.RoomDownsyncFrame */ ) {
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "40edd08e-316c-44b8-a50f-bd173554c554",
|
||||
"uuid": "22e2b0ab-1350-4f5e-9960-f2b45b0bf353",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
int const punchServerCnt = 3;
|
||||
int const punchPeerCnt = 3;
|
||||
int const broadcastUpsyncCnt = 1;
|
||||
int const broadcastUpsyncCnt = 2;
|
||||
|
||||
uv_udp_t *udpRecvSocket = NULL, *udpSendSocket = NULL;
|
||||
uv_thread_t recvTid, sendTid;
|
||||
|
@@ -30,6 +30,7 @@ const (
|
||||
SNAP_INTO_PLATFORM_OVERLAP = float64(0.1)
|
||||
SNAP_INTO_PLATFORM_THRESHOLD = float64(0.5)
|
||||
VERTICAL_PLATFORM_THRESHOLD = float64(0.9)
|
||||
MAGIC_FRAMES_TO_BE_ONWALL = int32(12)
|
||||
|
||||
NO_SKILL = -1
|
||||
NO_SKILL_HIT = -1
|
||||
@@ -939,11 +940,6 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !currPlayerDownsync.OnWall && thatPlayerInNextFrame.OnWall {
|
||||
// To avoid mysterious climbing up the wall after sticking on it
|
||||
thatPlayerInNextFrame.VelY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if !thatPlayerInNextFrame.OnWall {
|
||||
@@ -1071,9 +1067,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
oldNextCharacterState := thatPlayerInNextFrame.CharacterState
|
||||
switch oldNextCharacterState {
|
||||
case ATK_CHARACTER_STATE_IDLE1, ATK_CHARACTER_STATE_WALKING, ATK_CHARACTER_STATE_TURNAROUND:
|
||||
if thatPlayerInNextFrame.OnWall {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ONWALL
|
||||
} else if jumpedOrNotList[i] || ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP == currPlayerDownsync.CharacterState {
|
||||
if jumpedOrNotList[i] || ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP == currPlayerDownsync.CharacterState {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP
|
||||
} else {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1_NO_JUMP
|
||||
@@ -1086,6 +1080,17 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
}
|
||||
}
|
||||
|
||||
if thatPlayerInNextFrame.OnWall {
|
||||
switch thatPlayerInNextFrame.CharacterState {
|
||||
case ATK_CHARACTER_STATE_WALKING, ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP, ATK_CHARACTER_STATE_INAIR_IDLE1_NO_JUMP:
|
||||
hasBeenOnWallChState := (ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState)
|
||||
hasBeenOnWallCollisionResultForSameChState := (currPlayerDownsync.OnWall && MAGIC_FRAMES_TO_BE_ONWALL <= thatPlayerInNextFrame.FramesInChState)
|
||||
if hasBeenOnWallChState || hasBeenOnWallCollisionResultForSameChState {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ONWALL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset "FramesInChState" if "CharacterState" is changed
|
||||
if thatPlayerInNextFrame.CharacterState != currPlayerDownsync.CharacterState {
|
||||
thatPlayerInNextFrame.FramesInChState = 0
|
||||
|
Reference in New Issue
Block a user