mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-19 13:39:18 +00:00
Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5d92b339f6 | ||
|
642adff919 | ||
|
62c51b1838 | ||
|
2751569e0c | ||
|
d623916b3c | ||
|
efd070a11b | ||
|
d111de0a7a | ||
|
c2fa251e69 | ||
|
de16e8e8de | ||
|
365177a3af | ||
|
b79e2dc935 | ||
|
7b0c807496 |
@@ -75,4 +75,18 @@ Instead of replacing all use of TCP by UDP, it's more reasonable to keep using T
|
||||
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.
|
||||

|
||||

|
||||
|
||||
# Would using WebRTC for all frontends be a `UDP for all` solution?
|
||||
Theoretically yes.
|
||||
|
||||
## Plan to integrate WebRTC
|
||||
The actual integration of WebRTC to enable `browser v.s. native app w/ WebRTC` requires detailed planning :)
|
||||
|
||||
In my current implementation, there's only 1 backend process and it's responsible for all of the following things. The plan for integrating/migrating each item is written respectively.
|
||||
- TURN for UDP tunneling/relay
|
||||
- Some minor modification to [Room.PlayerSecondaryDownsyncSessionDict](https://github.com/genxium/DelayNoMore/blob/365177a3af6033f1cd629a4a4d59beb4557cc311/battle_srv/models/room.go#L126) should be enough to yield a WebRTC API friendly TURN. It's interesting that [though UDP based in transport layer, a WebRTC session is stateful and more similar to WebSocket in terms of API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API).
|
||||
- STUN for UDP holepunching
|
||||
- Some minor modification to [Player.UdpAddr](https://github.com/genxium/DelayNoMore/blob/365177a3af6033f1cd629a4a4d59beb4557cc311/battle_srv/models/player.go#L56) should be enough to yield a WebRTC API friendly STUN.
|
||||
- reconnection recovery
|
||||
- Not sure whether or not I should separate this feature from STUN and TURN, but if I were to do so, [both `Room.RenderFrameBuffer` and `Room.InputsBuffer`](https://github.com/genxium/DelayNoMore/blob/365177a3af6033f1cd629a4a4d59beb4557cc311/battle_srv/models/room.go) should be moved to a shared fast I/O storage (e.g. using Redis) to achieve the same level of `High Availability` in design as STUN and TURN.
|
||||
|
26
README.md
26
README.md
@@ -6,10 +6,14 @@ This project is a demo for a websocket-based rollback netcode inspired by [GGPO]
|
||||
|
||||

|
||||
|
||||
(battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1RL-9M-cK8cFS_Q8afMTrJA?pwd=ryzv))
|
||||
|
||||

|
||||
|
||||
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. 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.
|
||||
- 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 (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw).
|
||||
- Browser vs `native app` is possible but in that case only websocket is used. For WebRTC integration plan please see [ConcerningEdgeCases](./ConcerningEdgeCases.md). You might also be interested in visiting [netplayjs](https://github.com/rameshvarun/netplayjs) to see how others use WebRTC for browser game synchronization as well.
|
||||
|
||||
|
||||
# Notable Features
|
||||
@@ -116,6 +120,24 @@ When building for native platforms, it's much more convenient to trigger the Coc
|
||||
```
|
||||
shell> cd <proj-root>
|
||||
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=true"
|
||||
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.win32 && MSBUILD DelayNoMore.vcxproj -property:Configuration=Debug
|
||||
```
|
||||
or
|
||||
```
|
||||
shell> cd <proj-root>
|
||||
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=false"
|
||||
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.win32 && MSBUILD DelayNoMore.vcxproj -property:Configuration=Release
|
||||
```
|
||||
|
||||
for release.
|
||||
|
||||
If `MSBUILD` command is not yet added to `PATH`, Use `Get-Command MSBUILD` in `Developer Command Prompt for VS 2017/2019` to see where the command should come from and add it to `PATH`.
|
||||
|
||||
Similarly for Android release build
|
||||
```
|
||||
shell> cd <proj-root>
|
||||
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=android;debug=false"
|
||||
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.android-studio && ./gradlew assembleRelease
|
||||
```
|
||||
|
||||
### 2.4 CococCreator native build reloading
|
||||
|
@@ -168,7 +168,7 @@ func (pR *Room) updateScore() {
|
||||
pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State)
|
||||
}
|
||||
|
||||
func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) bool {
|
||||
func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) bool {
|
||||
playerId := pPlayerFromDbInit.Id
|
||||
// TODO: Any thread-safety concern for accessing "pR" here?
|
||||
if RoomBattleStateIns.IDLE != pR.State && RoomBattleStateIns.WAITING != pR.State {
|
||||
@@ -180,7 +180,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
|
||||
return false
|
||||
}
|
||||
|
||||
defer pR.onPlayerAdded(playerId)
|
||||
defer pR.onPlayerAdded(playerId, speciesId)
|
||||
|
||||
pPlayerFromDbInit.UdpAddr = nil
|
||||
pPlayerFromDbInit.BattleUdpTunnelAddr = nil
|
||||
@@ -416,15 +416,6 @@ func (pR *Room) StartBattle() {
|
||||
|
||||
pR.RenderFrameId = 0
|
||||
|
||||
for _, player := range pR.Players {
|
||||
speciesId := int(player.JoinIndex - 1) // FIXME: Hardcoded the values for now
|
||||
if player.JoinIndex == 1 {
|
||||
speciesId = 4096
|
||||
}
|
||||
chosenCh := battle.Characters[speciesId]
|
||||
pR.CharacterConfigsArr[player.JoinIndex-1] = chosenCh
|
||||
pR.SpeciesIdList[player.JoinIndex-1] = int32(speciesId)
|
||||
}
|
||||
Logger.Info("[StartBattle] ", zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("SpeciesIdList", pR.SpeciesIdList))
|
||||
|
||||
// Initialize the "collisionSys" as well as "RenderFrameBuffer"
|
||||
@@ -954,7 +945,7 @@ func (pR *Room) clearPlayerNetworkSession(playerId int32) {
|
||||
}
|
||||
}
|
||||
|
||||
func (pR *Room) onPlayerAdded(playerId int32) {
|
||||
func (pR *Room) onPlayerAdded(playerId int32, speciesId int) {
|
||||
pR.EffectivePlayerCount++
|
||||
|
||||
if 1 == pR.EffectivePlayerCount {
|
||||
@@ -966,8 +957,9 @@ func (pR *Room) onPlayerAdded(playerId int32) {
|
||||
pR.Players[playerId].JoinIndex = int32(index) + 1
|
||||
pR.JoinIndexBooleanArr[index] = true
|
||||
|
||||
speciesId := index // FIXME
|
||||
pR.SpeciesIdList[index] = int32(speciesId)
|
||||
chosenCh := battle.Characters[speciesId]
|
||||
pR.CharacterConfigsArr[index] = chosenCh
|
||||
pR.Players[playerId].Speed = chosenCh.Speed
|
||||
|
||||
// Lazily assign the initial position of "Player" for "RoomDownsyncFrame".
|
||||
|
@@ -50,7 +50,17 @@ func Serve(c *gin.Context) {
|
||||
|
||||
boundRoomId := 0
|
||||
expectedRoomId := 0
|
||||
speciesId := 0
|
||||
var err error
|
||||
if speciesIdStr, hasSpeciesId := c.GetQuery("speciesId"); hasSpeciesId {
|
||||
speciesId, err = strconv.Atoi(speciesIdStr)
|
||||
if err != nil {
|
||||
// TODO: Abort with specific message.
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if boundRoomIdStr, hasBoundRoomId := c.GetQuery("boundRoomId"); hasBoundRoomId {
|
||||
boundRoomId, err = strconv.Atoi(boundRoomIdStr)
|
||||
if err != nil {
|
||||
@@ -195,7 +205,7 @@ func Serve(c *gin.Context) {
|
||||
|
||||
if pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) {
|
||||
playerSuccessfullyAddedToRoom = true
|
||||
} else if pRoom.AddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) {
|
||||
} else if pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer) {
|
||||
playerSuccessfullyAddedToRoom = true
|
||||
} else {
|
||||
Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forExpectedRoomId", expectedRoomId))
|
||||
@@ -219,7 +229,7 @@ func Serve(c *gin.Context) {
|
||||
} else {
|
||||
pRoom = tmpRoom
|
||||
Logger.Info("Successfully popped:\n", zap.Any("roomId", pRoom.Id), zap.Any("forPlayerId", playerId))
|
||||
res := pRoom.AddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer)
|
||||
res := pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer)
|
||||
if !res {
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.PlayerNotAddableToRoom, fmt.Sprintf("AddPlayerIfPossible returns false for roomId == %v, playerId == %v!", pRoom.Id, playerId))
|
||||
}
|
||||
|
BIN
charts/Phone4g_battle_spedup.gif
Normal file
BIN
charts/Phone4g_battle_spedup.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 MiB |
@@ -48,13 +48,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"frame": 0.45,
|
||||
"frame": 0.4,
|
||||
"value": {
|
||||
"__uuid__": "487b65c3-44e3-4b0e-9350-e0d1c952785b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"frame": 0.5,
|
||||
"frame": 0.4166666666666667,
|
||||
"value": {
|
||||
"__uuid__": "9a5357ae-a160-4198-a6d5-cc9631fde754"
|
||||
}
|
||||
|
@@ -59,6 +59,12 @@
|
||||
"__uuid__": "0ecf4a0c-0f13-42fa-a214-b4826acd8556"
|
||||
}
|
||||
},
|
||||
{
|
||||
"frame": 0.3,
|
||||
"value": {
|
||||
"__uuid__": "cabf9cb6-99ca-426d-9a23-95cdec6f06b9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"frame": 0.3333333333333333,
|
||||
"value": {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -88,7 +88,7 @@
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 3
|
||||
|
@@ -77,12 +77,6 @@
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 49
|
||||
},
|
||||
{
|
||||
"__id__": 50
|
||||
},
|
||||
{
|
||||
"__id__": 51
|
||||
},
|
||||
@@ -91,6 +85,12 @@
|
||||
},
|
||||
{
|
||||
"__id__": 53
|
||||
},
|
||||
{
|
||||
"__id__": 54
|
||||
},
|
||||
{
|
||||
"__id__": 55
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -244,10 +244,10 @@
|
||||
"__uuid__": "670b477e-61a1-4778-879b-35913f7c79d2"
|
||||
},
|
||||
"boundRoomIdLabel": {
|
||||
"__id__": 26
|
||||
"__id__": 28
|
||||
},
|
||||
"countdownLabel": {
|
||||
"__id__": 33
|
||||
"__id__": 35
|
||||
},
|
||||
"resultPanelPrefab": {
|
||||
"__uuid__": "c4cfe3bd-c59e-4d5b-95cb-c933b120e184"
|
||||
@@ -261,26 +261,26 @@
|
||||
"countdownToBeginGamePrefab": {
|
||||
"__uuid__": "230eeb1f-e0f9-4a41-ab6c-05b3771cbf3e"
|
||||
},
|
||||
"playersInfoPrefab": {
|
||||
"__uuid__": "b4e519f4-e698-4403-9ff2-47b8dacb077e"
|
||||
},
|
||||
"forceBigEndianFloatingNumDecoding": false,
|
||||
"renderFrameIdLagTolerance": 4,
|
||||
"sendingQLabel": {
|
||||
"inputFrameFrontLabel": {
|
||||
"__id__": 13
|
||||
},
|
||||
"inputFrameDownsyncQLabel": {
|
||||
"sendingQLabel": {
|
||||
"__id__": 15
|
||||
},
|
||||
"peerInputFrameUpsyncQLabel": {
|
||||
"inputFrameDownsyncQLabel": {
|
||||
"__id__": 17
|
||||
},
|
||||
"rollbackFramesLabel": {
|
||||
"peerInputFrameUpsyncQLabel": {
|
||||
"__id__": 19
|
||||
},
|
||||
"skippedRenderFrameCntLabel": {
|
||||
"rollbackFramesLabel": {
|
||||
"__id__": 21
|
||||
},
|
||||
"skippedRenderFrameCntLabel": {
|
||||
"__id__": 23
|
||||
},
|
||||
"_id": "d12gkAmppNlIzqcRDELa91"
|
||||
},
|
||||
{
|
||||
@@ -292,13 +292,13 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 48
|
||||
"__id__": 50
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -360,20 +360,20 @@
|
||||
{
|
||||
"__id__": 6
|
||||
},
|
||||
{
|
||||
"__id__": 32
|
||||
},
|
||||
{
|
||||
"__id__": 34
|
||||
},
|
||||
{
|
||||
"__id__": 38
|
||||
"__id__": 36
|
||||
},
|
||||
{
|
||||
"__id__": 40
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 42
|
||||
"__id__": 44
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -442,7 +442,7 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 31
|
||||
"__id__": 33
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -536,7 +536,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
216.50635094610968,
|
||||
216.65450766436658,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -620,15 +620,18 @@
|
||||
},
|
||||
{
|
||||
"__id__": 22
|
||||
},
|
||||
{
|
||||
"__id__": 24
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 29
|
||||
"__id__": 31
|
||||
},
|
||||
{
|
||||
"__id__": 30
|
||||
"__id__": 32
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -681,7 +684,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "sendingQ",
|
||||
"_name": "inputFrameFront",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
@@ -739,7 +742,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "ack7XH+0lEj6chSUsKBLU5"
|
||||
"_id": "83n6IAj9tAkop6CA/MyM0A"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -771,11 +774,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "deCJfLuoFO36c/O4lXWJ1N"
|
||||
"_id": "a4CfeVmexNm7nULcCRHcVl"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "inputFrameDownsyncQ",
|
||||
"_name": "sendingQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
@@ -833,7 +836,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "d9n+NRm9pA0YtKxvy4bW+U"
|
||||
"_id": "ack7XH+0lEj6chSUsKBLU5"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -865,11 +868,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "90NvjGFrpBFqqzl1WZczta"
|
||||
"_id": "deCJfLuoFO36c/O4lXWJ1N"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "peerInputFrameUpsyncQ",
|
||||
"_name": "inputFrameDownsyncQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
@@ -927,7 +930,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "45FAmgRLZNNbLt/GcnTXVx"
|
||||
"_id": "d9n+NRm9pA0YtKxvy4bW+U"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -959,11 +962,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "42PRTrjEpDw6Z8N5yWFTpd"
|
||||
"_id": "90NvjGFrpBFqqzl1WZczta"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "rollbackFrames",
|
||||
"_name": "peerInputFrameUpsyncQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
@@ -1021,7 +1024,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "f5SBs3b1pPNbHbnqVLYsHp"
|
||||
"_id": "45FAmgRLZNNbLt/GcnTXVx"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -1053,11 +1056,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "77aNARt1VATLsjIzwbqvkh"
|
||||
"_id": "42PRTrjEpDw6Z8N5yWFTpd"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "skippedCnt",
|
||||
"_name": "rollbackFrames",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
@@ -1073,9 +1076,9 @@
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 243,
|
||||
"g": 225,
|
||||
"b": 11,
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_contentSize": {
|
||||
@@ -1115,7 +1118,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "cdlF8Z8TZEdLRHQQ8T8qX7"
|
||||
"_id": "f5SBs3b1pPNbHbnqVLYsHp"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -1147,6 +1150,100 @@
|
||||
"_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__": 23
|
||||
}
|
||||
],
|
||||
"_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,
|
||||
-24.355000000000018,
|
||||
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__": 22
|
||||
},
|
||||
"_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"
|
||||
},
|
||||
{
|
||||
@@ -1158,19 +1255,19 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
{
|
||||
"__id__": 25
|
||||
"__id__": 27
|
||||
}
|
||||
],
|
||||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 27
|
||||
"__id__": 29
|
||||
},
|
||||
{
|
||||
"__id__": 28
|
||||
"__id__": 30
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1226,13 +1323,13 @@
|
||||
"_name": "label",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 22
|
||||
"__id__": 24
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 24
|
||||
"__id__": 26
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1288,7 +1385,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1316,13 +1413,13 @@
|
||||
"_name": "BoundRoomIdLabel",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 22
|
||||
"__id__": 24
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 26
|
||||
"__id__": 28
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1378,7 +1475,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 25
|
||||
"__id__": 27
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1406,7 +1503,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 22
|
||||
"__id__": 24
|
||||
},
|
||||
"_enabled": true,
|
||||
"_layoutSize": {
|
||||
@@ -1439,7 +1536,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 22
|
||||
"__id__": 24
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1561,7 +1658,7 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 33
|
||||
"__id__": 35
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1617,7 +1714,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 32
|
||||
"__id__": 34
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1651,7 +1748,7 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
@@ -1709,16 +1806,16 @@
|
||||
"_name": "Background",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 34
|
||||
"__id__": 36
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 36
|
||||
"__id__": 38
|
||||
},
|
||||
{
|
||||
"__id__": 37
|
||||
"__id__": 39
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1774,7 +1871,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1808,7 +1905,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -1839,7 +1936,7 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
@@ -1897,16 +1994,16 @@
|
||||
"_name": "Background",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 38
|
||||
"__id__": 40
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 40
|
||||
"__id__": 42
|
||||
},
|
||||
{
|
||||
"__id__": 41
|
||||
"__id__": 43
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1962,7 +2059,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1996,7 +2093,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -2054,16 +2151,16 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 46
|
||||
"__id__": 48
|
||||
},
|
||||
{
|
||||
"__id__": 47
|
||||
"__id__": 49
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -2119,13 +2216,13 @@
|
||||
"_name": "Joystick",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 45
|
||||
"__id__": 47
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -2181,7 +2278,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -2215,7 +2312,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -2249,7 +2346,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -2384,10 +2481,10 @@
|
||||
"__id__": 3
|
||||
},
|
||||
"stickhead": {
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
},
|
||||
"base": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"joyStickEps": 0.1,
|
||||
"magicLeanLowerBound": 0.414,
|
||||
@@ -2408,10 +2505,10 @@
|
||||
"linearMovingEps": 0.1,
|
||||
"scaleByEps": 0.0375,
|
||||
"btnA": {
|
||||
"__id__": 34
|
||||
"__id__": 36
|
||||
},
|
||||
"btnB": {
|
||||
"__id__": 38
|
||||
"__id__": 40
|
||||
},
|
||||
"_id": "e9oVYTr7ROlpp/IrNjBUmR"
|
||||
}
|
||||
|
@@ -461,7 +461,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
216.50635094610968,
|
||||
210.4441731196186,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@@ -77,12 +77,6 @@
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 49
|
||||
},
|
||||
{
|
||||
"__id__": 50
|
||||
},
|
||||
{
|
||||
"__id__": 51
|
||||
},
|
||||
@@ -91,6 +85,12 @@
|
||||
},
|
||||
{
|
||||
"__id__": 53
|
||||
},
|
||||
{
|
||||
"__id__": 54
|
||||
},
|
||||
{
|
||||
"__id__": 55
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -276,21 +276,23 @@
|
||||
"gameRulePrefab": null,
|
||||
"findingPlayerPrefab": null,
|
||||
"countdownToBeginGamePrefab": null,
|
||||
"playersInfoPrefab": null,
|
||||
"forceBigEndianFloatingNumDecoding": false,
|
||||
"renderFrameIdLagTolerance": 4,
|
||||
"sendingQLabel": {
|
||||
"inputFrameFrontLabel": {
|
||||
"__id__": 14
|
||||
},
|
||||
"inputFrameDownsyncQLabel": {
|
||||
"sendingQLabel": {
|
||||
"__id__": 16
|
||||
},
|
||||
"peerInputFrameUpsyncQLabel": {
|
||||
"inputFrameDownsyncQLabel": {
|
||||
"__id__": 18
|
||||
},
|
||||
"rollbackFramesLabel": {
|
||||
"peerInputFrameUpsyncQLabel": {
|
||||
"__id__": 20
|
||||
},
|
||||
"rollbackFramesLabel": {
|
||||
"__id__": 22
|
||||
},
|
||||
"skippedRenderFrameCntLabel": null,
|
||||
"_id": "e5xQdv12xLoIRr0b36Pie+"
|
||||
},
|
||||
@@ -303,13 +305,13 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 48
|
||||
"__id__": 50
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -371,20 +373,20 @@
|
||||
{
|
||||
"__id__": 7
|
||||
},
|
||||
{
|
||||
"__id__": 32
|
||||
},
|
||||
{
|
||||
"__id__": 34
|
||||
},
|
||||
{
|
||||
"__id__": 38
|
||||
"__id__": 36
|
||||
},
|
||||
{
|
||||
"__id__": 40
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 42
|
||||
"__id__": 44
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -453,7 +455,7 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 31
|
||||
"__id__": 33
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -547,7 +549,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
216.50635094610968,
|
||||
210.4441731196186,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -631,12 +633,15 @@
|
||||
},
|
||||
{
|
||||
"__id__": 23
|
||||
},
|
||||
{
|
||||
"__id__": 25
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 30
|
||||
"__id__": 32
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -689,7 +694,7 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "sendingQ",
|
||||
"_name": "inputFrameFront",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 12
|
||||
@@ -725,7 +730,7 @@
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
76.48,
|
||||
79.66666666666667,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -747,7 +752,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "6fOGsdJxxKMLM7+K16V2wq"
|
||||
"_id": "16ecz642FAMIrtMtKWTI/3"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -777,11 +782,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "38SxePoOdHT4wrqnMdR0ql"
|
||||
"_id": "e2W8Kja+VG7IEwuBQapscC"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "inputFrameDownsyncQ",
|
||||
"_name": "sendingQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 12
|
||||
@@ -817,7 +822,7 @@
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
38.239999999999995,
|
||||
47.8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -839,7 +844,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "c7OaUtV+9JaL4UcCC0gp8r"
|
||||
"_id": "6fOGsdJxxKMLM7+K16V2wq"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -869,11 +874,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "e22YW/mxlPRoYkJwMCezpE"
|
||||
"_id": "38SxePoOdHT4wrqnMdR0ql"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "peerInputFrameUpsyncQ",
|
||||
"_name": "inputFrameDownsyncQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 12
|
||||
@@ -909,7 +914,7 @@
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
-7.105427357601002e-15,
|
||||
15.933333333333325,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -931,7 +936,7 @@
|
||||
"_is3DNode": false,
|
||||
"_groupIndex": 0,
|
||||
"groupIndex": 0,
|
||||
"_id": "3fAMol6MJN+IHH1Ckh13wR"
|
||||
"_id": "c7OaUtV+9JaL4UcCC0gp8r"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
@@ -961,11 +966,11 @@
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "3dO5iM/4VHVb21M3X82p0z"
|
||||
"_id": "e22YW/mxlPRoYkJwMCezpE"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "rollbackFrames",
|
||||
"_name": "peerInputFrameUpsyncQ",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 12
|
||||
@@ -1001,7 +1006,99 @@
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
-38.24000000000001,
|
||||
-15.933333333333346,
|
||||
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": "3fAMol6MJN+IHH1Ckh13wR"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 19
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
{
|
||||
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
|
||||
}
|
||||
],
|
||||
"_useOriginalSize": false,
|
||||
"_string": "0",
|
||||
"_N$string": "0",
|
||||
"_fontSize": 22,
|
||||
"_lineHeight": 23,
|
||||
"_enableWrapText": true,
|
||||
"_N$file": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
"_batchAsBitmap": false,
|
||||
"_N$horizontalAlign": 0,
|
||||
"_N$verticalAlign": 1,
|
||||
"_N$fontFamily": "Arial",
|
||||
"_N$overflow": 0,
|
||||
"_N$cacheMode": 0,
|
||||
"_id": "3dO5iM/4VHVb21M3X82p0z"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "rollbackFrames",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 12
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 22
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
"_opacity": 255,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 12.24,
|
||||
"height": 28.98
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 0.5
|
||||
},
|
||||
"_trs": {
|
||||
"__type__": "TypedArray",
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
-47.80000000000002,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -1030,7 +1127,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 19
|
||||
"__id__": 21
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1066,7 +1163,7 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 22
|
||||
"__id__": 24
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1093,7 +1190,7 @@
|
||||
"ctor": "Float64Array",
|
||||
"array": [
|
||||
0,
|
||||
-76.48000000000002,
|
||||
-79.66666666666669,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -1122,7 +1219,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 21
|
||||
"__id__": 23
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1158,19 +1255,19 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 24
|
||||
"__id__": 26
|
||||
},
|
||||
{
|
||||
"__id__": 26
|
||||
"__id__": 28
|
||||
}
|
||||
],
|
||||
"_active": false,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 28
|
||||
"__id__": 30
|
||||
},
|
||||
{
|
||||
"__id__": 29
|
||||
"__id__": 31
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1226,13 +1323,13 @@
|
||||
"_name": "label",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 25
|
||||
"__id__": 27
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1288,7 +1385,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 24
|
||||
"__id__": 26
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1316,13 +1413,13 @@
|
||||
"_name": "BoundRoomIdLabel",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 27
|
||||
"__id__": 29
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1378,7 +1475,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 26
|
||||
"__id__": 28
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1406,7 +1503,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
"_enabled": true,
|
||||
"_layoutSize": {
|
||||
@@ -1439,7 +1536,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 23
|
||||
"__id__": 25
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [],
|
||||
@@ -1535,7 +1632,7 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 33
|
||||
"__id__": 35
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1591,7 +1688,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 32
|
||||
"__id__": 34
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1625,7 +1722,7 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
@@ -1683,16 +1780,16 @@
|
||||
"_name": "Background",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 34
|
||||
"__id__": 36
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 36
|
||||
"__id__": 38
|
||||
},
|
||||
{
|
||||
"__id__": 37
|
||||
"__id__": 39
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1748,7 +1845,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1782,7 +1879,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 37
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -1813,7 +1910,7 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
@@ -1871,16 +1968,16 @@
|
||||
"_name": "Background",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 38
|
||||
"__id__": 40
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 40
|
||||
"__id__": 42
|
||||
},
|
||||
{
|
||||
"__id__": 41
|
||||
"__id__": 43
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -1936,7 +2033,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -1970,7 +2067,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 39
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -2028,16 +2125,16 @@
|
||||
},
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 46
|
||||
"__id__": 48
|
||||
},
|
||||
{
|
||||
"__id__": 47
|
||||
"__id__": 49
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -2093,13 +2190,13 @@
|
||||
"_name": "Joystick",
|
||||
"_objFlags": 0,
|
||||
"_parent": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 45
|
||||
"__id__": 47
|
||||
}
|
||||
],
|
||||
"_prefab": null,
|
||||
@@ -2155,7 +2252,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -2189,7 +2286,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_enabled": true,
|
||||
"_materials": [
|
||||
@@ -2223,7 +2320,7 @@
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"_enabled": true,
|
||||
"alignMode": 0,
|
||||
@@ -2358,10 +2455,10 @@
|
||||
"__id__": 3
|
||||
},
|
||||
"stickhead": {
|
||||
"__id__": 44
|
||||
"__id__": 46
|
||||
},
|
||||
"base": {
|
||||
"__id__": 43
|
||||
"__id__": 45
|
||||
},
|
||||
"joyStickEps": 0.1,
|
||||
"magicLeanLowerBound": 0.414,
|
||||
@@ -2382,10 +2479,10 @@
|
||||
"linearMovingEps": 0.1,
|
||||
"scaleByEps": 0.0375,
|
||||
"btnA": {
|
||||
"__id__": 34
|
||||
"__id__": 36
|
||||
},
|
||||
"btnB": {
|
||||
"__id__": 38
|
||||
"__id__": 40
|
||||
},
|
||||
"_id": "e9oVYTr7ROlpp/IrNjBUmR"
|
||||
}
|
||||
|
41
frontend/assets/scripts/CharacterSelectCell.js
Normal file
41
frontend/assets/scripts/CharacterSelectCell.js
Normal file
@@ -0,0 +1,41 @@
|
||||
cc.Class({
|
||||
extends: cc.Component,
|
||||
properties: {
|
||||
panelNode: {
|
||||
type: cc.Node,
|
||||
default: null
|
||||
},
|
||||
chosenFlag: {
|
||||
type: cc.Sprite,
|
||||
default: null
|
||||
},
|
||||
avatarNode: {
|
||||
type: cc.Button,
|
||||
default: null
|
||||
},
|
||||
animNode: {
|
||||
type: cc.Node,
|
||||
default: null
|
||||
},
|
||||
speciesId: {
|
||||
type: cc.Integer,
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
|
||||
ctor() {},
|
||||
|
||||
setInteractable(enabled) {
|
||||
this.avatarNode.interactable = enabled;
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const avatarNodeClickEventHandler = new cc.Component.EventHandler();
|
||||
avatarNodeClickEventHandler.target = this.panelNode;
|
||||
avatarNodeClickEventHandler.component = "GameRule";
|
||||
avatarNodeClickEventHandler.handler = "onSpeciesSelected";
|
||||
avatarNodeClickEventHandler.customEventData = this.speciesId;
|
||||
this.avatarNode.clickEvents.push(avatarNodeClickEventHandler);
|
||||
},
|
||||
});
|
||||
|
9
frontend/assets/scripts/CharacterSelectCell.js.meta
Normal file
9
frontend/assets/scripts/CharacterSelectCell.js.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "6dd2c047-fa5c-4080-8221-27fabfd275d6",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
@@ -47,7 +47,7 @@ cc.Class({
|
||||
},
|
||||
|
||||
hideExitButton() {
|
||||
if (null == this.exitBtnNode != null) {
|
||||
if (null == this.exitBtnNode) {
|
||||
return;
|
||||
}
|
||||
this.exitBtnNode.active = false;
|
||||
|
@@ -10,15 +10,46 @@ cc.Class({
|
||||
type: cc.Node,
|
||||
default: null
|
||||
},
|
||||
characterSelectCells: {
|
||||
type: cc.Node,
|
||||
default: []
|
||||
},
|
||||
chosenSpeciesId: {
|
||||
type: cc.Integer,
|
||||
default: 0
|
||||
},
|
||||
loadingNode: {
|
||||
default: null,
|
||||
type: cc.Node
|
||||
},
|
||||
},
|
||||
|
||||
// LIFE-CYCLE CALLBACKS:
|
||||
onLoad() {
|
||||
const modeBtnClickEventHandler = new cc.Component.EventHandler();
|
||||
modeBtnClickEventHandler.target = this.mapNode;
|
||||
modeBtnClickEventHandler.component = "Map";
|
||||
modeBtnClickEventHandler.handler = "onGameRule1v1ModeClicked";
|
||||
this.modeButton.clickEvents.push(modeBtnClickEventHandler);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
onSpeciesSelected(evt, val) {
|
||||
for (let cell of this.characterSelectCells) {
|
||||
const comp = cell.getComponent("CharacterSelectCell");
|
||||
if (comp.speciesId != val) {
|
||||
comp.chosenFlag.node.active = false;
|
||||
} else {
|
||||
comp.chosenFlag.node.active = true;
|
||||
this.chosenSpeciesId = val;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onModeButtonClicked(evt) {
|
||||
for (let cell of this.characterSelectCells) {
|
||||
const comp = cell.getComponent("CharacterSelectCell");
|
||||
comp.setInteractable(false);
|
||||
}
|
||||
this.modeButton.node.active = false;
|
||||
this.loadingNode.active = true;
|
||||
this.loadingNode.runAction(
|
||||
cc.repeatForever(cc.rotateBy(1.0, 360))
|
||||
);
|
||||
this.mapNode.getComponent("Map").onGameRule1v1ModeClicked(this.chosenSpeciesId);
|
||||
},
|
||||
});
|
||||
|
@@ -354,6 +354,7 @@ cc.Class({
|
||||
self.loadingNode.getChildByName('loadingSprite').runAction(
|
||||
cc.repeatForever(cc.rotateBy(1.0, 360))
|
||||
);
|
||||
self.loadingNode.getChildByName('loadingLabel').active = true;
|
||||
cc.director.loadScene('default_map');
|
||||
} else {
|
||||
console.log("OnLoggedIn failed, about to remove `selfPlayer` in local cache.")
|
||||
|
@@ -103,10 +103,6 @@ cc.Class({
|
||||
type: cc.Prefab,
|
||||
default: null
|
||||
},
|
||||
playersInfoPrefab: {
|
||||
type: cc.Prefab,
|
||||
default: null
|
||||
},
|
||||
forceBigEndianFloatingNumDecoding: {
|
||||
default: false,
|
||||
},
|
||||
@@ -114,6 +110,10 @@ cc.Class({
|
||||
type: cc.Integer,
|
||||
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
|
||||
},
|
||||
inputFrameFrontLabel: {
|
||||
type: cc.Label,
|
||||
default: null
|
||||
},
|
||||
sendingQLabel: {
|
||||
type: cc.Label,
|
||||
default: null
|
||||
@@ -280,13 +280,6 @@ cc.Class({
|
||||
}
|
||||
},
|
||||
|
||||
onManualRejoinRequired(labelString) {
|
||||
const self = this;
|
||||
self.battleState = ALL_BATTLE_STATES.NONE; // Effectively stops "update(dt)"
|
||||
self.showPopupInCanvas(self.gameRuleNode);
|
||||
self.popupSimplePressToGo(labelString, false);
|
||||
},
|
||||
|
||||
popupSimplePressToGo(labelString, hideYesButton) {
|
||||
const self = this;
|
||||
self.state = ALL_MAP_STATES.SHOWING_MODAL_POPUP;
|
||||
@@ -392,20 +385,12 @@ cc.Class({
|
||||
self.rdfIdToActuallyUsedInput = new Map();
|
||||
|
||||
self.networkDoctor = new NetworkDoctor(20);
|
||||
self.allowSkippingRenderFrameFlag = true;
|
||||
self.skipRenderFrameFlag = false;
|
||||
|
||||
self.allowRollbackOnPeerUpsync = true;
|
||||
|
||||
self.countdownNanos = null;
|
||||
if (self.countdownLabel) {
|
||||
self.countdownLabel.string = "";
|
||||
}
|
||||
if (self.playersInfoNode) {
|
||||
safelyAddChild(self.widgetsAboveAllNode, self.playersInfoNode);
|
||||
}
|
||||
if (self.findingPlayerNode) {
|
||||
safelyAddChild(self.widgetsAboveAllNode, self.findingPlayerNode);
|
||||
}
|
||||
},
|
||||
|
||||
initDebugDrawers() {
|
||||
@@ -508,8 +493,6 @@ cc.Class({
|
||||
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
|
||||
findingPlayerScriptIns.init(self);
|
||||
|
||||
self.playersInfoNode = cc.instantiate(self.playersInfoPrefab);
|
||||
|
||||
self.countdownToBeginGameNode = cc.instantiate(self.countdownToBeginGamePrefab);
|
||||
self.countdownToBeginGameNode.width = self.canvasNode.width;
|
||||
self.countdownToBeginGameNode.height = self.canvasNode.height;
|
||||
@@ -551,6 +534,11 @@ cc.Class({
|
||||
tiledMapIns.tmxAsset = null;
|
||||
mapNode.removeAllChildren();
|
||||
self._resetCurrentMatch();
|
||||
if (self.countdownLabel) {
|
||||
self.countdownLabel.string = "";
|
||||
}
|
||||
self.hideGameRuleNode();
|
||||
self.showFindingPlayerGUI(null);
|
||||
|
||||
tiledMapIns.tmxAsset = tmxAsset;
|
||||
const newMapSize = tiledMapIns.getMapSize();
|
||||
@@ -597,7 +585,6 @@ cc.Class({
|
||||
|
||||
self.initAfterWSConnected = () => {
|
||||
const self = window.mapIns;
|
||||
self.hideGameRuleNode();
|
||||
self.transitToState(ALL_MAP_STATES.WAITING);
|
||||
self._inputControlEnabled = false;
|
||||
}
|
||||
@@ -643,7 +630,8 @@ cc.Class({
|
||||
if (null == self.gameRuleNode) {
|
||||
return;
|
||||
}
|
||||
self.gameRuleNode.active = false;
|
||||
//self.gameRuleNode.active = false;
|
||||
self.gameRuleNode.setPosition(cc.v2(Number.MAX_VALUE, Number.MAX_VALUE));
|
||||
},
|
||||
|
||||
enableInputControls() {
|
||||
@@ -718,14 +706,6 @@ cc.Class({
|
||||
self.chConfigsOrderedByJoinIndex = gopkgs.GetCharacterConfigsOrderedByJoinIndex(pbRdf.speciesIdList);
|
||||
self._initPlayerRichInfoDict(rdf.PlayersArr);
|
||||
|
||||
// Show the top status indicators for IN_BATTLE
|
||||
if (self.playersInfoNode) {
|
||||
const playersInfoScriptIns = self.playersInfoNode.getComponent("PlayersInfo");
|
||||
for (let i in pbRdf.playersArr) {
|
||||
playersInfoScriptIns.updateData(pbRdf.playersArr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldForceDumping1 || shouldForceDumping2 || shouldForceResync) {
|
||||
// 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
|
||||
|
||||
@@ -923,12 +903,15 @@ cc.Class({
|
||||
--------------------------------------------------------
|
||||
*/
|
||||
// The actual rollback-and-chase would later be executed in update(dt).
|
||||
console.log(`Mismatched input detected, resetting chaserRenderFrameId: ${self.chaserRenderFrameId}->${renderFrameId1} by
|
||||
if (CC_DEBUG) {
|
||||
// Printing of this message might induce a performance impact.
|
||||
console.log(`Mismatched input detected, resetting chaserRenderFrameId: ${self.chaserRenderFrameId}->${renderFrameId1} by
|
||||
firstPredictedYetIncorrectInputFrameId: ${firstPredictedYetIncorrectInputFrameId}
|
||||
lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}
|
||||
recentInputCache=${self._stringifyRecentInputCache(false)}
|
||||
batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}]
|
||||
fromUDP=${fromUDP}`);
|
||||
}
|
||||
self.chaserRenderFrameId = renderFrameId1;
|
||||
let rollbackFrames = (self.renderFrameId - self.chaserRenderFrameId);
|
||||
if (0 > rollbackFrames) {
|
||||
@@ -986,7 +969,7 @@ fromUDP=${fromUDP}`);
|
||||
//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) {
|
||||
if (true == 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 (
|
||||
@@ -1009,12 +992,7 @@ fromUDP=${fromUDP}`);
|
||||
|
||||
onPlayerAdded(rdf /* pb.RoomDownsyncFrame */ ) {
|
||||
const self = this;
|
||||
// Update the "finding player" GUI and show it if not previously present
|
||||
if (!self.findingPlayerNode.parent) {
|
||||
self.showPopupInCanvas(self.findingPlayerNode);
|
||||
}
|
||||
let findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
|
||||
findingPlayerScriptIns.updatePlayersInfo(rdf.playersArr);
|
||||
self.showFindingPlayerGUI(rdf);
|
||||
},
|
||||
|
||||
onBattleStopped() {
|
||||
@@ -1034,9 +1012,6 @@ fromUDP=${fromUDP}`);
|
||||
resultPanelScriptIns.showPlayerInfo(self.playerRichInfoDict);
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
self.showPopupInCanvas(resultPanelNode);
|
||||
|
||||
// Clear player info
|
||||
self.playersInfoNode.getComponent("PlayersInfo").clearInfo();
|
||||
},
|
||||
|
||||
spawnPlayerNode(joinIndex, vx, vy, playerDownsyncInfo) {
|
||||
@@ -1077,18 +1052,22 @@ fromUDP=${fromUDP}`);
|
||||
|
||||
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) {
|
||||
if (self.allowSkippingRenderFrameFlag && self.skipRenderFrameFlag) {
|
||||
self.networkDoctor.logSkippedRenderFrameCnt();
|
||||
self.skipRenderFrameFlag = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let st = performance.now();
|
||||
if (cc.sys.isNative) {
|
||||
DelayNoMore.UdpSession.pollUdpRecvRingBuff();
|
||||
}
|
||||
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId);
|
||||
let prevSelfInput = null,
|
||||
currSelfInput = null;
|
||||
if (gopkgs.ShouldGenerateInputFrameUpsync(self.renderFrameId)) {
|
||||
[prevSelfInput, currSelfInput] = self.getOrPrefabInputFrameUpsync(noDelayInputFrameId, true);
|
||||
self.networkDoctor.logInputFrameIdFront(noDelayInputFrameId);
|
||||
}
|
||||
|
||||
const delayedInputFrameId = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
|
||||
@@ -1140,21 +1119,22 @@ fromUDP=${fromUDP}`);
|
||||
console.warn(`Mismatched render frame@rdf.id=${rdf.Id} w/ inputFrameId=${delayedInputFrameId}:
|
||||
rdf=${JSON.stringify(rdf)}
|
||||
othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame)}`);
|
||||
// closeWSConnection(constants.RET_CODE.CLIENT_MISMATCHED_RENDER_FRAME, "");
|
||||
// self.onManualRejoinRequired("[DEBUG] CLIENT_MISMATCHED_RENDER_FRAME");
|
||||
rdf = othersForcedDownsyncRenderFrame;
|
||||
self.othersForcedDownsyncRenderFrameDict.delete(rdf.Id);
|
||||
}
|
||||
}
|
||||
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(self);
|
||||
const [skipRenderFrameFlag, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, doctorRollbackFrames, skippedRenderFrameCnt] = self.networkDoctor.isTooFast(self);
|
||||
if (self.allowSkippingRenderFrameFlag) {
|
||||
self.skipRenderFrameFlag = skipRenderFrameFlag;
|
||||
}
|
||||
if (self.showNetworkDoctorInfo) {
|
||||
self.showNetworkDoctorLabels(inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, doctorRollbackFrames, skippedRenderFrameCnt);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error during Map.update", err);
|
||||
self.onBattleStopped(); // TODO: Popup to ask player to refresh browser
|
||||
@@ -1227,11 +1207,11 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
|
||||
self.enableInputControls();
|
||||
},
|
||||
|
||||
onGameRule1v1ModeClicked(evt, cb) {
|
||||
onGameRule1v1ModeClicked(chosenSpeciesId) {
|
||||
const self = this;
|
||||
self.battleState = ALL_BATTLE_STATES.WAITING;
|
||||
window.chosenSpeciesId = chosenSpeciesId; // TODO: Find a better way to pass it into "self.initAfterWSConnected"!
|
||||
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
||||
self.hideGameRuleNode();
|
||||
},
|
||||
|
||||
showPopupInCanvas(toShowNode) {
|
||||
@@ -1243,6 +1223,18 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
|
||||
setLocalZOrder(toShowNode, 10);
|
||||
},
|
||||
|
||||
showFindingPlayerGUI(rdf) {
|
||||
const self = this;
|
||||
// Update the "finding player" GUI and show it if not previously present
|
||||
if (!self.findingPlayerNode.parent) {
|
||||
self.showPopupInCanvas(self.findingPlayerNode);
|
||||
}
|
||||
if (null != rdf) {
|
||||
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
|
||||
findingPlayerScriptIns.updatePlayersInfo(rdf.playersArr);
|
||||
}
|
||||
},
|
||||
|
||||
hideFindingPlayersGUI(rdf) {
|
||||
const self = this;
|
||||
// [WARNING] "cc.Node.removeChild" would trigger massive update of rendering nodes, thus a performance impact at the beginning of battle, avoid it by just moving the widget to infinitely far away!
|
||||
@@ -1255,13 +1247,6 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
|
||||
const self = this;
|
||||
const players = rdf.playersArr;
|
||||
|
||||
// Show the top status indicators for IN_BATTLE
|
||||
if (self.playersInfoNode) {
|
||||
const playersInfoScriptIns = self.playersInfoNode.getComponent("PlayersInfo");
|
||||
for (let i in players) {
|
||||
playersInfoScriptIns.updateData(players[i]);
|
||||
}
|
||||
}
|
||||
console.log("Calling `onBattleReadyToStart` with:", players);
|
||||
if (self.findingPlayerNode) {
|
||||
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
|
||||
@@ -1303,7 +1288,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
|
||||
}
|
||||
for (let k in rdf.MeleeBullets) {
|
||||
const meleeBullet = rdf.MeleeBullets[k];
|
||||
const isExploding = (window.BULLET_STATE.Exploding == meleeBullet.BlState);
|
||||
const isExploding = (window.BULLET_STATE.Exploding == meleeBullet.BlState && meleeBullet.FramesInBlState < meleeBullet.Bullet.ExplosionFrames);
|
||||
if (isExploding) {
|
||||
let pqNode = self.cachedFireballs.popAny(meleeBullet.BattleAttr.BulletLocalId);
|
||||
let speciesName = `MeleeExplosion`;
|
||||
@@ -1648,11 +1633,13 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
|
||||
}
|
||||
},
|
||||
|
||||
showNetworkDoctorLabels() {
|
||||
showNetworkDoctorLabels(inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt) {
|
||||
const self = this;
|
||||
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = self.networkDoctor.stats();
|
||||
if (self.inputFrameFrontLabel) {
|
||||
self.inputFrameFrontLabel.string = `inputFrameId front: ${inputFrameIdFront}`;
|
||||
}
|
||||
if (self.sendingQLabel) {
|
||||
self.sendingQLabel.string = `${sendingFps} fps sending`;
|
||||
self.sendingQLabel.string = `fps sending: ${sendingFps}`;
|
||||
if (sendingFps < self.networkDoctor.inputRateThreshold) {
|
||||
self.sendingQLabel.node.color = cc.Color.RED;
|
||||
} else {
|
||||
@@ -1660,7 +1647,7 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
|
||||
}
|
||||
}
|
||||
if (self.inputFrameDownsyncQLabel) {
|
||||
self.inputFrameDownsyncQLabel.string = `${srvDownsyncFps} fps srv-downsync`;
|
||||
self.inputFrameDownsyncQLabel.string = `fps srv-downsync: ${srvDownsyncFps}`;
|
||||
if (srvDownsyncFps < self.networkDoctor.inputRateThreshold) {
|
||||
self.inputFrameDownsyncQLabel.node.color = cc.Color.RED;
|
||||
} else {
|
||||
@@ -1668,7 +1655,7 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
|
||||
}
|
||||
}
|
||||
if (self.peerInputFrameUpsyncQLabel) {
|
||||
self.peerInputFrameUpsyncQLabel.string = `${peerUpsyncFps} fps peer-upsync`;
|
||||
self.peerInputFrameUpsyncQLabel.string = `fps peer-upsync: ${peerUpsyncFps}`;
|
||||
if (peerUpsyncFps > self.networkDoctor.peerUpsyncFps) {
|
||||
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.RED;
|
||||
} else {
|
||||
@@ -1684,7 +1671,7 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
|
||||
}
|
||||
}
|
||||
if (self.skippedRenderFrameCntLabel) {
|
||||
self.skippedRenderFrameCntLabel.string = `${skippedRenderFrameCnt} frames skipped`
|
||||
self.skippedRenderFrameCntLabel.string = `frames skipped: ${skippedRenderFrameCnt}`
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@@ -5,6 +5,7 @@ var NetworkDoctor = function(capacity) {
|
||||
};
|
||||
|
||||
NetworkDoctor.prototype.reset = function(capacity) {
|
||||
this.inputFrameIdFront = 0;
|
||||
this.sendingQ = new RingBuffer(capacity);
|
||||
this.inputFrameDownsyncQ = new RingBuffer(capacity);
|
||||
this.peerInputFrameUpsyncQ = new RingBuffer(capacity);
|
||||
@@ -17,6 +18,10 @@ NetworkDoctor.prototype.reset = function(capacity) {
|
||||
this.rollbackFramesThreshold = 8; // Roughly the minimum "TurnAroundFramesToRecover".
|
||||
};
|
||||
|
||||
NetworkDoctor.prototype.logInputFrameIdFront = function(inputFrameId) {
|
||||
this.inputFrameIdFront = inputFrameId;
|
||||
};
|
||||
|
||||
NetworkDoctor.prototype.logSending = function(stFrameId, edFrameId) {
|
||||
this.sendingQ.put({
|
||||
i: stFrameId,
|
||||
@@ -50,7 +55,8 @@ NetworkDoctor.prototype.logRollbackFrames = function(x) {
|
||||
};
|
||||
|
||||
NetworkDoctor.prototype.stats = function() {
|
||||
let sendingFps = 0,
|
||||
let inputFrameIdFront = this.inputFrameIdFront,
|
||||
sendingFps = 0,
|
||||
srvDownsyncFps = 0,
|
||||
peerUpsyncFps = 0,
|
||||
rollbackFrames = this.immediateRollbackFrames;
|
||||
@@ -72,7 +78,7 @@ NetworkDoctor.prototype.stats = function() {
|
||||
const elapsedMillis = ed.t - st.t;
|
||||
peerUpsyncFps = Math.round(this.peerInputFrameUpsyncCnt * 1000 / elapsedMillis);
|
||||
}
|
||||
return [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, this.skippedRenderFrameCnt];
|
||||
return [inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, this.skippedRenderFrameCnt];
|
||||
};
|
||||
|
||||
NetworkDoctor.prototype.logSkippedRenderFrameCnt = function() {
|
||||
@@ -80,31 +86,36 @@ NetworkDoctor.prototype.logSkippedRenderFrameCnt = function() {
|
||||
}
|
||||
|
||||
NetworkDoctor.prototype.isTooFast = function(mapIns) {
|
||||
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats();
|
||||
const [inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats();
|
||||
if (sendingFps >= this.inputRateThreshold + 3) {
|
||||
// Don't send too fast
|
||||
console.log(`Sending too fast, sendingFps=${sendingFps}`);
|
||||
return true;
|
||||
if (CC_DEBUG) {
|
||||
// Printing of this message might induce a performance impact.
|
||||
console.log(`Sending too fast, sendingFps=${sendingFps}`);
|
||||
}
|
||||
return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt];
|
||||
} else {
|
||||
const sendingFpsNormal = (sendingFps >= this.inputRateThreshold);
|
||||
// An outstanding lag within the "inputFrameDownsyncQ" will reduce "srvDownsyncFps", HOWEVER, a constant lag wouldn't impact "srvDownsyncFps"! In native platforms we might use PING value might help as a supplement information to confirm that the "selfPlayer" is not lagged within the time accounted by "inputFrameDownsyncQ".
|
||||
const recvFpsNormal = (srvDownsyncFps >= this.inputRateThreshold || peerUpsyncFps >= this.inputRateThreshold * (window.boundRoomCapacity - 1));
|
||||
if (sendingFpsNormal && recvFpsNormal) {
|
||||
let selfInputFrameIdFront = gopkgs.ConvertToNoDelayInputFrameId(mapIns.renderFrameId);
|
||||
let minInputFrameIdFront = Number.MAX_VALUE;
|
||||
for (let k = 0; k < window.boundRoomCapacity; ++k) {
|
||||
if (k + 1 == mapIns.selfPlayerInfo.JoinIndex) continue;
|
||||
if (mapIns.lastIndividuallyConfirmedInputFrameId[k] >= minInputFrameIdFront) continue;
|
||||
minInputFrameIdFront = mapIns.lastIndividuallyConfirmedInputFrameId[k];
|
||||
}
|
||||
if ((selfInputFrameIdFront > minInputFrameIdFront) && ((selfInputFrameIdFront - minInputFrameIdFront) > (mapIns.inputFrameUpsyncDelayTolerance + 1))) {
|
||||
if ((inputFrameIdFront > minInputFrameIdFront) && ((inputFrameIdFront - minInputFrameIdFront) > (mapIns.inputFrameUpsyncDelayTolerance + 1))) {
|
||||
// first comparison condition is to avoid numeric overflow
|
||||
console.log(`Game logic ticking too fast, selfInputFrameIdFront=${selfInputFrameIdFront}, minInputFrameIdFront=${minInputFrameIdFront}, inputFrameUpsyncDelayTolerance=${mapIns.inputFrameUpsyncDelayTolerance}`);
|
||||
return true;
|
||||
if (CC_DEBUG) {
|
||||
// Printing of this message might induce a performance impact.
|
||||
console.log(`Game logic ticking too fast, selfInputFrameIdFront=${inputFrameIdFront}, minInputFrameIdFront=${minInputFrameIdFront}, inputFrameUpsyncDelayTolerance=${mapIns.inputFrameUpsyncDelayTolerance}`);
|
||||
}
|
||||
return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt];
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return [false, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt];
|
||||
};
|
||||
|
||||
module.exports = NetworkDoctor;
|
||||
|
@@ -58,10 +58,21 @@ window.getBoundRoomCapacityFromPersistentStorage = function() {
|
||||
return (null == boundRoomCapacityStr ? null : parseInt(boundRoomCapacityStr));
|
||||
};
|
||||
|
||||
window.getChosenSpeciesIdFromPersistentStorage = function() {
|
||||
const boundRoomIdExpiresAt = parseInt(cc.sys.localStorage.getItem("boundRoomIdExpiresAt"));
|
||||
if (!boundRoomIdExpiresAt || Date.now() >= boundRoomIdExpiresAt) {
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
return null;
|
||||
}
|
||||
const chosenSpeciesIdStr = cc.sys.localStorage.getItem("chosenSpeciesId");
|
||||
return (null == chosenSpeciesIdStr ? 0 : parseInt(chosenSpeciesIdStr));
|
||||
};
|
||||
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage = function() {
|
||||
window.boundRoomId = null;
|
||||
cc.sys.localStorage.removeItem("boundRoomId");
|
||||
cc.sys.localStorage.removeItem("boundRoomCapacity");
|
||||
cc.sys.localStorage.removeItem("chosenSpeciesId");
|
||||
cc.sys.localStorage.removeItem("boundRoomIdExpiresAt");
|
||||
};
|
||||
|
||||
@@ -84,6 +95,7 @@ window.handleHbRequirements = function(resp) {
|
||||
window.boundRoomCapacity = resp.bciFrame.boundRoomCapacity;
|
||||
cc.sys.localStorage.setItem('boundRoomId', window.boundRoomId);
|
||||
cc.sys.localStorage.setItem('boundRoomCapacity', window.boundRoomCapacity);
|
||||
cc.sys.localStorage.setItem('chosenSpeciesId', window.chosenSpeciesId);
|
||||
cc.sys.localStorage.setItem('boundRoomIdExpiresAt', Date.now() + 10 * 60 * 1000); // Temporarily hardcoded, for `boundRoomId` only.
|
||||
}
|
||||
console.log(`Handle hb requirements #3`);
|
||||
@@ -179,6 +191,13 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
}
|
||||
}
|
||||
|
||||
if (null == window.chosenSpeciesId) {
|
||||
window.chosenSpeciesId = getChosenSpeciesIdFromPersistentStorage();
|
||||
}
|
||||
if (null != window.chosenSpeciesId) {
|
||||
urlToConnect = urlToConnect + "&speciesId=" + window.chosenSpeciesId;
|
||||
}
|
||||
|
||||
const clientSession = new WebSocket(urlToConnect);
|
||||
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
|
||||
|
||||
@@ -230,9 +249,9 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
const peerAddrList = resp.rdf.peerUdpAddrList;
|
||||
console.log(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR peerAddrList=${JSON.stringify(peerAddrList)}; boundRoomCapacity=${window.boundRoomCapacity}`);
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
setTimeout(()=> {
|
||||
DelayNoMore.UdpSession.upsertPeerUdpAddr(peerAddrList, window.boundRoomCapacity, window.mapIns.selfPlayerInfo.JoinIndex); // In C++ impl it actually broadcasts the peer-punching message to all known peers within "window.boundRoomCapacity"
|
||||
}, j*500);
|
||||
setTimeout(() => {
|
||||
DelayNoMore.UdpSession.upsertPeerUdpAddr(peerAddrList, window.boundRoomCapacity, window.mapIns.selfPlayerInfo.JoinIndex); // In C++ impl it actually broadcasts the peer-punching message to all known peers within "window.boundRoomCapacity"
|
||||
}, j * 500);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "171e2c96-28b4-4225-bdcc-5e464f07d91a",
|
||||
"uuid": "eeaa56f4-bd6c-4208-bec4-6ab1aa39ca93",
|
||||
"isPlugin": true,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
|
7
frontend/build-templates/.cocos-project.json
Normal file
7
frontend/build-templates/.cocos-project.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"engine_version": "2.2.1",
|
||||
"has_native": true,
|
||||
"project_type": "js",
|
||||
"projectName": "DelayNoMore",
|
||||
"packageName": "org.genxium.delaynomore"
|
||||
}
|
@@ -18,11 +18,11 @@
|
||||
"from": "cocos/scripting/js-bindings/manual/jsb_module_register.cpp",
|
||||
"to": "frameworks/runtime-src/Classes/jsb_module_register.cpp"
|
||||
}, {
|
||||
"from": "frameworks/runtime-src/Classes/send_ring_buff.hpp",
|
||||
"to": "frameworks/runtime-src/Classes/send_ring_buff.hpp"
|
||||
"from": "frameworks/runtime-src/Classes/ring_buff.hpp",
|
||||
"to": "frameworks/runtime-src/Classes/ring_buff.hpp"
|
||||
}, {
|
||||
"from": "frameworks/runtime-src/Classes/send_ring_buff.cpp",
|
||||
"to": "frameworks/runtime-src/Classes/send_ring_buff.cpp"
|
||||
"from": "frameworks/runtime-src/Classes/ring_buff.cpp",
|
||||
"to": "frameworks/runtime-src/Classes/ring_buff.cpp"
|
||||
}, {
|
||||
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
|
||||
"to": "frameworks/runtime-src/Classes/udp_session.hpp"
|
||||
|
@@ -0,0 +1,102 @@
|
||||
#include <string.h>
|
||||
#include "ring_buff.hpp"
|
||||
|
||||
// Sending
|
||||
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
|
||||
while (0 < cnt && cnt >= n) {
|
||||
// Make room for the new element
|
||||
this->pop();
|
||||
}
|
||||
eles[ed].bytesLen = newBytesLen;
|
||||
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
|
||||
memcpy(eles[ed].bytes, newBytes, newBytesLen);
|
||||
eles[ed].peerAddr = *(pNewPeerAddr);
|
||||
ed++;
|
||||
cnt++;
|
||||
if (ed >= n) {
|
||||
ed -= n; // Deliberately not using "%" operator for performance concern
|
||||
}
|
||||
}
|
||||
|
||||
SendWork* SendRingBuff::pop() {
|
||||
if (0 == cnt) {
|
||||
return NULL;
|
||||
}
|
||||
SendWork* ret = &(eles[st]);
|
||||
cnt--;
|
||||
st++;
|
||||
if (st >= n) {
|
||||
st -= n;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Recving
|
||||
void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
|
||||
RecvWork* slotEle = (&eles[ed.load()]); // Save for later update
|
||||
|
||||
int oldCnt = cnt.load();
|
||||
int oldSt = st.load(); // Used to guard against "cnt decremented in pop(...), but st not yet incremented and thus return value not yet copied to avoid contamination"
|
||||
int tried = 0;
|
||||
while (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 > tried) {
|
||||
// Make room for the new element
|
||||
this->pop(NULL);
|
||||
oldCnt = cnt.load(); // If "pop()" above failed, it'd only be due to concurrent calls to "pop()", either way the updated "cnt" should be good to go
|
||||
oldSt = st.load();
|
||||
++tried;
|
||||
}
|
||||
if (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 == tried) {
|
||||
// Failed silently, UDP packet can be dropped.
|
||||
return;
|
||||
}
|
||||
slotEle->bytesLen = newBytesLen;
|
||||
memset(slotEle->ui8Arr, 0, sizeof slotEle->ui8Arr);
|
||||
for (size_t i = 0; i < newBytesLen; i++) {
|
||||
*(slotEle->ui8Arr + i) = *(newBytes + i);
|
||||
}
|
||||
|
||||
// No need to compare-and-swap, only "UvRecvThread" will access "RecvRingBuff.ed".
|
||||
ed++;
|
||||
if (ed >= n) {
|
||||
ed -= n; // Deliberately not using "%" operator for performance concern
|
||||
}
|
||||
|
||||
// Only increment cnt when the putting of new element is fully done.
|
||||
cnt++;
|
||||
}
|
||||
|
||||
bool RecvRingBuff::pop(RecvWork* out) {
|
||||
int oldCnt = std::atomic_fetch_sub(&cnt, 1);
|
||||
/*
|
||||
[WARNING]
|
||||
|
||||
After here, two cases should be taken care of.
|
||||
1. If "n == oldCnt", we need guard against "put" to avoid contaminating "ret" by the "putting".
|
||||
2. If "0 >= oldCnt", we need guard against another "pop" to avoid over-popping.
|
||||
*/
|
||||
if (0 >= oldCnt) {
|
||||
// "pop" could be accessed by either "GameThread/pollUdpRecvRingBuff" or "UvRecvThread/put", thus we should be proactively guard against concurrent popping while "1 == cnt"
|
||||
++cnt;
|
||||
return false;
|
||||
}
|
||||
|
||||
// When concurrent "pop"s reach here, over-popping is definitely avoided.
|
||||
int oldSt = st.load();
|
||||
if (out) {
|
||||
RecvWork* src = (&eles[oldSt]);
|
||||
memset(out->ui8Arr, 0, sizeof out->ui8Arr);
|
||||
memcpy(out->ui8Arr, src, src->bytesLen);
|
||||
out->bytesLen = src->bytesLen;
|
||||
}
|
||||
int newSt = oldSt + 1;
|
||||
if (newSt >= n) {
|
||||
newSt -= n;
|
||||
}
|
||||
if (st.compare_exchange_weak(oldSt, newSt)) {
|
||||
return true;
|
||||
} else {
|
||||
// Failed concurrent access should recover the "cnt"
|
||||
++cnt;
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -4,6 +4,8 @@
|
||||
#include "uv/uv.h"
|
||||
#define __SSIZE_T // Otherwise "ssize_t" would have conflicting macros error that stops compiling
|
||||
|
||||
#include <atomic>
|
||||
|
||||
int const RING_BUFF_CONSECUTIVE_SET = 0;
|
||||
int const RING_BUFF_NON_CONSECUTIVE_SET = 1;
|
||||
int const RING_BUFF_FAILED_TO_SET = 2;
|
||||
@@ -41,4 +43,32 @@ public:
|
||||
SendWork* pop();
|
||||
};
|
||||
|
||||
// TODO: Move "RecvXxxx" to a dedicated class.
|
||||
class RecvWork {
|
||||
public:
|
||||
uint8_t ui8Arr[maxUdpPayloadBytes]; // Wasting some RAM here thus no need for explicit recursive destruction
|
||||
size_t bytesLen;
|
||||
};
|
||||
|
||||
/*
|
||||
[WARNING] This class is specific to "RecvWork"; its "put" and "pop" methods are designed to be thread-safe & lock-free for our particular case, i.e. only concurrent access from "UvRecvThread" & "GameThread", in a sense more sophisticated than the Golang or JavaScript versions.
|
||||
|
||||
There's yet no plan to support thread-safe & lock-free "getByFrameId/setByFrameId" -- being thread-safe is easy by use of mutex, which is very SLOWWWWW when used in 60fps race-conditions.
|
||||
|
||||
The generic "thread-safe, lock-free ring buffer or circular buffer" is a big problem, widely discussed over the internet and in literatures, search "lock-free circular buffer" for more information.
|
||||
*/
|
||||
class RecvRingBuff {
|
||||
public:
|
||||
int n;
|
||||
std::atomic_int ed, st, cnt;
|
||||
RecvWork eles[maxBuffedMsgs]; // preallocated on stack to save heap alloc/dealloc time
|
||||
RecvRingBuff(int newN) {
|
||||
this->n = newN;
|
||||
this->st = this->ed = this->cnt = 0;
|
||||
}
|
||||
|
||||
void put(char* newBytes, size_t newBytesLen);
|
||||
|
||||
bool pop(RecvWork* out);
|
||||
};
|
||||
#endif
|
@@ -1,31 +0,0 @@
|
||||
#include <string.h>
|
||||
#include "send_ring_buff.hpp"
|
||||
|
||||
void SendRingBuff::put(BYTEC* const newBytes, size_t newBytesLen, PeerAddr* pNewPeerAddr) {
|
||||
while (0 < cnt && cnt >= n) {
|
||||
// Make room for the new element
|
||||
this->pop();
|
||||
}
|
||||
eles[ed].bytesLen = newBytesLen;
|
||||
memset(eles[ed].bytes, 0, sizeof eles[ed].bytes);
|
||||
memcpy(eles[ed].bytes, newBytes, newBytesLen);
|
||||
eles[ed].peerAddr = *(pNewPeerAddr);
|
||||
ed++;
|
||||
cnt++;
|
||||
if (ed >= n) {
|
||||
ed -= n; // Deliberately not using "%" operator for performance concern
|
||||
}
|
||||
}
|
||||
|
||||
SendWork* SendRingBuff::pop() {
|
||||
if (0 == cnt) {
|
||||
return NULL;
|
||||
}
|
||||
SendWork* ret = &(eles[st]);
|
||||
cnt--;
|
||||
st++;
|
||||
if (st >= n) {
|
||||
st -= n;
|
||||
}
|
||||
return ret;
|
||||
}
|
@@ -16,11 +16,15 @@ uv_loop_t *recvLoop = NULL, *sendLoop = NULL;
|
||||
uv_mutex_t sendRingBuffLock; // used along with "uvSendLoopTriggerSig" as a "uv_cond_t"
|
||||
SendRingBuff* sendRingBuff = NULL;
|
||||
|
||||
uv_mutex_t recvRingBuffLock;
|
||||
RecvRingBuff* recvRingBuff = NULL;
|
||||
|
||||
char SRV_IP[256];
|
||||
int SRV_PORT = 0;
|
||||
int UDP_TUNNEL_SRV_PORT = 0;
|
||||
struct PeerAddr udpPunchingServerAddr, udpTunnelAddr;
|
||||
struct PeerAddr peerAddrList[maxPeerCnt];
|
||||
bool peerPunchedMarks[maxPeerCnt];
|
||||
|
||||
void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr const* addr, unsigned flags) {
|
||||
if (nread < 0) {
|
||||
@@ -29,6 +33,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
||||
free(buf->base);
|
||||
return;
|
||||
}
|
||||
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
|
||||
|
||||
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||
char ip[INET_ADDRSTRLEN];
|
||||
memset(ip, 0, sizeof ip);
|
||||
@@ -38,10 +44,8 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
||||
// The null check for "addr" is necessary, on Android there'd be such mysterious call to "_onRead"!
|
||||
switch (addr->sa_family) {
|
||||
case AF_INET: {
|
||||
struct sockaddr_in const* sockAddr = (struct sockaddr_in const*)addr;
|
||||
uv_inet_ntop(sockAddr->sin_family, &(sockAddr->sin_addr), ip, INET_ADDRSTRLEN);
|
||||
port = ntohs(sockAddr->sin_port);
|
||||
CCLOG("UDP received %u bytes from %s:%d", nread, ip, port);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -53,37 +57,24 @@ void _onRead(uv_udp_t* req, ssize_t nread, uv_buf_t const* buf, struct sockaddr
|
||||
#endif
|
||||
|
||||
if (6 == nread) {
|
||||
// holepunching
|
||||
} else if (0 < nread) {
|
||||
// Non-holepunching; it might be more effective in RAM usage to use a threadsafe RingBuff to pass msg to GameThread here, but as long as it's not a performance blocker don't bother optimize here...
|
||||
uint8_t* const ui8Arr = (uint8_t*)malloc(maxUdpPayloadBytes*sizeof(uint8_t));
|
||||
memset(ui8Arr, 0, sizeof(ui8Arr));
|
||||
for (int i = 0; i < nread; i++) {
|
||||
*(ui8Arr+i) = *(buf->base + i);
|
||||
// Peer holepunching
|
||||
for (int i = 0; i < maxPeerCnt; i++) {
|
||||
if (peerAddrList[i].sockAddrIn.sin_addr.s_addr != sockAddr->sin_addr.s_addr) continue;
|
||||
if (peerAddrList[i].sockAddrIn.sin_port != sockAddr->sin_port) continue;
|
||||
peerPunchedMarks[i] = true;
|
||||
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||
CCLOG("UDP received peer-holepunching from %s:%d", ip, port);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread([=]() {
|
||||
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
||||
se::Value onUdpMessageCb;
|
||||
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
|
||||
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
||||
se::AutoHandleScope hs;
|
||||
//CCLOG("UDP received %d bytes upsync -- 1", nread);
|
||||
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, ui8Arr, nread);
|
||||
//CCLOG("UDP received %d bytes upsync -- 2", nread);
|
||||
se::ValueArray args = { se::Value(gameThreadMsg) };
|
||||
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
|
||||
// Temporarily assume that the "this" ptr within callback is NULL.
|
||||
bool ok = onUdpMessageCb.toObject()->call(args, NULL);
|
||||
if (!ok) {
|
||||
se::ScriptEngine::getInstance()->clearException();
|
||||
}
|
||||
}
|
||||
//CCLOG("UDP received %d bytes upsync -- 3", nread);
|
||||
gameThreadMsg->decRef(); // Reference http://docs.cocos.com/creator/2.2/manual/en/advanced-topics/JSB2.0-learning.html#seobject
|
||||
//CCLOG("UDP received %d bytes upsync -- 4", nread);
|
||||
free(ui8Arr);
|
||||
//CCLOG("UDP received %d bytes upsync -- 5", nread);
|
||||
});
|
||||
} else if (0 < nread) {
|
||||
// Non-holepunching; the previously used "cocos2d::Application::getInstance()->getScheduler()->performFunctionInCocosThread(...)" approach was so non-deterministic in terms of the lag till GameThread actually recognizes this latest received packet due to scheduler uncertainty -- and was also heavier in RAM due to lambda usage
|
||||
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
|
||||
CCLOG("UDP received %u bytes inputFrameUpsync from %s:%d", nread, ip, port);
|
||||
#endif
|
||||
//uv_mutex_lock(&recvRingBuffLock);
|
||||
recvRingBuff->put(buf->base, nread);
|
||||
//uv_mutex_unlock(&recvRingBuffLock);
|
||||
}
|
||||
free(buf->base);
|
||||
|
||||
@@ -169,6 +160,7 @@ void startRecvLoop(void* arg) {
|
||||
|
||||
int uvCloseRet = uv_loop_close(l);
|
||||
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
|
||||
uv_mutex_destroy(&recvRingBuffLock);
|
||||
}
|
||||
|
||||
void startSendLoop(void* arg) {
|
||||
@@ -196,6 +188,10 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
|
||||
}
|
||||
uv_mutex_init(&sendRingBuffLock);
|
||||
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
|
||||
|
||||
uv_mutex_init(&recvRingBuffLock);
|
||||
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
|
||||
|
||||
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
|
||||
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
|
||||
|
||||
@@ -222,6 +218,12 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
|
||||
struct sockaddr_in udpAddr;
|
||||
uv_ip4_addr("0.0.0.0", port, &udpAddr);
|
||||
struct sockaddr const* pUdpAddr = (struct sockaddr const*)&udpAddr;
|
||||
|
||||
memset(peerPunchedMarks, false, sizeof(peerPunchedMarks));
|
||||
for (int i = 0; i < maxPeerCnt; i++) {
|
||||
peerAddrList[i].authKey = -1; // hardcoded for now
|
||||
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
|
||||
}
|
||||
/*
|
||||
[WARNING] On Android, the libuv documentation of "UV_UDP_REUSEADDR" is true, i.e. only the socket that binds later on the same port will be triggered the recv callback; however on Windows, experiment shows that the exact reverse is true instead.
|
||||
|
||||
@@ -259,11 +261,8 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
|
||||
uv_thread_join(&recvTid);
|
||||
free(udpRecvSocket);
|
||||
free(recvLoop);
|
||||
delete recvRingBuff;
|
||||
|
||||
for (int i = 0; i < maxPeerCnt; i++) {
|
||||
peerAddrList[i].authKey = -1; // hardcoded for now
|
||||
memset((char*)&peerAddrList[i].sockAddrIn, 0, sizeof(peerAddrList[i].sockAddrIn));
|
||||
}
|
||||
CCLOG("Closed udp session and dealloc all resources in GameThread...");
|
||||
|
||||
return true;
|
||||
@@ -321,8 +320,7 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
||||
uv_mutex_lock(&sendRingBuffLock);
|
||||
// Might want to send several times for better arrival rate
|
||||
for (int j = 0; j < broadcastUpsyncCnt; j++) {
|
||||
// Send to room udp tunnel in case of hole punching failure
|
||||
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
|
||||
int peerPunchedCnt = 0;
|
||||
for (int i = 0; i < roomCapacity; i++) {
|
||||
if (i + 1 == selfJoinIndex) {
|
||||
continue;
|
||||
@@ -331,8 +329,17 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
||||
// Peer addr not initialized
|
||||
continue;
|
||||
}
|
||||
if (false == peerPunchedMarks[i]) {
|
||||
// Not punched yet, save some bandwidth
|
||||
continue;
|
||||
}
|
||||
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i]));
|
||||
++peerPunchedCnt;
|
||||
}
|
||||
|
||||
sendRingBuff->put(bytes, bytesLen, &(peerAddrList[i])); // Content hardcoded for now
|
||||
if (peerPunchedCnt + 1 < roomCapacity) {
|
||||
// Send to room udp tunnel in case of ANY hole punching failure
|
||||
sendRingBuff->put(bytes, bytesLen, &udpTunnelAddr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,3 +348,37 @@ bool DelayNoMore::UdpSession::broadcastInputFrameUpsync(BYTEC* const bytes, size
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
|
||||
// This function is called by GameThread 60 fps.
|
||||
|
||||
//uv_mutex_lock(&recvRingBuffLock);
|
||||
while (true) {
|
||||
RecvWork f;
|
||||
bool res = recvRingBuff->pop(&f);
|
||||
if (!res) return false;
|
||||
|
||||
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
||||
se::AutoHandleScope hs;
|
||||
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
||||
se::Value onUdpMessageCb;
|
||||
se::ScriptEngine::getInstance()->getGlobalObject()->getProperty("onUdpMessage", &onUdpMessageCb);
|
||||
if (onUdpMessageCb.isObject() && onUdpMessageCb.toObject()->isFunction()) {
|
||||
//CCLOG("UDP received %d bytes upsync -- 1", nread);
|
||||
se::Object* const gameThreadMsg = se::Object::createTypedArray(se::Object::TypedArrayType::UINT8, f.ui8Arr, f.bytesLen);
|
||||
//CCLOG("UDP received %d bytes upsync -- 2", nread);
|
||||
se::ValueArray args = { se::Value(gameThreadMsg) };
|
||||
|
||||
// Temporarily assume that the "this" ptr within callback is NULL.
|
||||
bool ok = onUdpMessageCb.toObject()->call(args, NULL);
|
||||
if (!ok) {
|
||||
se::ScriptEngine::getInstance()->clearException();
|
||||
}
|
||||
//CCLOG("UDP received %d bytes upsync -- 3", nread);
|
||||
gameThreadMsg->decRef(); // Reference http://docs.cocos.com/creator/2.2/manual/en/advanced-topics/JSB2.0-learning.html#seobject
|
||||
//CCLOG("UDP received %d bytes upsync -- 4", nread);
|
||||
}
|
||||
}
|
||||
//uv_mutex_unlock(&recvRingBuffLock);
|
||||
return true;
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
#ifndef udp_session_hpp
|
||||
#define udp_session_hpp
|
||||
|
||||
#include "send_ring_buff.hpp"
|
||||
#include "ring_buff.hpp"
|
||||
|
||||
int const maxPeerCnt = 10;
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace DelayNoMore {
|
||||
//static bool clearPeerUDPAddrList();
|
||||
static bool punchToServer(CHARC* const srvIp, int const srvPort, BYTEC* const bytes, size_t bytesLen, int const udpTunnelSrvPort, BYTEC* const udpTunnelBytes, size_t udpTunnelBytesBytesLen);
|
||||
static bool broadcastInputFrameUpsync(BYTEC* const bytes, size_t bytesLen, int roomCapacity, int selfJoinIndex);
|
||||
static bool pollUdpRecvRingBuff();
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
@@ -126,6 +126,20 @@ bool upsertPeerUdpAddr(se::State& s) {
|
||||
}
|
||||
SE_BIND_FUNC(upsertPeerUdpAddr)
|
||||
|
||||
bool pollUdpRecvRingBuff(se::State& s) {
|
||||
const auto& args = s.args();
|
||||
size_t argc = args.size();
|
||||
CC_UNUSED bool ok = true;
|
||||
if (0 == argc) {
|
||||
SE_PRECONDITION2(ok, false, "upsertPeerUdpAddr: Error processing arguments");
|
||||
return DelayNoMore::UdpSession::pollUdpRecvRingBuff();
|
||||
}
|
||||
SE_REPORT_ERROR("wrong number of arguments: %d, was expecting %d; or wrong arg type!", (int)argc, 0);
|
||||
|
||||
return false;
|
||||
}
|
||||
SE_BIND_FUNC(pollUdpRecvRingBuff)
|
||||
|
||||
static bool udpSessionFinalize(se::State& s) {
|
||||
CCLOGINFO("jsbindings: finalizing JS object %p (DelayNoMore::UdpSession)", s.nativeThisObject());
|
||||
auto iter = se::NonRefNativePtrCreatedByCtorMap::find(s.nativeThisObject());
|
||||
@@ -158,6 +172,7 @@ bool registerUdpSession(se::Object* obj) {
|
||||
cls->defineStaticFunction("broadcastInputFrameUpsync", _SE(broadcastInputFrameUpsync));
|
||||
cls->defineStaticFunction("closeUdpSession", _SE(closeUdpSession));
|
||||
cls->defineStaticFunction("upsertPeerUdpAddr", _SE(upsertPeerUdpAddr));
|
||||
cls->defineStaticFunction("pollUdpRecvRingBuff", _SE(pollUdpRecvRingBuff));
|
||||
cls->defineFinalizeFunction(_SE(udpSessionFinalize));
|
||||
cls->install();
|
||||
|
||||
|
@@ -15,5 +15,5 @@ SE_DECLARE_FUNC(punchToServer);
|
||||
SE_DECLARE_FUNC(broadcastInputFrameUpsync);
|
||||
SE_DECLARE_FUNC(closeUdpSession);
|
||||
SE_DECLARE_FUNC(upsertPeerUdpAddr);
|
||||
|
||||
SE_DECLARE_FUNC(pollUdpRecvRingBuff);
|
||||
#endif
|
||||
|
@@ -15,7 +15,7 @@ LOCAL_SRC_FILES := hellojavascript/main.cpp \
|
||||
../../../Classes/jsb_module_register.cpp \
|
||||
../../../Classes/udp_session.cpp \
|
||||
../../../Classes/udp_session_bridge.cpp \
|
||||
../../../Classes/send_ring_buff.cpp
|
||||
../../../Classes/ring_buff.cpp
|
||||
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../Classes
|
||||
|
||||
|
@@ -190,11 +190,11 @@ copy "$(ProjectDir)..\..\..\project.json" "$(OutDir)\" /Y</Command>
|
||||
<ClCompile Include="..\Classes\AppDelegate.cpp" />
|
||||
<ClCompile Include="..\Classes\udp_session.cpp" />
|
||||
<ClCompile Include="..\Classes\udp_session_bridge.cpp" />
|
||||
<ClCompile Include="..\Classes\send_ring_buff.cpp" />
|
||||
<ClCompile Include="..\Classes\ring_buff.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="main.h" />
|
||||
<ClInclude Include="..\Classes\send_ring_buff.hpp" />
|
||||
<ClInclude Include="..\Classes\ring_buff.hpp" />
|
||||
<ClInclude Include="..\Classes\udp_session.hpp" />
|
||||
<ClInclude Include="..\Classes\udp_session_bridge.hpp" />
|
||||
<ClInclude Include="..\Classes\AppDelegate.h" />
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<ClCompile Include="..\Classes\jsb_module_register.cpp">
|
||||
<Filter>Classes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Classes\send_ring_buff.cpp">
|
||||
<ClCompile Include="..\Classes\ring_buff.cpp">
|
||||
<Filter>Classes</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Classes\udp_session.cpp">
|
||||
@@ -40,7 +40,7 @@
|
||||
<Filter>win32</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="..\Classes\send_ring_buff.hpp">
|
||||
<ClInclude Include="..\Classes\ring_buff.hpp">
|
||||
<Filter>Classes</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Classes\udp_session.hpp">
|
||||
|
@@ -716,7 +716,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
} else if stoppingFromWalking {
|
||||
thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover
|
||||
} else {
|
||||
thatPlayerInNextFrame.FramesToRecover = (chConfig.InertiaFramesToRecover >> 1)
|
||||
thatPlayerInNextFrame.FramesToRecover = ((chConfig.InertiaFramesToRecover >> 1) + (chConfig.InertiaFramesToRecover >> 2))
|
||||
}
|
||||
} else {
|
||||
thatPlayerInNextFrame.CapturedByInertia = false
|
||||
@@ -755,10 +755,21 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
// Reset playerCollider position from the "virtual grid position"
|
||||
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
|
||||
if 0 >= thatPlayerInNextFrame.Hp && 0 == thatPlayerInNextFrame.FramesToRecover {
|
||||
// Revive
|
||||
// Revive from Dying
|
||||
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
|
||||
thatPlayerInNextFrame.FramesInChState = ATK_CHARACTER_STATE_GET_UP1
|
||||
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
|
||||
thatPlayerInNextFrame.FramesInvinsible = chConfig.GetUpInvinsibleFrames
|
||||
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
|
||||
// Hardcoded initial character orientation/facing
|
||||
if 0 == (thatPlayerInNextFrame.JoinIndex % 2) {
|
||||
thatPlayerInNextFrame.DirX = -2
|
||||
thatPlayerInNextFrame.DirY = 0
|
||||
} else {
|
||||
thatPlayerInNextFrame.DirX = +2
|
||||
thatPlayerInNextFrame.DirY = 0
|
||||
}
|
||||
}
|
||||
if jumpedOrNotList[i] {
|
||||
// We haven't proceeded with "OnWall" calculation for "thatPlayerInNextFrame", thus use "currPlayerDownsync.OnWall" for checking
|
||||
@@ -899,8 +910,12 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
if collision := playerCollider.Check(0, 0); nil != collision {
|
||||
for _, obj := range collision.Objects {
|
||||
isBarrier, isAnotherPlayer, isBullet := false, false, false
|
||||
switch obj.Data.(type) {
|
||||
switch v := obj.Data.(type) {
|
||||
case *PlayerDownsync:
|
||||
if ATK_CHARACTER_STATE_DYING == v.CharacterState {
|
||||
// ignore collision with dying player
|
||||
continue
|
||||
}
|
||||
isAnotherPlayer = true
|
||||
case *MeleeBullet, *FireballBullet:
|
||||
isBullet = true
|
||||
@@ -912,6 +927,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
// ignore bullets for this step
|
||||
continue
|
||||
}
|
||||
|
||||
bShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
overlapped, pushbackX, pushbackY, overlapResult := calcPushbacks(0, 0, playerShape, bShape)
|
||||
if !overlapped {
|
||||
@@ -945,7 +961,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
if fallStopping {
|
||||
thatPlayerInNextFrame.VelY = 0
|
||||
thatPlayerInNextFrame.VelX = 0
|
||||
if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
|
||||
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
|
||||
} else if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
|
||||
thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover
|
||||
} else {
|
||||
@@ -960,9 +977,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
thatPlayerInNextFrame.FramesToRecover = 0
|
||||
}
|
||||
} else {
|
||||
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp
|
||||
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp or Dying
|
||||
if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent {
|
||||
if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
|
||||
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
|
||||
// No update needed for Dying
|
||||
} else if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
|
||||
if 0 == thatPlayerInNextFrame.FramesToRecover {
|
||||
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
|
||||
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
|
||||
@@ -1010,113 +1029,83 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
|
||||
collision := bulletCollider.Check(0, 0)
|
||||
bulletCollider.Space.Remove(bulletCollider) // Make sure that the bulletCollider is always removed for each renderFrame
|
||||
exploded := false
|
||||
if nil != collision {
|
||||
switch v := bulletCollider.Data.(type) {
|
||||
case *MeleeBullet:
|
||||
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon)
|
||||
offender := currRenderFrame.PlayersArr[v.BattleAttr.OffenderJoinIndex-1]
|
||||
for _, obj := range collision.Objects {
|
||||
defenderShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
switch t := obj.Data.(type) {
|
||||
case *PlayerDownsync:
|
||||
if v.BattleAttr.OffenderJoinIndex == t.JoinIndex {
|
||||
continue
|
||||
}
|
||||
overlapped, _, _, _ := calcPushbacks(0, 0, bulletShape, defenderShape)
|
||||
if !overlapped {
|
||||
continue
|
||||
}
|
||||
exploded = true
|
||||
if _, existent := invinsibleSet[t.CharacterState]; existent {
|
||||
continue
|
||||
}
|
||||
if 0 < t.FramesInvinsible {
|
||||
continue
|
||||
}
|
||||
xfac := int32(1) // By now, straight Punch offset doesn't respect "y-axis"
|
||||
if 0 > offender.DirX {
|
||||
xfac = -xfac
|
||||
}
|
||||
atkedPlayerInNextFrame := nextRenderFramePlayers[t.JoinIndex-1]
|
||||
atkedPlayerInNextFrame.Hp -= v.Bullet.Damage
|
||||
if 0 >= atkedPlayerInNextFrame.Hp {
|
||||
// [WARNING] We don't have "dying in air" animation for now, and for better graphical recognition, play the same dying animation even in air
|
||||
atkedPlayerInNextFrame.Hp = 0
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_DYING
|
||||
atkedPlayerInNextFrame.FramesToRecover = DYING_FRAMES_TO_RECOVER
|
||||
} else {
|
||||
pushbackVelX, pushbackVelY := xfac*v.Bullet.PushbackVelX, v.Bullet.PushbackVelY
|
||||
atkedPlayerInNextFrame.VelX = pushbackVelX
|
||||
atkedPlayerInNextFrame.VelY = pushbackVelY
|
||||
if v.Bullet.BlowUp {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1
|
||||
} else {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1
|
||||
}
|
||||
oldFramesToRecover := nextRenderFramePlayers[t.JoinIndex-1].FramesToRecover
|
||||
if v.Bullet.HitStunFrames > oldFramesToRecover {
|
||||
atkedPlayerInNextFrame.FramesToRecover = v.Bullet.HitStunFrames
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *FireballBullet:
|
||||
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon)
|
||||
offender := currRenderFrame.PlayersArr[v.BattleAttr.OffenderJoinIndex-1]
|
||||
for _, obj := range collision.Objects {
|
||||
defenderShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
switch t := obj.Data.(type) {
|
||||
case *PlayerDownsync:
|
||||
if v.BattleAttr.OffenderJoinIndex == t.JoinIndex {
|
||||
continue
|
||||
}
|
||||
overlapped, _, _, _ := calcPushbacks(0, 0, bulletShape, defenderShape)
|
||||
if !overlapped {
|
||||
continue
|
||||
}
|
||||
exploded = true
|
||||
if _, existent := invinsibleSet[t.CharacterState]; existent {
|
||||
continue
|
||||
}
|
||||
if 0 < t.FramesInvinsible {
|
||||
continue
|
||||
}
|
||||
xfac := int32(1) // By now, straight Punch offset doesn't respect "y-axis"
|
||||
if 0 > offender.DirX {
|
||||
xfac = -xfac
|
||||
}
|
||||
atkedPlayerInNextFrame := nextRenderFramePlayers[t.JoinIndex-1]
|
||||
atkedPlayerInNextFrame.Hp -= v.Bullet.Damage
|
||||
if 0 >= atkedPlayerInNextFrame.Hp {
|
||||
// [WARNING] We don't have "dying in air" animation for now, and for better graphical recognition, play the same dying animation even in air
|
||||
atkedPlayerInNextFrame.Hp = 0
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_DYING
|
||||
atkedPlayerInNextFrame.FramesToRecover = DYING_FRAMES_TO_RECOVER
|
||||
} else {
|
||||
pushbackVelX, pushbackVelY := xfac*v.Bullet.PushbackVelX, v.Bullet.PushbackVelY
|
||||
atkedPlayerInNextFrame.VelX = pushbackVelX
|
||||
atkedPlayerInNextFrame.VelY = pushbackVelY
|
||||
if v.Bullet.BlowUp {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1
|
||||
} else {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1
|
||||
}
|
||||
oldFramesToRecover := nextRenderFramePlayers[t.JoinIndex-1].FramesToRecover
|
||||
if v.Bullet.HitStunFrames > oldFramesToRecover {
|
||||
atkedPlayerInNextFrame.FramesToRecover = v.Bullet.HitStunFrames
|
||||
}
|
||||
}
|
||||
default:
|
||||
exploded = true
|
||||
explodedOnAnotherPlayer := false
|
||||
if nil == collision {
|
||||
continue
|
||||
}
|
||||
|
||||
var bulletStaticAttr *BulletConfig = nil
|
||||
var bulletBattleAttr *BulletBattleAttr = nil
|
||||
switch v := bulletCollider.Data.(type) {
|
||||
case *MeleeBullet:
|
||||
bulletStaticAttr = v.Bullet
|
||||
bulletBattleAttr = v.BattleAttr
|
||||
case *FireballBullet:
|
||||
bulletStaticAttr = v.Bullet
|
||||
bulletBattleAttr = v.BattleAttr
|
||||
}
|
||||
|
||||
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon)
|
||||
offender := currRenderFrame.PlayersArr[bulletBattleAttr.OffenderJoinIndex-1]
|
||||
for _, obj := range collision.Objects {
|
||||
defenderShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
switch t := obj.Data.(type) {
|
||||
case *PlayerDownsync:
|
||||
if bulletBattleAttr.OffenderJoinIndex == t.JoinIndex {
|
||||
continue
|
||||
}
|
||||
overlapped, _, _, _ := calcPushbacks(0, 0, bulletShape, defenderShape)
|
||||
if !overlapped {
|
||||
continue
|
||||
}
|
||||
if _, existent := invinsibleSet[t.CharacterState]; existent {
|
||||
continue
|
||||
}
|
||||
if 0 < t.FramesInvinsible {
|
||||
continue
|
||||
}
|
||||
exploded = true
|
||||
explodedOnAnotherPlayer = true
|
||||
xfac := int32(1) // By now, straight Punch offset doesn't respect "y-axis"
|
||||
if 0 > offender.DirX {
|
||||
xfac = -xfac
|
||||
}
|
||||
atkedPlayerInNextFrame := nextRenderFramePlayers[t.JoinIndex-1]
|
||||
atkedPlayerInNextFrame.Hp -= bulletStaticAttr.Damage
|
||||
if 0 >= atkedPlayerInNextFrame.Hp {
|
||||
// [WARNING] We don't have "dying in air" animation for now, and for better graphical recognition, play the same dying animation even in air
|
||||
atkedPlayerInNextFrame.Hp = 0
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_DYING
|
||||
atkedPlayerInNextFrame.FramesToRecover = DYING_FRAMES_TO_RECOVER
|
||||
} else {
|
||||
pushbackVelX, pushbackVelY := xfac*bulletStaticAttr.PushbackVelX, bulletStaticAttr.PushbackVelY
|
||||
atkedPlayerInNextFrame.VelX = pushbackVelX
|
||||
atkedPlayerInNextFrame.VelY = pushbackVelY
|
||||
if bulletStaticAttr.BlowUp {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1
|
||||
} else {
|
||||
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATKED1
|
||||
}
|
||||
oldFramesToRecover := nextRenderFramePlayers[t.JoinIndex-1].FramesToRecover
|
||||
if bulletStaticAttr.HitStunFrames > oldFramesToRecover {
|
||||
atkedPlayerInNextFrame.FramesToRecover = bulletStaticAttr.HitStunFrames
|
||||
}
|
||||
}
|
||||
default:
|
||||
exploded = true
|
||||
}
|
||||
}
|
||||
|
||||
if exploded {
|
||||
switch v := bulletCollider.Data.(type) {
|
||||
case *MeleeBullet:
|
||||
v.BlState = BULLET_EXPLODING
|
||||
v.FramesInBlState = 0
|
||||
if explodedOnAnotherPlayer {
|
||||
v.FramesInBlState = 0
|
||||
} else {
|
||||
// When hitting a barrier, don't play explosion anim
|
||||
v.FramesInBlState = v.Bullet.ExplosionFrames + 1
|
||||
}
|
||||
//fmt.Printf("melee exploded @currRenderFrame.Id=%d, bulletLocalId=%d, blState=%d\n", currRenderFrame.Id, v.BattleAttr.BulletLocalId, v.BlState)
|
||||
case *FireballBullet:
|
||||
v.BlState = BULLET_EXPLODING
|
||||
|
@@ -538,7 +538,7 @@ var skills = map[int]*Skill{
|
||||
HitboxSizeX: int32(float64(64) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
HitboxSizeY: int32(float64(48) * WORLD_TO_VIRTUAL_GRID_RATIO),
|
||||
BlowUp: false,
|
||||
ExplosionFrames: 10,
|
||||
ExplosionFrames: 30,
|
||||
SpeciesId: int32(1),
|
||||
},
|
||||
},
|
||||
@@ -781,10 +781,10 @@ var skills = map[int]*Skill{
|
||||
Hits: []interface{}{
|
||||
&MeleeBullet{
|
||||
Bullet: &BulletConfig{
|
||||
StartupFrames: int32(3),
|
||||
StartupFrames: int32(4),
|
||||
ActiveFrames: int32(20),
|
||||
HitStunFrames: int32(18),
|
||||
BlockStunFrames: int32(9),
|
||||
HitStunFrames: int32(9),
|
||||
BlockStunFrames: int32(5),
|
||||
Damage: int32(5),
|
||||
SelfLockVelX: NO_LOCK_VEL,
|
||||
SelfLockVelY: NO_LOCK_VEL,
|
||||
|
@@ -1,3 +1,5 @@
|
||||
# TODO: For websocket traffic, use a "consistent hash" on "expectedRoomId" and "boundRoomId"!
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name tsrht.lokcol.com;
|
||||
|
Reference in New Issue
Block a user