Compare commits

...

11 Commits

Author SHA1 Message Date
genxium
b5b43bb596 Minor update. 2023-01-22 14:32:32 +08:00
genxium
59767c1ed5 Added network doctor stats. 2023-01-22 11:39:27 +08:00
genxium
1c6ad5c8f8 Minor fix. 2023-01-21 23:03:55 +08:00
genxium
34e0893eb8 Added network doctor for frontend. 2023-01-21 22:53:41 +08:00
genxium
cc7524becd Updated README. 2023-01-21 11:59:01 +08:00
genxium
d06cb18a08 Fixed inertia on jumping. 2023-01-21 11:11:48 +08:00
genxium
00816fb636 Minor update. 2023-01-21 10:41:38 +08:00
genxium
ff24bea055 Fixed fireball trigger. 2023-01-21 10:40:13 +08:00
genxium
56d66a128a Minor updates. 2023-01-20 23:44:26 +08:00
genxium
2f097dfec5 Added turn-around and dashing actual triggers. 2023-01-20 23:22:02 +08:00
genxium
ff48b47ecc Updated documentation for the plan of UDP secondary session. 2023-01-20 20:49:26 +08:00
30 changed files with 1917 additions and 1124 deletions

View File

@@ -69,4 +69,10 @@ To summarize, if UDP is used we need
When using UDP, it's also necessary to verify authorization of each incoming packet, e.g. by simple time limited symmetric key, due to being connectionless.
## Why not hybrid?
Instead of replacing all use of TCP by UDP, it's more reasonable to keep using TCP for login and the "all-confirmed downsync inputFrames" from server to players (and possibly "upsync inputFrames" from player to server, but tradeoff on that remains to be discussed), while using UDP for broadcasting inputFrames of each individual player asap (either using p2p or not) just for **better prediction performance**!
Instead of replacing all use of TCP by UDP, it's more reasonable to keep using TCP for login and the "all-confirmed downsync inputFrames" from server to players (and possibly "upsync inputFrames" from player to server, but tradeoff on that remains to be discussed), while using a `UDP secondary session` for broadcasting inputFrames of each individual player asap (either using p2p or not) just for **better prediction performance**!
## How do you actually implement the `UDP secondary session`?
It's not a global consensus, but in practice many UDP communications are platform specific due to their paired asynchronous I/O choices, e.g. epoll in Linux and kqueue in BSD-ish. Of course there're many 3rd party higher level encapsulated tools for cross-platform use but that introduces extra debugging when things go wrong.
Therefore, the following plan doesn't assume use of any specific 3rd party encapsulation of UDP communication.
![UDP_secondary_session](./charts/UDPEssentials.jpg)

View File

@@ -3,19 +3,14 @@
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md). 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) -- not necessarily correct but that's indeed a question to face :)
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](https://pan.baidu.com/s/1ML6hNupaPHPJRd5rcTvQvw?pwd=8ruc)
- [phone v.s. PC over internet battle#1](https://pan.baidu.com/s/1NuGxuMwrV_jalcToyUZPLg?pwd=kfkr)
- [phone v.s. PC over internet battle#2](https://pan.baidu.com/s/1kMiFdwDHyJpZJ0GGU1Y3eA?pwd=46gd)
- [PC Wifi viewing Phone 4g](https://pan.baidu.com/s/1PJEtC9iB_fcabMWhbx2oAg?pwd=tp7k)
- [PC Wifi viewing Phone Wifi (over internet of course)](https://pan.baidu.com/s/108rvC1CcUdiQeMauXWsLJg?pwd=mn39)
- [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.
![gif_demo_1](./charts/internet_fireball_explosion_wallmove_spedup.gif)
![gif_demo_2](./charts/jump_sync_spedup.gif)
All gifs are sped up to ~1.5x for file size reduction, kindly note that animations are resumed from a partial progress!
![gif_demo_2](./charts/internet_dash_turnaround_cut_spedup.gif)
# Notable Features
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786)
@@ -111,3 +106,6 @@ Moreover, in practice I found that to spot sync anomalies, the following tools a
- Detection of [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/frontend/assets/scripts/Map.js#L842).
- Detection of [type#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1246).
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1259).
There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`.
![networkstats](./charts/networkstats.png)

File diff suppressed because one or more lines are too long

BIN
charts/UDPEssentials.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

BIN
charts/networkstats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@@ -1,66 +0,0 @@
function NetworkDoctor(serverFps, clientUpsyncFps) {
this.serverFps = serverFps;
this.clientUpsyncFps = clientUpsyncFps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
this.setupFps = function(fps) {
this.serverFps = this.clientUpsyncFps = fps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
}
this._lastFrameDiffRecvTime = null;
this._tooLongSinceLastFrameDiffReceived = function() {
if (undefined === this._lastFrameDiffRecvTime || null === this._lastFrameDiffRecvTime) return false;
return (this._tooLongSinceLastFrameDiffReceivedThreshold <= (Date.now() - this._lastFrameDiffRecvTime));
};
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold = 120;
this.onNewFrameDiffReceived = function(frameDiff) {
var now = Date.now();
if (undefined !== this._lastFrameDiffRecvTime && null !== this._lastFrameDiffRecvTime) {
var intervalFromLastFrameDiff = (now - this._lastFrameDiffRecvTime);
if ((this.millisPerServerFrame << 5) < intervalFromLastFrameDiff) {
++this._consecutiveALittleLongFrameDiffReceivedIntervalCount;
console.log('Medium delay, intervalFromLastFrameDiff is', intervalFromLastFrameDiff);
} else {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
}
}
this._lastFrameDiffRecvTime = now;
};
this._networkComplaintPrefix = "\nNetwork is not good >_<\n";
this.generateNetworkComplaint = function(excludeTypeConstantALittleLongFrameDiffReceivedInterval, excludeTypeTooLongSinceLastFrameDiffReceived) {
if (this.hasBattleStopped) return null;
var shouldComplain = false;
var ret = this._networkComplaintPrefix;
if (true != excludeTypeConstantALittleLongFrameDiffReceivedInterval && this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold <= this._consecutiveALittleLongFrameDiffReceivedIntervalCount) {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
ret += "\nConstantly having a little long recv interval.\n";
shouldComplain = true;
}
if (true != excludeTypeTooLongSinceLastFrameDiffReceived && this._tooLongSinceLastFrameDiffReceived()) {
ret += "\nToo long since last received frameDiff.\n";
shouldComplain = true;
}
return (shouldComplain ? ret : null);
};
this.hasBattleStopped = false;
this.onBattleStopped = function() {
this.hasBattleStopped = true;
};
this.isClientSessionConnected = function() {
if (!window.game) return false;
if (!window.game.clientSession) return false;
return window.game.clientSession.connected;
};
}
window.NetworkDoctor = NetworkDoctor;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.5",
"uuid": "d41313ca-b2c3-4436-a05f-7e0eb290b1e6",
"uuid": "40edd08e-316c-44b8-a50f-bd173554c554",
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": true,

View File

@@ -0,0 +1,43 @@
{
"__type__": "cc.AnimationClip",
"_name": "TurnAround1",
"_objFlags": 0,
"_native": "",
"_duration": 0.15,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "28ee1f29-e538-4d36-bb5c-275f9e3b392b"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "211a73bb-31d7-4e6c-901e-f6939d9214e0"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "048c41dc-fc00-4bc4-8041-6003e7c2b6e4"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "9435195e-4560-495e-b1ae-083c0c87e8a0"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "e906322d-a08b-4477-a2e9-98acd42fa034",
"subMetas": {}
}

View File

@@ -0,0 +1,43 @@
{
"__type__": "cc.AnimationClip",
"_name": "TurnAround1",
"_objFlags": 0,
"_native": "",
"_duration": 0.15,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "ee5e05fa-b515-470f-bc3c-43544f02cb92"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "ffa521b6-118e-46e8-be1c-51cc54381ec8"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "0b27d2c9-c5a3-4020-adbe-0297c1ba3aeb"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "a47f518e-62fb-4549-8897-4f2d387bd145"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "edd23b2f-1caa-4836-88a7-e4af1f26743e",
"subMetas": {}
}

View File

@@ -0,0 +1,43 @@
{
"__type__": "cc.AnimationClip",
"_name": "TurnAround1",
"_objFlags": 0,
"_native": "",
"_duration": 0.15,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "c1a00209-f74d-41b5-a5da-df5720ac34b4"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "2b52c0f1-2360-4a2b-9233-bf5662de09a5"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "e3f9dfe7-ed91-4dc3-b68b-a3a3c2637074"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "7515ef50-3a14-4e58-8811-a0c890fc40f3"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "6e1139d4-03dd-4bd4-9510-606e94f629fe",
"subMetas": {}
}

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="7" nextobjectid="135">
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="7" nextobjectid="137">
<tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/>
<tileset firstgid="129" source="tiles2.tsx"/>
<layer id="6" name="Ground" width="128" height="64">
<data encoding="base64" compression="zlib">
eJzt2ztuAjEURmELlIYuiKRHyk4iGjo2wP6XkRDGUmThxwx3fC3+U3wNw8v3eEyabEMIWwAAAAAAAAAAAADo4IQ/3h08+3t/B2/KM1BeOzPQXjsz0F47M6iv/c3oc6zeh/7+/Q+NHr1P6z7YGz2H/svWXur1TP/SvqL/OP1LnSz6t7Qt9a1dp//4/VvOgJJeM3hlLf2tzNljI83glY2+9vcMpRko9E/PgFz3NfbBKDNQ7r+kvdU+GHEGHv2/Gl9zNHrOKOh/t5vQX8dt7d+JeC19PDo2yr1+NHEG3i28+m8m8f6//jpnbAzkfsNr15eqfR/1+z/tfxP/Fvj/mFX/0dD/7pz0fsR69p8rmHv20D9vl5mZ1flb67VkP81pT/8QPialmT1z/5f2QK/7v7Q/6Z/vb6F0DvToXzuf1PvDv4Nn/4s4+mujvzb6a6O/Nvpro782+mujvzb6a6O/Nvpro782+mujvzb6a6O/Nvpro782+mujvzb6a6O/tvg/UN4t6E9/+tO/d3+s1/8HUhSy6A==
eJzt201uwjAQhuEIxCY7KugeqTepumHXC/T+xyCqYimy4sQ4E2bE9y6eDf+e1w4bOHZddwQAAAAAAAAAAACAF/jBP+8Onv29P4M35Rkor50ZaK+dGWivnRlor50ZtK39UmnL5/owegz94/X/on8Ykfsv9V27n/72a0/drPqfBucZeeMSjxm8m5b+W1m9Dv3fv//c9eG88f3p397fytr1vNR9j31A/1hq21vtg4gziNz/ZvQY+vubrr0fefdv3QOt+0O9/3cm3ZffntwqlZ4fTZqBdwuv/odROv9/g3vBwUDpjFq8dst7q5//vP/0O2B6m1V/T/Qv979nvedY9/jcwTPXHPov9+lXZldjrb/1fqL/c/2vo6W5bTn/S3tg7/Nfsz/pX+5v+Z1L/3i8f3cdhXcHz/6/4uivjf7a6K+N/tror43+2uivjf7a6K+N/tror43+2uivjf7a6K+N/tror43+2uivjf7a6K+N/trSf6C8W9Cf/vSn/6v7Y7/+Dyz1uAA=
</data>
</layer>
<objectgroup id="1" name="PlayerStartingPos">
@@ -64,7 +64,7 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="60" x="1232" y="432" width="208" height="16">
<object id="60" x="1232" y="448" width="208" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
@@ -99,7 +99,7 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="90" x="1232" y="496" width="320" height="16">
<object id="90" x="1248" y="464" width="320" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
@@ -114,37 +114,37 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="100" x="1552" y="576" width="128" height="16">
<object id="100" x="1538" y="560" width="144" height="32">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="101" x="1568" y="560" width="112" height="16">
<object id="101" x="1568" y="528" width="112" height="32">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="102" x="1584" y="544" width="96" height="16">
<object id="102" x="1136" y="368" width="96" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="103" x="1600" y="528" width="80" height="16">
<object id="103" x="1600" y="496" width="80" height="32">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="104" x="768" y="382" width="304" height="16">
<object id="104" x="816" y="414" width="304" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="105" x="768" y="302" width="16" height="96">
<object id="105" x="816" y="366" width="16" height="64">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="106" x="1056" y="302" width="16" height="96">
<object id="106" x="1104" y="334" width="16" height="96">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
@@ -164,42 +164,7 @@
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="120" x="736" y="512" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="121" x="736" y="336" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="125" x="688" y="448" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="127" x="1088" y="320" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="128" x="1120" y="336" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="129" x="1136" y="368" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="130" x="1168" y="384" width="16" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="132" x="1184" y="416" width="16" height="16">
<object id="136" x="1232" y="432" width="208" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>

View File

@@ -482,6 +482,13 @@
},
{
"__uuid__": "e8247e2a-1b5b-4618-86f8-224b25246b55"
},
null,
null,
null,
null,
{
"__uuid__": "6e1139d4-03dd-4bd4-9510-606e94f629fe"
}
],
"playOnLoad": false,
@@ -653,6 +660,9 @@
},
{
"__uuid__": "411f964a-4dd8-424c-b2e2-d92b10474ce2"
},
{
"__uuid__": "e906322d-a08b-4477-a2e9-98acd42fa034"
}
],
"playOnLoad": false,
@@ -822,6 +832,11 @@
},
{
"__uuid__": "0abbd156-980e-475e-9994-3c958bd913fc"
},
null,
null,
{
"__uuid__": "edd23b2f-1caa-4836-88a7-e4af1f26743e"
}
],
"playOnLoad": false,

View File

@@ -78,19 +78,19 @@
"_active": true,
"_components": [
{
"__id__": 40
"__id__": 50
},
{
"__id__": 41
"__id__": 51
},
{
"__id__": 42
"__id__": 52
},
{
"__id__": 43
"__id__": 53
},
{
"__id__": 44
"__id__": 54
}
],
"_prefab": null,
@@ -158,7 +158,7 @@
"__id__": 5
},
{
"__id__": 39
"__id__": 49
}
],
"_prefab": null,
@@ -247,10 +247,10 @@
"__uuid__": "670b477e-61a1-4778-879b-35913f7c79d2"
},
"boundRoomIdLabel": {
"__id__": 16
"__id__": 26
},
"countdownLabel": {
"__id__": 23
"__id__": 33
},
"resultPanelPrefab": {
"__uuid__": "c4cfe3bd-c59e-4d5b-95cb-c933b120e184"
@@ -269,9 +269,21 @@
},
"forceBigEndianFloatingNumDecoding": false,
"renderFrameIdLagTolerance": 4,
"jigglingEps1D": 0.001,
"bulletTriggerEnabled": true,
"closeOnForcedtoResyncNotSelf": true,
"sendingQLabel": {
"__id__": 13
},
"inputFrameDownsyncQLabel": {
"__id__": 15
},
"peerInputFrameUpsyncQLabel": {
"__id__": 17
},
"rollbackFramesLabel": {
"__id__": 19
},
"skippedRenderFrameCntLabel": {
"__id__": 21
},
"_id": "d12gkAmppNlIzqcRDELa91"
},
{
@@ -283,13 +295,13 @@
},
"_children": [
{
"__id__": 33
"__id__": 43
}
],
"_active": true,
"_components": [
{
"__id__": 38
"__id__": 48
}
],
"_prefab": null,
@@ -352,19 +364,19 @@
"__id__": 6
},
{
"__id__": 22
"__id__": 32
},
{
"__id__": 24
"__id__": 34
},
{
"__id__": 28
"__id__": 38
}
],
"_active": true,
"_components": [
{
"__id__": 32
"__id__": 42
}
],
"_prefab": null,
@@ -433,7 +445,7 @@
"_active": true,
"_components": [
{
"__id__": 21
"__id__": 31
}
],
"_prefab": null,
@@ -527,7 +539,7 @@
"array": [
0,
0,
209.57814771583418,
210.16474188040044,
0,
0,
0,
@@ -596,15 +608,30 @@
"_children": [
{
"__id__": 12
},
{
"__id__": 14
},
{
"__id__": 16
},
{
"__id__": 18
},
{
"__id__": 20
},
{
"__id__": 22
}
],
"_active": true,
"_components": [
{
"__id__": 19
"__id__": 29
},
{
"__id__": 20
"__id__": 30
}
],
"_prefab": null,
@@ -631,7 +658,7 @@
"ctor": "Float64Array",
"array": [
-447.294,
135.702,
138.942,
0,
0,
0,
@@ -655,6 +682,476 @@
"groupIndex": 0,
"_id": "35scg40jVAnKrTPiei5ckg"
},
{
"__type__": "cc.Node",
"_name": "sendingQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 13
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
88.695,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "ack7XH+0lEj6chSUsKBLU5"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "deCJfLuoFO36c/O4lXWJ1N"
},
{
"__type__": "cc.Node",
"_name": "inputFrameDownsyncQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
66.08499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "d9n+NRm9pA0YtKxvy4bW+U"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 14
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "90NvjGFrpBFqqzl1WZczta"
},
{
"__type__": "cc.Node",
"_name": "peerInputFrameUpsyncQ",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 17
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
43.47499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "45FAmgRLZNNbLt/GcnTXVx"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 16
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "42PRTrjEpDw6Z8N5yWFTpd"
},
{
"__type__": "cc.Node",
"_name": "rollbackFrames",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 19
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
20.86499999999998,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "f5SBs3b1pPNbHbnqVLYsHp"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 18
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "77aNARt1VATLsjIzwbqvkh"
},
{
"__type__": "cc.Node",
"_name": "skippedCnt",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 21
}
],
"_prefab": null,
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 243,
"g": 225,
"b": 11,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 18.33,
"height": 22.61
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
-1.7450000000000188,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": "cdlF8Z8TZEdLRHQQ8T8qX7"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 20
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "0",
"_N$string": "0",
"_fontSize": 22,
"_lineHeight": 37,
"_enableWrapText": true,
"_N$file": {
"__uuid__": "a564b3db-b8cb-48b4-952e-25bb56949116"
},
"_isSystemFontUsed": false,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 0,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_N$cacheMode": 0,
"_id": "2flWrlaK1PeJMUB/in+S1W"
},
{
"__type__": "cc.Node",
"_name": "RoomIdIndicator",
@@ -664,19 +1161,19 @@
},
"_children": [
{
"__id__": 13
"__id__": 23
},
{
"__id__": 15
"__id__": 25
}
],
"_active": false,
"_components": [
{
"__id__": 17
"__id__": 27
},
{
"__id__": 18
"__id__": 28
}
],
"_prefab": null,
@@ -732,13 +1229,13 @@
"_name": "label",
"_objFlags": 0,
"_parent": {
"__id__": 12
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 14
"__id__": 24
}
],
"_prefab": null,
@@ -794,7 +1291,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 13
"__id__": 23
},
"_enabled": true,
"_materials": [],
@@ -822,13 +1319,13 @@
"_name": "BoundRoomIdLabel",
"_objFlags": 0,
"_parent": {
"__id__": 12
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 16
"__id__": 26
}
],
"_prefab": null,
@@ -884,7 +1381,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 15
"__id__": 25
},
"_enabled": true,
"_materials": [],
@@ -912,7 +1409,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
"__id__": 22
},
"_enabled": true,
"_layoutSize": {
@@ -945,7 +1442,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 12
"__id__": 22
},
"_enabled": true,
"_materials": [],
@@ -1067,7 +1564,7 @@
"_active": true,
"_components": [
{
"__id__": 23
"__id__": 33
}
],
"_prefab": null,
@@ -1123,7 +1620,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 22
"__id__": 32
},
"_enabled": true,
"_materials": [
@@ -1157,7 +1654,7 @@
},
"_children": [
{
"__id__": 25
"__id__": 35
}
],
"_active": true,
@@ -1215,16 +1712,16 @@
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 24
"__id__": 34
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 26
"__id__": 36
},
{
"__id__": 27
"__id__": 37
}
],
"_prefab": null,
@@ -1280,7 +1777,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 25
"__id__": 35
},
"_enabled": true,
"_materials": [
@@ -1314,7 +1811,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 25
"__id__": 35
},
"_enabled": true,
"alignMode": 0,
@@ -1345,7 +1842,7 @@
},
"_children": [
{
"__id__": 29
"__id__": 39
}
],
"_active": true,
@@ -1403,16 +1900,16 @@
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 28
"__id__": 38
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 30
"__id__": 40
},
{
"__id__": 31
"__id__": 41
}
],
"_prefab": null,
@@ -1468,7 +1965,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 29
"__id__": 39
},
"_enabled": true,
"_materials": [
@@ -1502,7 +1999,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 29
"__id__": 39
},
"_enabled": true,
"alignMode": 0,
@@ -1560,16 +2057,16 @@
},
"_children": [
{
"__id__": 34
"__id__": 44
}
],
"_active": true,
"_components": [
{
"__id__": 36
"__id__": 46
},
{
"__id__": 37
"__id__": 47
}
],
"_prefab": null,
@@ -1625,13 +2122,13 @@
"_name": "Joystick",
"_objFlags": 0,
"_parent": {
"__id__": 33
"__id__": 43
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 35
"__id__": 45
}
],
"_prefab": null,
@@ -1687,7 +2184,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 34
"__id__": 44
},
"_enabled": true,
"_materials": [
@@ -1721,7 +2218,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 33
"__id__": 43
},
"_enabled": true,
"_materials": [
@@ -1755,7 +2252,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 33
"__id__": 43
},
"_enabled": true,
"alignMode": 0,
@@ -1908,10 +2405,10 @@
"__id__": 3
},
"stickhead": {
"__id__": 34
"__id__": 44
},
"base": {
"__id__": 33
"__id__": 43
},
"joyStickEps": 0.1,
"magicLeanLowerBound": 0.414,
@@ -1932,10 +2429,10 @@
"linearMovingEps": 0.1,
"scaleByEps": 0.0375,
"btnA": {
"__id__": 24
"__id__": 34
},
"btnB": {
"__id__": 28
"__id__": 38
},
"_id": "e9oVYTr7ROlpp/IrNjBUmR"
}

View File

@@ -362,7 +362,7 @@
"array": [
0,
0,
215.64032554232523,
216.6734179122529,
0,
0,
0,

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ window.ATK_CHARACTER_STATE = {
Atk5: [14, "Atk5"],
Dashing: [15, "Dashing"],
OnWall: [16, "OnWall"],
TurnAround1: [17, "TurnAround1"],
};
window.ATK_CHARACTER_STATE_ARR = [];
@@ -93,7 +94,7 @@ cc.Class({
} else if (0 < rdfPlayer.DirX) {
this.animNode.scaleX = (+1.0);
}
if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState) {
if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState || ATK_CHARACTER_STATE.TurnAround1[0] == newCharacterState) {
if (0 < rdfPlayer.OnWallNormX) {
this.animNode.scaleX = (-1.0);
} else {

View File

@@ -2,6 +2,7 @@ const i18n = require('LanguageData');
i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field
const RingBuffer = require('./RingBuffer');
const NetworkDoctor = require('./NetworkDoctor');
const PriorityQueue = require("./PriorityQueue");
window.ALL_MAP_STATES = {
@@ -96,16 +97,26 @@ cc.Class({
type: cc.Integer,
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
},
jigglingEps1D: {
type: cc.Float,
default: 1e-3
sendingQLabel: {
type: cc.Label,
default: null
},
bulletTriggerEnabled: {
default: false
inputFrameDownsyncQLabel: {
type: cc.Label,
default: null
},
closeOnForcedtoResyncNotSelf: {
default: true
peerInputFrameUpsyncQLabel: {
type: cc.Label,
default: null
},
rollbackFramesLabel: {
type: cc.Label,
default: null
},
skippedRenderFrameCntLabel: {
type: cc.Label,
default: null
}
},
_inputFrameIdDebuggable(inputFrameId) {
@@ -184,6 +195,7 @@ cc.Class({
// Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly.
batchInputFrameIdSt = self.recentInputCache.StFrameId;
}
self.networkDoctor.logSending(batchInputFrameIdSt, latestLocalInputFrameId);
for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.GetByFrameId(i);
if (null == inputFrameDownsync) {
@@ -342,6 +354,9 @@ cc.Class({
self.othersForcedDownsyncRenderFrameDict = new Map();
self.rdfIdToActuallyUsedInput = new Map();
self.networkDoctor = new NetworkDoctor(20);
self.skipRenderFrameFlag = false;
self.countdownNanos = null;
if (self.countdownLabel) {
self.countdownLabel.string = "";
@@ -409,6 +424,7 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
@@ -417,6 +433,7 @@ cc.Class({
window.forceBigEndianFloatingNumDecoding = self.forceBigEndianFloatingNumDecoding;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
console.warn("+++++++ Map onLoad()");
@@ -690,6 +707,7 @@ cc.Class({
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId".
self.chaserRenderFrameId = rdf.Id;
self.networkDoctor.logRollbackFrames(0);
const canvasNode = self.canvasNode;
self.ctrl = canvasNode.getComponent("TouchEventsManager");
@@ -796,6 +814,7 @@ cc.Class({
return;
}
self.networkDoctor.logInputFrameDownsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
let firstPredictedYetIncorrectInputFrameId = null;
for (let k in batch) {
const inputFrameDownsync = batch[k];
@@ -844,6 +863,7 @@ lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}
recentInputCache=${self._stringifyRecentInputCache(false)}
batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}]`);
self.chaserRenderFrameId = renderFrameId1;
self.networkDoctor.logRollbackFrames(self.renderFrameId - self.chaserRenderFrameId);
},
onPeerInputFrameUpsync(peerJoinIndex, batch /* []*pb.InputFrameDownsync */ ) {
@@ -860,6 +880,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
return;
}
let effCnt = 0;
//console.log(`Received peer inputFrameUpsync batch w/ inputFrameId in [${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}] for prediction assistance`);
for (let k in batch) {
const inputFrameDownsync = batch[k];
@@ -867,11 +888,15 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
if (inputFrameDownsyncId <= self.lastAllConfirmedInputFrameId) {
continue;
}
effCnt += 1;
self.getOrPrefabInputFrameUpsync(inputFrameDownsyncId); // Make sure that inputFrame exists locally
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameDownsyncId);
existingInputFrame.InputList[peerJoinIndex - 1] = inputFrameDownsync.inputList[peerJoinIndex - 1]; // No need to change "confirmedList", leave it to "onInputFrameDownsyncBatch" -- we're just helping prediction here
self.recentInputCache.SetByFrameId(existingInputFrame, inputFrameDownsyncId);
}
if (0 < effCnt) {
self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
}
},
onPlayerAdded(rdf /* pb.RoomDownsyncFrame */ ) {
@@ -945,6 +970,11 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
Kindly note that Significantly different network bandwidths or delay fluctuations would result in frequent [type#1 forceConfirmation] too, but CAUSE FROM DIFFERENT LOCAL "update(dt)" RATE SHOULD BE THE FIRST TO INVESTIGATE AND ELIMINATE -- because we have control on it, but no one has control on the internet.
*/
if (self.skipRenderFrameFlag) {
self.networkDoctor.logSkippedRenderFrameCnt();
self.skipRenderFrameFlag = false;
return;
}
try {
let st = performance.now();
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
@@ -956,7 +986,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
const delayedInputFrameId = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
if (null == self.recentInputCache.GetByFrameId(delayedInputFrameId)) {
// Possible edge case after resync
// Possible edge case after resync, kindly note that it's OK to prefab a "future inputFrame" here, because "sendInputFrameUpsyncBatch" would be capped by "noDelayInputFrameId from self.renderFrameId".
self.getOrPrefabInputFrameUpsync(delayedInputFrameId);
}
@@ -981,6 +1011,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
// Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const latestRdfResults = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.gopkgsCollisionSys, self.gopkgsCollisionSysMap, false);
self.networkDoctor.logRollbackFrames(self.renderFrameId - self.chaserRenderFrameId);
let prevRdf = latestRdfResults[0],
rdf = latestRdfResults[1];
/*
@@ -1005,9 +1036,13 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
}
self.applyRoomDownsyncFrameDynamics(rdf, prevRdf);
self.showDebugBoundaries(rdf);
if (self.showNetworkDoctorInfo) {
self.showNetworkDoctorLabels();
}
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
let t3 = performance.now();
self.skipRenderFrameFlag = self.networkDoctor.isTooFast();
} catch (err) {
console.error("Error during Map.update", err);
self.onBattleStopped(); // TODO: Popup to ask player to refresh browser
@@ -1499,4 +1534,44 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
}
}
},
showNetworkDoctorLabels() {
const self = this;
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = self.networkDoctor.stats();
if (self.sendingQLabel) {
self.sendingQLabel.string = `${sendingFps} fps sending`;
if (sendingFps < self.networkDoctor.inputRateThreshold) {
self.sendingQLabel.node.color = cc.Color.RED;
} else {
self.sendingQLabel.node.color = cc.Color.WHITE;
}
}
if (self.inputFrameDownsyncQLabel) {
self.inputFrameDownsyncQLabel.string = `${srvDownsyncFps} fps srv-downsync`;
if (srvDownsyncFps < self.networkDoctor.inputRateThreshold) {
self.inputFrameDownsyncQLabel.node.color = cc.Color.RED;
} else {
self.inputFrameDownsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.peerInputFrameUpsyncQLabel) {
self.peerInputFrameUpsyncQLabel.string = `${peerUpsyncFps} fps peer-upsync`;
if (peerUpsyncFps > self.networkDoctor.peerUpsyncFps) {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.RED;
} else {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.rollbackFramesLabel) {
self.rollbackFramesLabel.string = `rollbackFrames: ${rollbackFrames}`
if (rollbackFrames > self.networkDoctor.rollbackFramesThreshold) {
self.rollbackFramesLabel.node.color = cc.Color.RED;
} else {
self.rollbackFramesLabel.node.color = cc.Color.WHITE;
}
}
if (self.skippedRenderFrameCntLabel) {
self.skippedRenderFrameCntLabel.string = `${skippedRenderFrameCnt} frames skipped`
}
},
});

View File

@@ -0,0 +1,94 @@
const RingBuffer = require('./RingBuffer');
var NetworkDoctor = function(capacity) {
this.reset(capacity);
};
NetworkDoctor.prototype.reset = function(capacity) {
this.sendingQ = new RingBuffer(capacity);
this.inputFrameDownsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncCnt = 0;
this.immediateRollbackFrames = 0;
this.skippedRenderFrameCnt = 0;
this.inputRateThreshold = gopkgs.ConvertToNoDelayInputFrameId(60);
this.peerUpsyncThreshold = 8;
this.rollbackFramesThreshold = 4; // Slightly smaller than the minimum "TurnAroundFramesToRecover".
};
NetworkDoctor.prototype.logSending = function(stFrameId, edFrameId) {
this.sendingQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logInputFrameDownsync = function(stFrameId, edFrameId) {
this.inputFrameDownsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logPeerInputFrameUpsync = function(stFrameId, edFrameId) {
const firstPopped = this.peerInputFrameUpsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
if (null != firstPopped) {
this.peerInputFrameUpsyncCnt -= (firstPopped.j - firstPopped.i + 1);
}
this.peerInputFrameUpsyncCnt += (edFrameId - stFrameId + 1);
};
NetworkDoctor.prototype.logRollbackFrames = function(x) {
this.immediateRollbackFrames = x;
};
NetworkDoctor.prototype.stats = function() {
let sendingFps = 0,
srvDownsyncFps = 0,
peerUpsyncFps = 0,
rollbackFrames = this.immediateRollbackFrames;
if (1 < this.sendingQ.cnt) {
const st = this.sendingQ.getByFrameId(this.sendingQ.stFrameId);
const ed = this.sendingQ.getByFrameId(this.sendingQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
sendingFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.inputFrameDownsyncQ.cnt) {
const st = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.stFrameId);
const ed = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
srvDownsyncFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.peerInputFrameUpsyncQ.cnt) {
const st = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.stFrameId);
const ed = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
peerUpsyncFps = Math.round(this.peerInputFrameUpsyncCnt * 1000 / elapsedMillis);
}
return [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, this.skippedRenderFrameCnt];
};
NetworkDoctor.prototype.logSkippedRenderFrameCnt = function() {
this.skippedRenderFrameCnt += 1;
}
NetworkDoctor.prototype.isTooFast = function() {
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats();
if (sendingFps >= this.inputRateThreshold && srvDownsyncFps >= this.inputRateThreshold) {
// At least my network is OK for both TX & RX directions.
if (rollbackFrames >= this.rollbackFramesThreshold) {
// I got many frames rolled back while none of my peers effectively helped my preciction. Deliberately not using "peerUpsyncThreshold" here because when using UDP p2p upsync broadcasting, we expect to receive effective p2p upsyncs from every other player.
return true;
}
}
return false;
};
module.exports = NetworkDoctor;

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.5",
"uuid": "477c07c3-0d50-4d55-96f0-6eaf9f25e2da",
"uuid": "affd726a-02f0-4079-aace-39fe525d7478",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,

View File

@@ -11,11 +11,13 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
const self = this;
window.mapIns = self;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
const mapNode = self.node;
const canvasNode = mapNode.parent;

View File

@@ -13,9 +13,12 @@ var RingBuffer = function(capacity) {
};
RingBuffer.prototype.put = function(item) {
let firstPopped = null;
while (0 < this.cnt && this.cnt >= this.n) {
// Make room for the new element
this.pop();
const popped = this.pop();
if (null == firstPopped)
firstPopped = popped;
}
this.eles[this.ed] = item
this.edFrameId++;
@@ -24,6 +27,7 @@ RingBuffer.prototype.put = function(item) {
if (this.ed >= this.n) {
this.ed -= this.n; // Deliberately not using "%" operator for performance concern
}
return firstPopped;
};
RingBuffer.prototype.pop = function() {

View File

@@ -38,7 +38,7 @@
"orientation": "portrait"
},
"startScene": "2ff474d9-0c9e-4fe3-87ec-fbff7cae85b4",
"title": "TreasureHunterX",
"title": "DelayNoMore",
"webOrientation": "portrait",
"wechatgame": {
"REMOTE_SERVER_ROOT": "https://bgmoba.lokcol.com/static/",

View File

@@ -16,13 +16,13 @@ const (
PATTERN_ID_UNABLE_TO_OP = -2
PATTERN_ID_NO_OP = -1
WORLD_TO_VIRTUAL_GRID_RATIO = float64(100)
WORLD_TO_VIRTUAL_GRID_RATIO = float64(100.0)
VIRTUAL_GRID_TO_WORLD_RATIO = float64(1.0) / WORLD_TO_VIRTUAL_GRID_RATIO
GRAVITY_X = int32(0)
GRAVITY_Y = -int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO) // makes all "playerCollider.Y" a multiple of 0.5 in all cases
INPUT_DELAY_FRAMES = int32(8) // in the count of render frames
INPUT_DELAY_FRAMES = int32(4) // in the count of render frames
INPUT_SCALE_FRAMES = uint32(2) // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
NST_DELAY_FRAMES = int32(16) // network-single-trip delay in the count of render frames, proposed to be (InputDelayFrames >> 1) because we expect a round-trip delay to be exactly "InputDelayFrames"
@@ -448,7 +448,7 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
return &ret
}
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, inputsBuffer *RingBuffer) (int, bool, int32, int32) {
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *RingBuffer) (int, bool, int32, int32) {
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
delayedInputFrameIdForPrevRdf := ConvertToDelayedInputFrameId(currRenderFrame.Id - 1)
@@ -478,11 +478,19 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
prevBtnBLevel = prevDecodedInput.BtnBLevel
}
// Jumping is partially allowed within "CapturedByInertia", but moving is only allowed when "0 == FramesToRecover" (constrained later in "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame")
if 0 == currPlayerDownsync.FramesToRecover {
// Jumping and moving are only allowed here
effDx, effDy = decodedInput.Dx, decodedInput.Dy
}
patternId := PATTERN_ID_NO_OP
canJumpWithinInertia := currPlayerDownsync.CapturedByInertia && ((chConfig.InertiaFramesToRecover >> 1) > currPlayerDownsync.FramesToRecover)
if 0 == currPlayerDownsync.FramesToRecover || canJumpWithinInertia {
if decodedInput.BtnBLevel > prevBtnBLevel {
if _, existent := inAirSet[currPlayerDownsync.CharacterState]; !existent {
if chConfig.DashingEnabled && 0 > decodedInput.Dy {
// Checking "DashingEnabled" here to allow jumping when dashing-disabled players pressed "DOWN + BtnB"
patternId = 5
} else if _, existent := inAirSet[currPlayerDownsync.CharacterState]; !existent {
jumpedOrNot = true
} else if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState {
jumpedOrNot = true
@@ -490,18 +498,19 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
}
}
patternId := PATTERN_ID_NO_OP
if 0 < decodedInput.BtnALevel {
if decodedInput.BtnALevel > prevBtnALevel {
if 0 > effDy {
patternId = 3
} else if 0 < effDy {
patternId = 2
if PATTERN_ID_NO_OP == patternId {
if 0 < decodedInput.BtnALevel {
if decodedInput.BtnALevel > prevBtnALevel {
if 0 > decodedInput.Dy {
patternId = 3
} else if 0 < decodedInput.Dy {
patternId = 2
} else {
patternId = 1
}
} else {
patternId = 1
patternId = 4 // Holding
}
} else {
patternId = 4 // Holding
}
}
@@ -562,7 +571,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
chConfig := chConfigsOrderedByJoinIndex[i]
thatPlayerInNextFrame := nextRenderFramePlayers[i]
patternId, jumpedOrNot, effDx, effDy := deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame, currRenderFrame, inputsBuffer)
patternId, jumpedOrNot, effDx, effDy := deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame, currRenderFrame, chConfig, inputsBuffer)
jumpedOrNotList[i] = jumpedOrNot
joinIndex := currPlayerDownsync.JoinIndex
@@ -635,22 +644,24 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if 0 == currPlayerDownsync.FramesToRecover {
prevCapturedByInertia := currPlayerDownsync.CapturedByInertia
isWallJumping := (currPlayerDownsync.Speed < intAbs(currPlayerDownsync.VelX))
isWallJumping := (chConfig.OnWallEnabled && chConfig.WallJumpingInitVelX == intAbs(currPlayerDownsync.VelX))
/*
if isWallJumping {
fmt.Printf("joinIndex=%d is wall jumping\n{renderFrame.id: %d, currPlayerDownsync.Speed: %d, currPlayerDownsync.VelX: %d}\n", currPlayerDownsync.JoinIndex, currRenderFrame.Id, currPlayerDownsync.Speed, currPlayerDownsync.VelX)
}
*/
alignedWithInertia := true
exactTurningAround := false
if 0 == effDx && 0 != thatPlayerInNextFrame.VelX {
alignedWithInertia = false
} else if 0 != effDx && 0 == thatPlayerInNextFrame.VelX {
alignedWithInertia = false
} else if 0 > effDx*thatPlayerInNextFrame.VelX {
alignedWithInertia = false
exactTurningAround = true
}
if !isWallJumping && !prevCapturedByInertia && !alignedWithInertia {
if !jumpedOrNot && !isWallJumping && !prevCapturedByInertia && !alignedWithInertia {
/*
[WARNING] A "turn-around", or in more generic direction schema a "change in direction" is a hurdle for our current "prediction+rollback" approach, yet applying a "FramesToRecover" for "turn-around" can alleviate the graphical inconsistence to a huge extent! For better operational experience, this is intentionally NOT APPLIED TO WALL JUMPING!
@@ -659,6 +670,9 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
//fmt.Printf("joinIndex=%d is not wall jumping and not aligned w/ inertia\n{renderFrame.id: %d, effDx: %d, thatPlayerInNextFrame.VelX: %d}\n", currPlayerDownsync.JoinIndex, currRenderFrame.Id, effDx, thatPlayerInNextFrame.VelX)
thatPlayerInNextFrame.CapturedByInertia = true
thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover
if exactTurningAround {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_TURNAROUND
}
} else {
thatPlayerInNextFrame.CapturedByInertia = false
if 0 != effDx {
@@ -738,6 +752,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && !jumpedOrNotList[i] {
thatPlayerInNextFrame.VelX += GRAVITY_X
thatPlayerInNextFrame.VelY = chConfig.WallSlidingVelY
} else if ATK_CHARACTER_STATE_DASHING == currPlayerDownsync.CharacterState {
thatPlayerInNextFrame.VelX += GRAVITY_X
} else {
thatPlayerInNextFrame.VelX += GRAVITY_X
thatPlayerInNextFrame.VelY += GRAVITY_Y
@@ -1053,7 +1069,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if thatPlayerInNextFrame.InAir {
oldNextCharacterState := thatPlayerInNextFrame.CharacterState
switch oldNextCharacterState {
case ATK_CHARACTER_STATE_IDLE1, ATK_CHARACTER_STATE_WALKING:
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 {

View File

@@ -45,11 +45,11 @@ var Characters = map[int]*CharacterConfig{
GetUpInvinsibleFrames: int32(10),
GetUpFramesToRecover: int32(27),
Speed: int32(float64(2.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
Speed: int32(float64(2.1) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingInitVelY: int32(float64(8) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(5),
InertiaFramesToRecover: int32(9),
DashingEnabled: false,
OnWallEnabled: false,
@@ -94,11 +94,11 @@ var Characters = map[int]*CharacterConfig{
GetUpInvinsibleFrames: int32(10),
GetUpFramesToRecover: int32(27),
Speed: int32(float64(2.6) * WORLD_TO_VIRTUAL_GRID_RATIO),
Speed: int32(float64(2.19) * WORLD_TO_VIRTUAL_GRID_RATIO), // I don't know why "2.2" is so special that it throws a compile error
JumpingInitVelY: int32(float64(7.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(5),
InertiaFramesToRecover: int32(9),
DashingEnabled: true,
OnWallEnabled: true,
@@ -128,6 +128,9 @@ var Characters = map[int]*CharacterConfig{
}
}
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
return 12
}
// By default no skill can be fired
@@ -147,11 +150,11 @@ var Characters = map[int]*CharacterConfig{
GetUpInvinsibleFrames: int32(8),
GetUpFramesToRecover: int32(30),
Speed: int32(float64(2.0) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingInitVelY: int32(float64(7.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
Speed: int32(float64(1.8) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingInitVelY: int32(float64(7.8) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2),
InertiaFramesToRecover: int32(5),
InertiaFramesToRecover: int32(9),
DashingEnabled: false,
OnWallEnabled: false,
@@ -178,11 +181,11 @@ var Characters = map[int]*CharacterConfig{
}
}
} else if 2 == patternId {
if !currPlayerDownsync.InAir {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 11
}
} else if 3 == patternId {
if !currPlayerDownsync.InAir {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 10
}
}
@@ -508,7 +511,7 @@ var skills = map[int]*Skill{
PushbackVelX: int32(float64(2) * WORLD_TO_VIRTUAL_GRID_RATIO),
PushbackVelY: int32(0),
HitboxOffsetX: int32(float64(24) * WORLD_TO_VIRTUAL_GRID_RATIO),
HitboxOffsetY: int32(float64(5) * WORLD_TO_VIRTUAL_GRID_RATIO),
HitboxOffsetY: int32(float64(8) * WORLD_TO_VIRTUAL_GRID_RATIO),
HitboxSizeX: int32(float64(48) * WORLD_TO_VIRTUAL_GRID_RATIO),
HitboxSizeY: int32(float64(32) * WORLD_TO_VIRTUAL_GRID_RATIO),
BlowUp: false,
@@ -547,6 +550,33 @@ var skills = map[int]*Skill{
},
},
},
12: &Skill{
RecoveryFrames: int32(12),
RecoveryFramesOnBlock: int32(12),
RecoveryFramesOnHit: int32(12),
ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{
&MeleeBullet{
Bullet: &BulletConfig{
StartupFrames: int32(0),
ActiveFrames: int32(0),
HitStunFrames: MAX_INT32,
BlockStunFrames: int32(0),
Damage: int32(0),
SelfLockVelX: int32(float64(9) * WORLD_TO_VIRTUAL_GRID_RATIO),
SelfLockVelY: int32(0),
PushbackVelX: NO_LOCK_VEL,
PushbackVelY: NO_LOCK_VEL,
HitboxOffsetX: int32(0),
HitboxOffsetY: int32(0),
HitboxSizeX: int32(0),
HitboxSizeY: int32(0),
BlowUp: false,
},
},
},
},
255: &Skill{
RecoveryFrames: int32(30),
RecoveryFramesOnBlock: int32(30),