Compare commits

...

76 Commits

Author SHA1 Message Date
genxium
b79e2dc935 Enhanced UDP message callback handling. 2023-02-09 23:34:00 +08:00
genxium
7b0c807496 Drafted enhancement of UDP message callback. 2023-02-09 10:18:23 +08:00
genxium
5c611b626d Minor fix. 2023-02-08 19:28:40 +08:00
genxium
38149279bd Drafted hp handling. 2023-02-08 18:32:59 +08:00
genxium
a762c563d9 Updated skill sets and animations. 2023-02-07 11:31:59 +08:00
genxium
6a0d729dee Added more animation resources. 2023-02-06 12:02:44 +08:00
genxium
f10389bf55 Minor update. 2023-02-05 20:24:09 +08:00
genxium
6b3d1ed49a Enhanced UI handling upon battle started. 2023-02-05 18:44:37 +08:00
genxium
d25bb5ff10 Enhanced inertia handling. 2023-02-05 11:27:26 +08:00
genxium
d38d4b4ec9 Updated README. 2023-02-05 00:16:09 +08:00
genxium
03828db6ff Reverted input scale. 2023-02-04 21:41:58 +08:00
genxium
917fca2bcd Improved on wall dynamics. 2023-02-04 18:33:49 +08:00
genxium
680e4f1f59 Updated README. 2023-02-03 22:56:58 +08:00
genxium
f367609276 Fixes for rollback on UDP peer upsync. 2023-02-03 22:06:03 +08:00
genxium
70ae4a4c92 Fixed socket binding compatibility issue for Android and Windows. 2023-02-03 09:51:56 +08:00
genxium
6f561bea87 Fixed build for Android. 2023-02-03 00:21:07 +08:00
genxium
70a86c27b0 Enhancement for libuv thread safety. 2023-02-02 21:53:43 +08:00
genxium
b0f37d2237 Minor update. 2023-02-01 23:41:08 +08:00
genxium
09376b827d Reverted magic constants. 2023-02-01 23:27:27 +08:00
genxium
d560392c79 Minor fix. 2023-02-01 23:17:55 +08:00
genxium
c75f642011 Minor update again. 2023-02-01 22:55:27 +08:00
genxium
5cfcac6cf6 Minor update. 2023-02-01 21:44:33 +08:00
genxium
d37ebd4c33 Updated README. 2023-02-01 18:59:14 +08:00
genxium
1d138b17c3 Fixes for UDP p2p packets handling in frontend input buffer. 2023-02-01 17:43:15 +08:00
genxium
851678e2f3 Minor fix. 2023-02-01 13:09:17 +08:00
genxium
2fb6fd6bea Updated CameraTracker. 2023-01-31 23:11:46 +08:00
genxium
e3440a2a06 Fixes for UDP use in input prediction. 2023-01-31 22:39:21 +08:00
genxium
8de2d6e4e7 Enhancement for type#1 force-confirmation trigger. 2023-01-31 09:57:37 +08:00
genxium
ba2dd0b22e Added thread-safety comments for libuv codes. 2023-01-30 23:41:22 +08:00
genxium
754610d31b Updated documentation. 2023-01-30 09:56:19 +08:00
genxium
a35de9b83c Enhanced backend udp tunnel workflow. 2023-01-30 09:21:06 +08:00
genxium
2b6cb57050 Enabled backend udp tunnel forwarding. 2023-01-30 00:25:11 +08:00
genxium
677e76179c Minor fix. 2023-01-29 22:38:12 +08:00
genxium
c65c122f45 Added front-to-back UDP channel punching. 2023-01-29 18:20:48 +08:00
genxium
b5530b352b Updated pb files. 2023-01-29 13:10:19 +08:00
genxium
4e638fb2ec Preparing in battle udp tunnel on backend. 2023-01-29 12:42:48 +08:00
genxium
7c454130db Minor fix. 2023-01-28 23:50:32 +08:00
genxium
5863f88435 Minor update. 2023-01-28 12:55:22 +08:00
genxium
bbf07fe518 Fixed Android segfault on mysterious udp reception callback. 2023-01-28 10:52:58 +08:00
genxium
26660d75d2 Minor fix. 2023-01-28 00:13:30 +08:00
genxium
76cdbc8f1f Updated trouble shooting doc. 2023-01-28 00:10:10 +08:00
genxium
4097a8da75 Fixed libuv multithreading. 2023-01-27 22:51:34 +08:00
genxium
e7bf6ec16b Added basic input upsync via udp. 2023-01-26 21:34:29 +08:00
genxium
7ab983949c Fixes in udp_session type usage for cross-platform use. 2023-01-26 12:43:38 +08:00
genxium
2028f8277d Fixed Android building setup. 2023-01-26 00:08:46 +08:00
genxium
60bb74169e Fixed libuv resource deallocation. 2023-01-25 23:47:54 +08:00
genxium
8536521136 Integrated basic holepunching for multiplayer codebase. 2023-01-25 18:26:13 +08:00
genxium
5df545e168 Fixes for frontend hole punching compatibility. 2023-01-25 11:57:59 +08:00
genxium
6bc3feab58 Fixed part of Cpp to Js callback scopes. 2023-01-24 21:30:58 +08:00
genxium
e21e1b840f Drafted udp holepunching upsync pathway. 2023-01-24 12:00:49 +08:00
genxium
ef345e0e48 Fine tune for rollback params. 2023-01-24 10:08:34 +08:00
genxium
0168e2182e Further updates on win32 build templates. 2023-01-23 23:18:21 +08:00
genxium
58b06f6a10 Updated win32 build templates. 2023-01-23 12:22:34 +08:00
genxium
58e60a789f Preparation for native app build. 2023-01-22 23:05:32 +08:00
genxium
b5b43bb596 Minor update. 2023-01-22 14:32:32 +08:00
genxium
59767c1ed5 Added network doctor stats. 2023-01-22 11:39:27 +08:00
genxium
1c6ad5c8f8 Minor fix. 2023-01-21 23:03:55 +08:00
genxium
34e0893eb8 Added network doctor for frontend. 2023-01-21 22:53:41 +08:00
genxium
cc7524becd Updated README. 2023-01-21 11:59:01 +08:00
genxium
d06cb18a08 Fixed inertia on jumping. 2023-01-21 11:11:48 +08:00
genxium
00816fb636 Minor update. 2023-01-21 10:41:38 +08:00
genxium
ff24bea055 Fixed fireball trigger. 2023-01-21 10:40:13 +08:00
genxium
56d66a128a Minor updates. 2023-01-20 23:44:26 +08:00
genxium
2f097dfec5 Added turn-around and dashing actual triggers. 2023-01-20 23:22:02 +08:00
genxium
ff48b47ecc Updated documentation for the plan of UDP secondary session. 2023-01-20 20:49:26 +08:00
genxium
7a0127b17d Updated README. 2023-01-20 14:32:38 +08:00
genxium
59c8427c70 Enhanced turn-around performance. 2023-01-20 11:29:27 +08:00
genxium
c357ebad3b Updated README again. 2023-01-19 22:19:40 +08:00
genxium
b2e1f7c2a6 Updated README. 2023-01-19 10:45:48 +08:00
genxium
9a8c32197e Added TurnAroundFramesToRecover. 2023-01-19 09:20:52 +08:00
genxium
a82a238ce9 Added frontend log for inspection of "onPeerInputFrameUpsync". 2023-01-18 22:43:10 +08:00
genxium
5b76c5bbfb Minor fix. 2023-01-18 18:03:54 +08:00
genxium
b81c470135 Drafted peer inputFrameUpsync broadcasting mechanism. 2023-01-18 17:53:58 +08:00
genxium
48074d48af Misc fixes for UI alignments. 2023-01-18 12:22:12 +08:00
genxium
342efc623c Made frontend landscape. 2023-01-17 23:35:23 +08:00
genxium
b8e757064d Further enhanced frontend input processing and fixed frame data logging. 2023-01-17 17:38:18 +08:00
122 changed files with 18254 additions and 7230 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.vs
**/.vs
battle_srv/test_cases/test_cases
battle_srv/test_cases/tests
*.pid

View File

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

View File

@@ -1,14 +1,16 @@
# Preface
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md). As lots of feedbacks ask for a discussion on using UDP instead, I tried to summarize my personal opinion about it in [ConcerningEdgeCases](./ConcerningEdgeCases.md) -- not necessarily correct but that's indeed a question to face :)
This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
The following video is recorded over INTERNET using an input delay of 4 frames and it feels SMOOTH when playing! Please also checkout [this demo video](https://pan.baidu.com/s/1ML6hNupaPHPJRd5rcTvQvw?pwd=8ruc) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance.
[Demo recorded over INTERNET (Phone-Wifi v.s. PC-Wifi UDP holepunched) using an input delay of 6 frames](https://pan.baidu.com/s/1UArwqDShLoPjYppjjqsTqQ?pwd=10wc), and it feels SMOOTH when playing!
![gif_demo_1](./charts/internet_fireball_explosion_wallmove_spedup.gif)
![Merged_cut_annotated_spedup](./charts/Merged_cut_annotated_spedup.gif)
![gif_demo_2](./charts/jump_sync_spedup.gif)
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.
All gifs are sped up to ~1.5x for file size reduction, kindly note that animations are resumed from a partial progress!
# Notable Features
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786)
@@ -20,7 +22,7 @@ _(how input delay roughly works)_
![input_delay_intro](./charts/InputDelayIntro.jpg)
_(how rollback-and-chase in this project roughly works, kindly note that by the current implementation, each frontend only maintains a `lastAllConfirmedInputFrameId` for all the other peers, because the backend only downsyncs all-confirmed inputFrames, see [markConfirmationIfApplicable](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L1085) for more information -- if a serverless peer-to-peer communication is seriously needed here, consider porting [markConfirmationIfApplicable](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L1085) into frontend for maintaining `lastAllConfirmedInputFrameId` under chaotic reception order of inputFrames from peers)_
_(how rollback-and-chase in this project roughly works)_
![server_clients](./charts/ServerClients.jpg)
![rollback_and_chase_intro](./charts/RollbackAndChase.jpg)
@@ -67,7 +69,7 @@ user@proj-root/battle_srv/configs> cp -r ./configs.template ./configs
user@proj-root/frontend/assets/plugin_scripts> cp ./conf.js.template ./conf.js
```
## 1.2 Actual building & running
## 1.3 Actual building & running
### Backend
```
### The following command runs mysql-server in foreground, it's almost NEVER run in such a way, please find a proper way to run it for yourself
@@ -96,3 +98,48 @@ ErrFatal {"err": "MISCONF Redis is configured to save RDB snapshots, but
```
Just restart your `redis-server` process.
### 2.2 Why not show "PING value" on frontend display?
The most important reason for not showing "PING value" is simple: in most games the "PING value" is collected by a dedicated kernel thread which doesn't interfere the UI thread or the primary networking thread. As this demo primarily runs on browser by far, I don't have this capability easily.
Moreover, in practice I found that to spot sync anomalies, the following tools are much more useful than the "PING VALUE".
- Detection of [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/frontend/assets/scripts/Map.js#L842).
- Detection of [type#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1246).
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1259).
There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`.
![networkstats](./charts/networkstats.png)
### 2.3 WIN32 platform tool versioning
![visual_studio](./charts/VisualStudioSetup.png)
When building for native platforms, it's much more convenient to trigger the CocosCreator project forming frmo CLI, e.g.
```
shell> cd <proj-root>
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=true"
```
### 2.4 CococCreator native build reloading
CocosCreator doesn't have perfect file cache management during native project building, use "Developer Tools > Reload" to reset the IDE status upon mysterious errors.
![ccc_reload](./charts/NativeBuildReload.png)
Another issue is with the package name, see the screenshot below, kindly note that after successfully building with a blank package name, you can then re-fill the desired package name and build again!
![ccc_package_name](./charts/PackageNameIssueInCcc.png)
### 2.5 Checking UDP port binding result
__*nix__
```
netstat -anp | grep <your_port>
```
__Windows__
```
netstat -ano | grep <your_port>
```
### 2.6 Checking native code crash on non-rooted Android phone
```
DeveloperOs> adb bugreport ./logs.zip
# The file "logs.zip" will be automatically pulled to current folder of the DeveloperOS, copy "logs/FS/data/tomestones" out of the zip, then use the binary "$NDK_ROOT/ndk-stack" to analyze whichever tombstone you're interested in, for example, I often use the following
DeveloperOs> ${NDK_ROOT}/ndk-stack.cmd -sym \path\to\DelayNoMore\frontend\build\jsb-link\frameworks\runtime-src\proj.android-studio\app\build\intermediates\ndkBuild\debug\obj\local\arm64-v8a -dump \path\to\tombstones\tombstone_03
# The param "-sym \path\to\objs" tells "ndk-stack" to decode "tombstone_03" with symbols provided by all the files inside that "\path\to\objs".
```

View File

@@ -37,6 +37,8 @@ type mysqlConf struct {
type sioConf struct {
HostAndPort string `json:"hostAndPort"`
UdpHost string `json:"udpHost"`
UdpPort int `json:"udpPort"`
}
type botServerConf struct {

View File

@@ -1,3 +1,5 @@
{
"hostAndPort": "0.0.0.0:9992"
"hostAndPort": "0.0.0.0:9992",
"udpHost": "0.0.0.0",
"udpPort": 3000
}

View File

@@ -23,6 +23,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/robfig/cron"
"go.uber.org/zap"
"net"
)
func main() {
@@ -34,7 +36,7 @@ func main() {
env_tools.MergeTestPlayerAccounts()
}
models.InitRoomHeapManager()
startScheduler()
// startScheduler()
router := gin.Default()
setRouter(router)
@@ -54,6 +56,7 @@ func main() {
}
Logger.Info("Listening and serving HTTP on", zap.Any("Conf.Sio.HostAndPort", Conf.Sio.HostAndPort))
}()
go startGrandUdpServer()
var gracefulStop = make(chan os.Signal)
signal.Notify(gracefulStop, syscall.SIGTERM)
signal.Notify(gracefulStop, syscall.SIGINT)
@@ -89,6 +92,7 @@ func setRouter(router *gin.Engine) {
router.StaticFS("/asset", http.Dir(filepath.Join(Conf.General.AppRoot, "asset")))
router.GET("/ping", f)
router.GET("/tsrht", ws.Serve)
router.GET("/tsrhtSecondary", ws.HandleSecondaryWsSessionForPlayer)
apiRouter := router.Group("/api")
{
@@ -113,3 +117,33 @@ func startScheduler() {
//c.AddFunc("*/1 * * * * *", FuncName)
c.Start()
}
func startGrandUdpServer() {
conn, err := net.ListenUDP("udp", &net.UDPAddr{
Port: Conf.Sio.UdpPort,
IP: net.ParseIP(Conf.Sio.UdpHost),
})
if err != nil {
panic(err)
}
defer func() {
conn.Close()
if r := recover(); r != nil {
Logger.Error("`GrandUdpServer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("The `GrandUdpServer` is stopped"))
}()
Logger.Info(fmt.Sprintf("`GrandUdpServer` started at %s", conn.LocalAddr().String()))
for {
message := make([]byte, 2046)
rlen, remote, err := conn.ReadFromUDP(message[:])
if err != nil {
panic(err)
}
Logger.Info(fmt.Sprintf("`GrandUdpServer` received: %d bytes from %s\n", rlen, remote))
ws.HandleUdpHolePunchingForPlayer(message[0:rlen], remote)
}
}

View File

@@ -22,33 +22,36 @@ func toPbRoomDownsyncFrame(rdf *battle.RoomDownsyncFrame) *pb.RoomDownsyncFrame
for i, last := range rdf.PlayersArr {
pbPlayer := &pb.PlayerDownsync{
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
Hp: last.Hp,
MaxHp: last.MaxHp,
ColliderRadius: last.ColliderRadius,
Score: last.Score,
Removed: last.Removed,
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
CapturedByInertia: last.CapturedByInertia,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
Hp: last.Hp,
MaxHp: last.MaxHp,
RevivalVirtualGridX: last.RevivalVirtualGridX,
RevivalVirtualGridY: last.RevivalVirtualGridY,
ColliderRadius: last.ColliderRadius,
Score: last.Score,
Removed: last.Removed,
}
ret.PlayersArr[i] = pbPlayer
}
@@ -146,31 +149,36 @@ func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) []*pb.Play
for _, last := range modelInstances {
pbPlayer := &pb.PlayerDownsync{
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
ColliderRadius: last.ColliderRadius,
Score: last.Score,
Removed: last.Removed,
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
CapturedByInertia: last.CapturedByInertia,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
Hp: last.Hp,
MaxHp: last.MaxHp,
RevivalVirtualGridX: last.RevivalVirtualGridX,
RevivalVirtualGridY: last.RevivalVirtualGridY,
ColliderRadius: last.ColliderRadius,
Score: last.Score,
Removed: last.Removed,
}
if withMetaInfo {
pbPlayer.Name = last.Name
@@ -191,33 +199,36 @@ func toJsPlayers(modelInstances map[int32]*Player) []*battle.PlayerDownsync {
for _, last := range modelInstances {
toRet[last.JoinIndex-1] = &battle.PlayerDownsync{
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
Hp: last.Hp,
MaxHp: last.MaxHp,
ColliderRadius: last.ColliderRadius,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
Score: last.Score,
Removed: last.Removed,
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
DirX: last.DirX,
DirY: last.DirY,
VelX: last.VelX,
VelY: last.VelY,
FramesToRecover: last.FramesToRecover,
FramesInChState: last.FramesInChState,
ActiveSkillId: last.ActiveSkillId,
ActiveSkillHit: last.ActiveSkillHit,
FramesInvinsible: last.FramesInvinsible,
Speed: last.Speed,
BattleState: last.BattleState,
CharacterState: last.CharacterState,
JoinIndex: last.JoinIndex,
BulletTeamId: last.BulletTeamId,
ChCollisionTeamId: last.ChCollisionTeamId,
Hp: last.Hp,
MaxHp: last.MaxHp,
RevivalVirtualGridX: last.RevivalVirtualGridX,
RevivalVirtualGridY: last.RevivalVirtualGridY,
ColliderRadius: last.ColliderRadius,
InAir: last.InAir,
OnWall: last.OnWall,
OnWallNormX: last.OnWallNormX,
OnWallNormY: last.OnWallNormY,
CapturedByInertia: last.CapturedByInertia,
Score: last.Score,
Removed: last.Removed,
}
}

View File

@@ -8,6 +8,7 @@ import (
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"net"
)
type PlayerBattleState struct {
@@ -46,10 +47,15 @@ type Player struct {
TutorialStage int `db:"tutorial_stage"`
// other in-battle info fields
LastReceivedInputFrameId int32
LastSentInputFrameId int32
AckingFrameId int32
AckingInputFrameId int32
LastReceivedInputFrameId int32
LastUdpReceivedInputFrameId int32
LastSentInputFrameId int32
AckingFrameId int32
AckingInputFrameId int32
UdpAddr *PeerUdpAddr
BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend
BattleUdpTunnelAuthKey int32
}
func ExistPlayerByName(name string) (bool, error) {

View File

@@ -13,6 +13,7 @@ import (
"io/ioutil"
"jsexport/battle"
"math/rand"
"net"
"os"
"path/filepath"
"resolv"
@@ -27,10 +28,12 @@ const (
UPSYNC_MSG_ACT_PLAYER_CMD = int32(2)
UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = int32(3)
DOWNSYNC_MSG_ACT_HB_REQ = int32(1)
DOWNSYNC_MSG_ACT_INPUT_BATCH = int32(2)
DOWNSYNC_MSG_ACT_BATTLE_STOPPED = int32(3)
DOWNSYNC_MSG_ACT_FORCED_RESYNC = int32(4)
DOWNSYNC_MSG_ACT_HB_REQ = int32(1)
DOWNSYNC_MSG_ACT_INPUT_BATCH = int32(2)
DOWNSYNC_MSG_ACT_BATTLE_STOPPED = int32(3)
DOWNSYNC_MSG_ACT_FORCED_RESYNC = int32(4)
DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH = int32(5)
DOWNSYNC_MSG_ACT_PEER_UDP_ADDR = int32(6)
DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = int32(-1)
DOWNSYNC_MSG_ACT_BATTLE_START = int32(0)
@@ -116,10 +119,15 @@ type Room struct {
*
* Moreover, during the invocation of `PlayerSignalToCloseDict`, the `Player` instance is supposed to be deallocated (though not synchronously).
*/
PlayerDownsyncSessionDict map[int32]*websocket.Conn
PlayerDownsyncChanDict map[int32](chan pb.InputsBufferSnapshot)
PlayerDownsyncSessionDict map[int32]*websocket.Conn
PlayerSignalToCloseDict map[int32]SignalToCloseConnCbType
PlayerDownsyncChanDict map[int32](chan pb.InputsBufferSnapshot)
PlayerSecondaryDownsyncSessionDict map[int32]*websocket.Conn
PlayerSecondarySignalToCloseDict map[int32]SignalToCloseConnCbType
PlayerSecondaryDownsyncChanDict map[int32](chan pb.InputsBufferSnapshot)
PlayerActiveWatchdogDict map[int32](*Watchdog)
PlayerSignalToCloseDict map[int32]SignalToCloseConnCbType
Score float32
State int32
Index int
@@ -128,7 +136,7 @@ type Room struct {
EffectivePlayerCount int32
DismissalWaitGroup sync.WaitGroup
InputsBuffer *battle.RingBuffer // Indices are STRICTLY consecutive
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId]
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId, player.LastUdpReceivedInputFrameId]
RenderFrameBuffer *battle.RingBuffer // Indices are STRICTLY consecutive
LatestPlayerUpsyncedInputFrameId int32
LastAllConfirmedInputFrameId int32
@@ -150,6 +158,10 @@ type Room struct {
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
LastIndividuallyConfirmedInputList []uint64
BattleUdpTunnelLock sync.Mutex
BattleUdpTunnelAddr *pb.PeerUdpAddr
BattleUdpTunnel *net.UDPConn
}
func (pR *Room) updateScore() {
@@ -170,10 +182,14 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
defer pR.onPlayerAdded(playerId)
pPlayerFromDbInit.UdpAddr = nil
pPlayerFromDbInit.BattleUdpTunnelAddr = nil
pPlayerFromDbInit.BattleUdpTunnelAuthKey = rand.Int31()
pPlayerFromDbInit.AckingFrameId = -1
pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastUdpReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -184,7 +200,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
newWatchdog := NewWatchdog(ConstVals.Ws.WillKickIfInactiveFor, func() {
Logger.Warn("Conn inactive watchdog triggered#1:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
signalToCloseConnOfThisPlayer(Constants.RetCode.ActiveWatchdog, "")
pR.signalToCloseAllSessionsOfPlayer(playerId, Constants.RetCode.ActiveWatchdog)
})
newWatchdog.Stop()
pR.PlayerActiveWatchdogDict[playerId] = newWatchdog
@@ -209,9 +225,13 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
*/
defer pR.onPlayerReAdded(playerId)
pEffectiveInRoomPlayerInstance := pR.Players[playerId]
pEffectiveInRoomPlayerInstance.UdpAddr = nil
pEffectiveInRoomPlayerInstance.BattleUdpTunnelAddr = nil
pEffectiveInRoomPlayerInstance.BattleUdpTunnelAuthKey = rand.Int31()
pEffectiveInRoomPlayerInstance.AckingFrameId = -1
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
// [WARNING] DON'T reset "player.LastReceivedInputFrameId" & "player.LastUdpReceivedInputFrameId" upon reconnection!
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -221,7 +241,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pR.PlayerSignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
pR.PlayerActiveWatchdogDict[playerId] = NewWatchdog(ConstVals.Ws.WillKickIfInactiveFor, func() {
Logger.Warn("Conn inactive watchdog triggered#2:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
signalToCloseConnOfThisPlayer(Constants.RetCode.ActiveWatchdog, "")
pR.signalToCloseAllSessionsOfPlayer(playerId, Constants.RetCode.ActiveWatchdog)
}) // For ReAdded player the new watchdog starts immediately
Logger.Warn("ReAddPlayerIfPossible finished.", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("joinIndex", pEffectiveInRoomPlayerInstance.JoinIndex), zap.Any("playerBattleState", pEffectiveInRoomPlayerInstance.BattleState), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount), zap.Any("AckingFrameId", pEffectiveInRoomPlayerInstance.AckingFrameId), zap.Any("AckingInputFrameId", pEffectiveInRoomPlayerInstance.AckingInputFrameId), zap.Any("LastSentInputFrameId", pEffectiveInRoomPlayerInstance.LastSentInputFrameId))
@@ -309,8 +329,8 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
// Appending of the array of strings can be very SLOW due to on-demand heap allocation! Use this printing with caution.
s := make([]string, 0)
s = append(s, fmt.Sprintf("{renderFrameId: %v, stInputFrameId: %v, edInputFrameId: %v, lastAllConfirmedInputFrameIdWithChange: %v, lastAllConfirmedInputFrameId: %v}", pR.RenderFrameId, pR.InputsBuffer.StFrameId, pR.InputsBuffer.EdFrameId, pR.LastAllConfirmedInputFrameIdWithChange, pR.LastAllConfirmedInputFrameId))
for playerId, player := range pR.PlayersArr {
s = append(s, fmt.Sprintf("{playerId: %v, ackingFrameId: %v, ackingInputFrameId: %v, lastSentInputFrameId: %v}", playerId, player.AckingFrameId, player.AckingInputFrameId, player.LastSentInputFrameId))
for _, player := range pR.PlayersArr {
s = append(s, fmt.Sprintf("{playerId: %v, ackingFrameId: %v, ackingInputFrameId: %v, lastSentInputFrameId: %v}", player.Id, player.AckingFrameId, player.AckingInputFrameId, player.LastSentInputFrameId))
}
for i := pR.InputsBuffer.StFrameId; i < pR.InputsBuffer.EdFrameId; i++ {
tmp := pR.InputsBuffer.GetByFrameId(i)
@@ -482,7 +502,7 @@ func (pR *Room) StartBattle() {
kickoffFrameJs := pR.RenderFrameBuffer.GetByFrameId(0).(*battle.RoomDownsyncFrame)
pbKickOffRenderFrame := toPbRoomDownsyncFrame(kickoffFrameJs)
pbKickOffRenderFrame.SpeciesIdList = pR.SpeciesIdList
pR.sendSafely(pbKickOffRenderFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId, true)
pR.sendSafely(pbKickOffRenderFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId, true, MAGIC_JOIN_INDEX_DEFAULT)
}
Logger.Info(fmt.Sprintf("In `battleMainLoop` for roomId=%v sent out kickoffFrame", pR.Id))
}
@@ -509,7 +529,7 @@ func (pR *Room) StartBattle() {
}
}
downsyncLoop := func(playerId int32, player *Player, playerDownsyncChan chan pb.InputsBufferSnapshot) {
downsyncLoop := func(playerId int32, player *Player, playerDownsyncChan chan pb.InputsBufferSnapshot, playerSecondaryDownsyncChan chan pb.InputsBufferSnapshot) {
defer func() {
if r := recover(); r != nil {
Logger.Error("downsyncLoop, recovery spot#1, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("panic", r))
@@ -517,19 +537,23 @@ func (pR *Room) StartBattle() {
Logger.Info(fmt.Sprintf("The `downsyncLoop` for (roomId=%v, playerId=%v) is stopped@renderFrameId=%v", pR.Id, playerId, pR.RenderFrameId))
}()
Logger.Debug(fmt.Sprintf("Started downsyncLoop for (roomId: %d, playerId:%d, playerDownsyncChan:%p)", pR.Id, playerId, playerDownsyncChan))
//Logger.Info(fmt.Sprintf("Started downsyncLoop for (roomId: %d, playerId:%d, playerDownsyncChan:%p)", pR.Id, playerId, playerDownsyncChan))
for {
nowBattleState := atomic.LoadInt32(&pR.State)
switch nowBattleState {
case RoomBattleStateIns.IDLE, RoomBattleStateIns.STOPPING_BATTLE_FOR_SETTLEMENT, RoomBattleStateIns.IN_SETTLEMENT, RoomBattleStateIns.IN_DISMISSAL:
Logger.Warn(fmt.Sprintf("Battle is not waiting/preparing/active for playerDownsyncChan for (roomId: %d, playerId:%d)", pR.Id, playerId))
return
}
select {
case inputsBufferSnapshot := <-playerDownsyncChan:
nowBattleState := atomic.LoadInt32(&pR.State)
switch nowBattleState {
case RoomBattleStateIns.IDLE, RoomBattleStateIns.STOPPING_BATTLE_FOR_SETTLEMENT, RoomBattleStateIns.IN_SETTLEMENT, RoomBattleStateIns.IN_DISMISSAL:
Logger.Warn(fmt.Sprintf("Battle is not waiting/preparing/active for playerDownsyncChan for (roomId: %d, playerId:%d)", pR.Id, playerId))
return
}
pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync)
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d)#2", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, playerId))
case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan:
pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex)
//Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId))
default:
}
}
@@ -542,7 +566,8 @@ func (pR *Room) StartBattle() {
Each "playerDownsyncChan" stays alive through out the lifecycle of room instead of each "playerDownsyncSession", i.e. not closed or dereferenced upon disconnection.
*/
pR.PlayerDownsyncChanDict[playerId] = make(chan pb.InputsBufferSnapshot, pR.InputsBuffer.N)
go downsyncLoop(playerId, player, pR.PlayerDownsyncChanDict[playerId])
pR.PlayerSecondaryDownsyncChanDict[playerId] = make(chan pb.InputsBufferSnapshot, pR.InputsBuffer.N)
go downsyncLoop(playerId, player, pR.PlayerDownsyncChanDict[playerId], pR.PlayerSecondaryDownsyncChanDict[playerId])
}
pR.onBattlePrepare(func() {
@@ -551,7 +576,7 @@ func (pR *Room) StartBattle() {
})
}
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq, fromUDP bool) {
/*
[WARNING] This function "OnBattleCmdReceived" could be called by different ws sessions and thus from different threads!
@@ -596,10 +621,21 @@ func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
//Logger.Debug(fmt.Sprintf("OnBattleCmdReceived-InputsBufferLock unlocked: roomId=%v, fromPlayerId=%v", pR.Id, playerId))
}()
inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player)
inputsBufferSnapshot := pR.markConfirmationIfApplicable(inputFrameUpsyncBatch, playerId, player, fromUDP)
if nil != inputsBufferSnapshot {
pR.downsyncToAllPlayers(inputsBufferSnapshot)
}
} /*else {
// FIXME: Enable this block after we can proactively detect whether there's any "secondary ws session player" in the battle to avoid waste of resource in creating the snapshot
// no new all-confirmed
toSendInputFrameDownsyncs := pR.cloneInputsBuffer(inputFrameUpsyncBatch[0].InputFrameId, inputFrameUpsyncBatch[len(inputFrameUpsyncBatch)-1].InputFrameId+1)
inputsBufferSnapshot = &pb.InputsBufferSnapshot{
ToSendInputFrameDownsyncs: toSendInputFrameDownsyncs,
PeerJoinIndex: player.JoinIndex,
}
//Logger.Info(fmt.Sprintf("OnBattleCmdReceived no new all-confirmed: roomId=%v, fromPlayerId=%v, forming peer broadcasting snapshot=%v", pR.Id, playerId, inputsBufferSnapshot))
pR.broadcastPeerUpsyncForBetterPrediction(inputsBufferSnapshot)
}*/
}
func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *battle.InputFrameDownsync, playerId int32) {
@@ -641,6 +677,10 @@ func (pR *Room) StopBattleForSettlement() {
if RoomBattleStateIns.IN_BATTLE != pR.State {
return
}
pR.BattleUdpTunnelLock.Lock()
pR.BattleUdpTunnel.Close()
pR.BattleUdpTunnelLock.Unlock()
pR.State = RoomBattleStateIns.STOPPING_BATTLE_FOR_SETTLEMENT
Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id))
pR.RenderFrameId++
@@ -650,7 +690,7 @@ func (pR *Room) StopBattleForSettlement() {
PlayersArr: toPbPlayers(pR.Players, false),
CountdownNanos: -1, // TODO: Replace this magic constant!
}
pR.sendSafely(&assembledFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_STOPPED, playerId, true)
pR.sendSafely(&assembledFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_STOPPED, playerId, true, MAGIC_JOIN_INDEX_DEFAULT)
}
// Note that `pR.onBattleStoppedForSettlement` will be called by `battleMainLoop`.
}
@@ -679,7 +719,7 @@ func (pR *Room) onBattlePrepare(cb BattleStartCbType) {
Logger.Info("Sending out frame for RoomBattleState.PREPARE:", zap.Any("battleReadyToStartFrame", battleReadyToStartFrame))
for _, player := range pR.Players {
pR.sendSafely(battleReadyToStartFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START, player.Id, true)
pR.sendSafely(battleReadyToStartFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START, player.Id, true, MAGIC_JOIN_INDEX_DEFAULT)
}
battlePreparationNanos := int64(6000000000)
@@ -748,6 +788,7 @@ func (pR *Room) OnDismissed() {
pR.CharacterConfigsArr = make([]*battle.CharacterConfig, pR.Capacity)
pR.CollisionSysMap = make(map[int32]*resolv.Object)
pR.PlayerDownsyncSessionDict = make(map[int32]*websocket.Conn)
pR.PlayerSecondaryDownsyncSessionDict = make(map[int32]*websocket.Conn)
for _, oldWatchdog := range pR.PlayerActiveWatchdogDict {
oldWatchdog.Stop()
}
@@ -756,7 +797,12 @@ func (pR *Room) OnDismissed() {
close(oldChan)
}
pR.PlayerDownsyncChanDict = make(map[int32](chan pb.InputsBufferSnapshot))
for _, oldChan := range pR.PlayerSecondaryDownsyncChanDict {
close(oldChan)
}
pR.PlayerSecondaryDownsyncChanDict = make(map[int32](chan pb.InputsBufferSnapshot))
pR.PlayerSignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.PlayerSecondarySignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.JoinIndexBooleanArr = make([]bool, pR.Capacity)
pR.RenderCacheSize = 1024
pR.RenderFrameBuffer = battle.NewRingBuffer(pR.RenderCacheSize)
@@ -771,7 +817,7 @@ func (pR *Room) OnDismissed() {
pR.RenderFrameId = 0
pR.CurDynamicsRenderFrameId = 0
pR.NstDelayFrames = 16
pR.NstDelayFrames = 24
serverFps := 60
pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript
@@ -779,39 +825,50 @@ func (pR *Room) OnDismissed() {
dilutedServerFps := float64(58.0) // Don't set this value too small, otherwise we might miss force confirmation needs for slow tickers!
pR.dilutedRollbackEstimatedDtNanos = int64(float64(pR.RollbackEstimatedDtNanos) * float64(serverFps) / dilutedServerFps)
pR.BattleDurationFrames = int32(60 * serverFps)
//pR.BattleDurationFrames = int32(20 * serverFps)
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
pR.InputFrameUpsyncDelayTolerance = battle.ConvertToNoDelayInputFrameId(pR.NstDelayFrames) - 1 // this value should be strictly smaller than (NstDelayFrames >> InputScaleFrames), otherwise "type#1 forceConfirmation" might become a lag avalanche
pR.MaxChasingRenderFramesPerUpdate = 12 // Don't set this value too high to avoid exhausting frontend CPU within a single frame
pR.MaxChasingRenderFramesPerUpdate = 9 // Don't set this value too high to avoid exhausting frontend CPU within a single frame, roughly as the "turn-around frames to recover" is empirically OK
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
pR.ForceAllResyncOnAnyActiveSlowTicker = true // See tradeoff discussion in "downsyncToAllPlayers"
pR.FrameDataLoggingEnabled = false // [WARNING] DON'T ENABLE ON LONG BATTLE DURATION! It consumes A LOT OF MEMORY!
pR.BattleUdpTunnelLock.Lock()
pR.BattleUdpTunnel = nil
pR.BattleUdpTunnelAddr = nil
pR.BattleUdpTunnelLock.Unlock()
pR.ChooseStage()
pR.EffectivePlayerCount = 0
// [WARNING] It's deliberately ordered such that "pR.State = RoomBattleStateIns.IDLE" is put AFTER all the refreshing operations above.
pR.State = RoomBattleStateIns.IDLE
go pR.startBattleUdpTunnel() // Would reassign "pR.BattleUdpTunnel"
pR.updateScore()
Logger.Info("The room is completely dismissed(all playerDownsyncChan closed):", zap.Any("roomId", pR.Id))
}
func (pR *Room) expelPlayerDuringGame(playerId int32) {
if signalToCloseConnOfThisPlayer, existent := pR.PlayerSignalToCloseDict[playerId]; existent {
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "") // TODO: Specify an error code
}
pR.signalToCloseAllSessionsOfPlayer(playerId, Constants.RetCode.UnknownError)
pR.onPlayerExpelledDuringGame(playerId)
}
func (pR *Room) expelPlayerForDismissal(playerId int32) {
if signalToCloseConnOfThisPlayer, existent := pR.PlayerSignalToCloseDict[playerId]; existent {
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "") // TODO: Specify an error code
}
pR.signalToCloseAllSessionsOfPlayer(playerId, Constants.RetCode.UnknownError)
pR.onPlayerExpelledForDismissal(playerId)
}
func (pR *Room) signalToCloseAllSessionsOfPlayer(playerId int32, retCode int) {
if signalToCloseConnOfThisPlayer, existent := pR.PlayerSignalToCloseDict[playerId]; existent {
signalToCloseConnOfThisPlayer(retCode, "") // TODO: Specify an error code
}
if signalToCloseConnOfThisPlayer2, existent2 := pR.PlayerSecondarySignalToCloseDict[playerId]; existent2 {
signalToCloseConnOfThisPlayer2(retCode, "") // TODO: Specify an error code
}
}
func (pR *Room) onPlayerExpelledDuringGame(playerId int32) {
pR.onPlayerLost(playerId)
}
@@ -829,6 +886,10 @@ func (pR *Room) OnPlayerDisconnected(playerId int32) {
}
}()
if signalToCloseConnOfThisPlayer2, existent2 := pR.PlayerSecondarySignalToCloseDict[playerId]; existent2 {
signalToCloseConnOfThisPlayer2(Constants.RetCode.UnknownError, "") // TODO: Specify an error code
}
if player, existent := pR.Players[playerId]; existent {
thatPlayerBattleState := atomic.LoadInt32(&(player.BattleState))
switch thatPlayerBattleState {
@@ -888,6 +949,8 @@ func (pR *Room) clearPlayerNetworkSession(playerId int32) {
delete(pR.PlayerActiveWatchdogDict, playerId)
delete(pR.PlayerDownsyncSessionDict, playerId)
delete(pR.PlayerSignalToCloseDict, playerId)
delete(pR.PlayerSecondaryDownsyncSessionDict, playerId)
delete(pR.PlayerSecondarySignalToCloseDict, playerId)
}
}
@@ -917,7 +980,10 @@ func (pR *Room) onPlayerAdded(playerId int32) {
if nil == playerPos {
panic(fmt.Sprintf("onPlayerAdded error, nil == playerPos, roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
pR.Players[playerId].VirtualGridX, pR.Players[playerId].VirtualGridY = battle.WorldToVirtualGridPos(playerPos.X, playerPos.Y)
pR.Players[playerId].RevivalVirtualGridX, pR.Players[playerId].RevivalVirtualGridY = battle.WorldToVirtualGridPos(playerPos.X, playerPos.Y)
pR.Players[playerId].VirtualGridX, pR.Players[playerId].VirtualGridY = pR.Players[playerId].RevivalVirtualGridX, pR.Players[playerId].RevivalVirtualGridY
pR.Players[playerId].MaxHp = 100 // Hardcoded for now
pR.Players[playerId].Hp = pR.Players[playerId].MaxHp
// Hardcoded initial character orientation/facing
if 0 == (pR.Players[playerId].JoinIndex % 2) {
pR.Players[playerId].DirX = -2
@@ -977,7 +1043,7 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
Logger.Debug(fmt.Sprintf("OnPlayerBattleColliderAcked-middle: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, thatPlayerId=%v, thatPlayerBattleState=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, thatPlayer.Id, thatPlayerBattleState))
if thatPlayerId == targetPlayer.Id || (PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK == thatPlayerBattleState || PlayerBattleStateIns.ACTIVE == thatPlayerBattleState) {
Logger.Debug(fmt.Sprintf("OnPlayerBattleColliderAcked-sending DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED: roomId=%v, roomState=%v, targetPlayerId=%v, targetPlayerBattleState=%v, capacity=%v, EffectivePlayerCount=%v", pR.Id, pR.State, targetPlayer.Id, targetPlayer.BattleState, pR.Capacity, pR.EffectivePlayerCount))
pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, thatPlayer.Id, true)
pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, thatPlayer.Id, true, MAGIC_JOIN_INDEX_DEFAULT)
}
}
atomic.StoreInt32(&(targetPlayer.BattleState), PlayerBattleStateIns.ACTIVE)
@@ -1010,28 +1076,45 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
return true
}
func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendInputFrameDownsyncs []*pb.InputFrameDownsync, act int32, playerId int32, needLockExplicitly bool) {
func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendInputFrameDownsyncs []*pb.InputFrameDownsync, act int32, playerId int32, needLockExplicitly bool, peerJoinIndex int32) {
defer func() {
if r := recover(); r != nil {
Logger.Error("sendSafely, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("panic", r))
}
}()
if playerDownsyncSession, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
pResp := &pb.WsResp{
Ret: int32(Constants.RetCode.Ok),
Act: act,
Rdf: roomDownsyncFrame,
InputFrameDownsyncBatch: toSendInputFrameDownsyncs,
}
pResp := &pb.WsResp{
Ret: int32(Constants.RetCode.Ok),
Act: act,
Rdf: roomDownsyncFrame,
InputFrameDownsyncBatch: toSendInputFrameDownsyncs,
PeerJoinIndex: peerJoinIndex,
}
theBytes, marshalErr := proto.Marshal(pResp)
if nil != marshalErr {
panic(fmt.Sprintf("Error marshaling downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
theBytes, marshalErr := proto.Marshal(pResp)
if nil != marshalErr {
panic(fmt.Sprintf("Error marshaling downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
if err := playerDownsyncSession.WriteMessage(websocket.BinaryMessage, theBytes); nil != err {
panic(fmt.Sprintf("Error sending downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v, err=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount, err))
shouldUseSecondaryWsSession := (MAGIC_JOIN_INDEX_DEFAULT != peerJoinIndex && DOWNSYNC_MSG_ACT_INPUT_BATCH == act) // FIXME: Simplify the condition
//Logger.Info(fmt.Sprintf("shouldUseSecondaryWsSession=%v: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", shouldUseSecondaryWsSession, pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
if !shouldUseSecondaryWsSession {
if playerDownsyncSession, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
if err := playerDownsyncSession.WriteMessage(websocket.BinaryMessage, theBytes); nil != err {
panic(fmt.Sprintf("Error sending primary downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v, err=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount, err))
}
}
} else {
/*
[FIXME]
This branch is preferred to use an additional session of each player for sending, and the session is preferrably UDP instead of any TCP-based protocol, but I'm being lazy here.
See `<proj-root>/ConcerningEdgeCases.md` for the advantage of using UDP as a supplement.
*/
if playerSecondaryDownsyncSession, existent := pR.PlayerSecondaryDownsyncSessionDict[playerId]; existent {
if err := playerSecondaryDownsyncSession.WriteMessage(websocket.BinaryMessage, theBytes); nil != err {
panic(fmt.Sprintf("Error sending secondary downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v, err=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount, err))
}
}
}
}
@@ -1082,7 +1165,7 @@ func (pR *Room) getOrPrefabInputFrameDownsync(inputFrameId int32) *battle.InputF
return currInputFrameDownsync
}
func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFrameUpsync, playerId int32, player *Player) *pb.InputsBufferSnapshot {
func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFrameUpsync, playerId int32, player *Player, fromUDP bool) *pb.InputsBufferSnapshot {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
// Step#1, put the received "inputFrameUpsyncBatch" into "pR.InputsBuffer"
for _, inputFrameUpsync := range inputFrameUpsyncBatch {
@@ -1093,6 +1176,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
continue
}
if clientInputFrameId < player.LastReceivedInputFrameId {
// [WARNING] It's important for correctness that we use "player.LastReceivedInputFrameId" instead of "player.LastUdpReceivedInputFrameId" here!
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastReceivedInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastReceivedInputFrameId, pR.InputsBufferString(false)))
continue
}
@@ -1105,11 +1189,25 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
targetInputFrameDownsync.InputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1))
player.LastReceivedInputFrameId = clientInputFrameId
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
if false == fromUDP {
/*
[WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
Moreover, only ws session upsyncs should advance "player.LastReceivedInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
Kindly note that the updates of "player.LastReceivedInputFrameId" could be discrete before and after reconnection.
*/
player.LastReceivedInputFrameId = clientInputFrameId
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
}
}
if clientInputFrameId > player.LastUdpReceivedInputFrameId {
// No need to update "player.LastUdpReceivedInputFrameId" only when "true == fromUDP", we should keep "player.LastUdpReceivedInputFrameId >= player.LastReceivedInputFrameId" at any moment.
player.LastUdpReceivedInputFrameId = clientInputFrameId
// It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block.
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
}
}
@@ -1176,6 +1274,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
totPlayerCnt := uint32(pR.Capacity)
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
unconfirmedMask := uint64(0)
// As "pR.LastAllConfirmedInputFrameId" can be advanced by UDP but "pR.LatestPlayerUpsyncedInputFrameId" could only be advanced by ws session, when the following condition is met we know that the slow ticker is really in trouble!
if pR.LatestPlayerUpsyncedInputFrameId > (pR.LastAllConfirmedInputFrameId + pR.InputFrameUpsyncDelayTolerance + 1) {
// Type#1 check whether there's a significantly slow ticker among players
oldLastAllConfirmedInputFrameId := pR.LastAllConfirmedInputFrameId
@@ -1297,13 +1396,13 @@ func (pR *Room) printBarrier(barrierCollider *resolv.Object) {
}
func (pR *Room) doBattleMainLoopPerTickBackendDynamicsWithProperLocking(prevRenderFrameId int32, pDynamicsDuration *int64) {
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock to about lock: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock to about lock: roomId=%v", pR.Id))
pR.InputsBufferLock.Lock()
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock locked: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock locked: roomId=%v", pR.Id))
defer func() {
pR.InputsBufferLock.Unlock()
Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock unlocked: roomId=%v", pR.Id))
//Logger.Debug(fmt.Sprintf("doBattleMainLoopPerTickBackendDynamicsWithProperLocking-InputsBufferLock unlocked: roomId=%v", pR.Id))
}()
if ok, thatRenderFrameId := battle.ShouldPrefabInputFrameDownsync(prevRenderFrameId, pR.RenderFrameId); ok {
@@ -1347,6 +1446,30 @@ func (pR *Room) doBattleMainLoopPerTickBackendDynamicsWithProperLocking(prevRend
}
}
func (pR *Room) broadcastPeerUpsyncForBetterPrediction(inputsBufferSnapshot *pb.InputsBufferSnapshot) {
// See `<proj-root>/ConcerningEdgeCases.md` for why this method exists.
for _, player := range pR.PlayersArr {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL, PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
continue
}
if player.JoinIndex == inputsBufferSnapshot.PeerJoinIndex {
continue
}
if playerSecondaryDownsyncChan, existent := pR.PlayerSecondaryDownsyncChanDict[player.Id]; existent {
/*
[FIXME]
This function is preferred to use an additional go-channel of each player for sending, see "downsyncLoop" & "Room.sendSafely" for more information!
*/
playerSecondaryDownsyncChan <- (*inputsBufferSnapshot)
} else {
Logger.Warn(fmt.Sprintf("playerDownsyncChan for (roomId: %d, playerId:%d) is gone", pR.Id, player.Id))
}
}
}
func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapshot) {
/*
[WARNING] This function MUST BE called while "pR.InputsBufferLock" is LOCKED to **preserve the order of generation of "inputsBufferSnapshot" for sending** -- see comments in "OnBattleCmdReceived" and [this issue](https://github.com/genxium/DelayNoMore/issues/12).
@@ -1423,14 +1546,14 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
We hereby assume that Golang runtime allocates & frees small amount of RAM quickly enough compared to either network I/O blocking in worst cases or the high frequency "per inputFrameDownsync*player" locking (though "OnBattleCmdReceived" locks at the same frequency but it's inevitable).
*/
playerJoinIndex := player.JoinIndex - 1
playerJoinIndexInBooleanArr := player.JoinIndex - 1
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL, PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
return
}
isSlowTicker := (0 < (unconfirmedMask & uint64(1<<uint32(playerJoinIndex))))
isSlowTicker := (0 < (unconfirmedMask & uint64(1<<uint32(playerJoinIndexInBooleanArr))))
shouldResync1 := (PlayerBattleStateIns.READDED_BATTLE_COLLIDER_ACKED == playerBattleState) // i.e. implies that "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId"
shouldResync2 := isSlowTicker // This condition is critical, if we don't send resync upon this condition, the "reconnected or slowly-clocking player" might never get its input synced
shouldResync3 := shouldForceResync
@@ -1455,13 +1578,13 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
refRenderFrame.BackendUnconfirmedMask = unconfirmedMask
pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame)
pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList
pR.sendSafely(pbRefRenderFrame, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId, false)
pR.sendSafely(pbRefRenderFrame, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)
//Logger.Warn(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: InputsBuffer=%v", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, pR.InputsBufferString(false)))
if shouldResync1 || shouldResync3 {
Logger.Debug(fmt.Sprintf("Sent refRenderFrameId=%v & inputFrameIds [%d, %d), for roomId=%v, playerId=%d, playerJoinIndex=%d, renderFrameId=%d, curDynamicsRenderFrameId=%d, playerLastSentInputFrameId=%d: shouldResync1=%v, shouldResync2=%v, shouldResync3=%v, playerBattleState=%d", refRenderFrameId, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id, playerId, player.JoinIndex, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, player.LastSentInputFrameId, shouldResync1, shouldResync2, shouldResync3, playerBattleState))
}
} else {
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false)
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)
}
player.LastSentInputFrameId = toSendInputFrameIdEd - 1
if shouldResync1 {
@@ -1469,6 +1592,16 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
}
}
func (pR *Room) downsyncPeerInputFrameUpsyncToSinglePlayer(playerId int32, player *Player, toSendInputFrameDownsyncsSnapshot []*pb.InputFrameDownsync, peerJoinIndex int32) {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL, PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
return
}
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH, playerId, false, peerJoinIndex)
}
func (pR *Room) cloneInputsBuffer(stFrameId, edFrameId int32) []*pb.InputFrameDownsync {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
cloned := make([]*pb.InputFrameDownsync, 0, edFrameId-stFrameId)
@@ -1501,3 +1634,163 @@ func (pR *Room) cloneInputsBuffer(stFrameId, edFrameId int32) []*pb.InputFrameDo
return cloned
}
func (pR *Room) SetSecondarySession(playerId int32, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) {
// TODO: Use a dedicated lock
if player, ok := pR.Players[playerId]; ok {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL:
// Kindly note that "PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK" are allowed
return
}
if _, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
if _, existent2 := pR.PlayerSecondaryDownsyncSessionDict[playerId]; !existent2 {
Logger.Info(fmt.Sprintf("SetSecondarySession for roomId=%v, playerId=%d, pR.Players=%v", pR.Id, playerId, pR.Players))
pR.PlayerSecondaryDownsyncSessionDict[playerId] = session
pR.PlayerSecondarySignalToCloseDict[playerId] = signalToCloseConnOfThisPlayer
}
}
}
}
func (pR *Room) UpdatePeerUdpAddrList(playerId int32, peerAddr *net.UDPAddr, pReq *pb.HolePunchUpsync) {
// TODO: There's a chance that by now "player.JoinIndex" is not yet determined, use a lock to sync
if player, ok := pR.Players[playerId]; ok && MAGIC_JOIN_INDEX_DEFAULT != player.JoinIndex {
playerBattleState := atomic.LoadInt32(&(player.BattleState))
switch playerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL:
// Kindly note that "PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK, PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK" are allowed
return
}
if _, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
player.UdpAddr = &pb.PeerUdpAddr{
Ip: peerAddr.IP.String(),
Port: int32(peerAddr.Port),
AuthKey: pReq.AuthKey,
}
Logger.Info(fmt.Sprintf("UpdatePeerUdpAddrList done for roomId=%v, playerId=%d, peerAddr=%s", pR.Id, playerId, peerAddr))
peerJoinIndex := player.JoinIndex
peerUdpAddrList := make([]*pb.PeerUdpAddr, pR.Capacity, pR.Capacity)
for _, otherPlayer := range pR.Players {
if MAGIC_JOIN_INDEX_DEFAULT == otherPlayer.JoinIndex {
// TODO: Again this shouldn't happen, apply proper locking
continue
}
// In case of highly concurrent update that might occur while later marshalling, use the ptr of a copy
peerUdpAddrList[otherPlayer.JoinIndex-1] = &pb.PeerUdpAddr{
Ip: otherPlayer.UdpAddr.Ip,
Port: otherPlayer.UdpAddr.Port,
AuthKey: otherPlayer.UdpAddr.AuthKey,
}
}
// Broadcast this new UDP addr to all the existing players
for otherPlayerId, otherPlayer := range pR.Players {
otherPlayerBattleState := atomic.LoadInt32(&(otherPlayer.BattleState))
switch otherPlayerBattleState {
case PlayerBattleStateIns.DISCONNECTED, PlayerBattleStateIns.LOST, PlayerBattleStateIns.EXPELLED_DURING_GAME, PlayerBattleStateIns.EXPELLED_IN_DISMISSAL:
continue
}
Logger.Info(fmt.Sprintf("Downsyncing peerUdpAddrList for roomId=%v, playerId=%d", pR.Id, otherPlayerId))
pR.sendSafely(&pb.RoomDownsyncFrame{
PeerUdpAddrList: peerUdpAddrList,
}, nil, DOWNSYNC_MSG_ACT_PEER_UDP_ADDR, otherPlayerId, false, peerJoinIndex)
}
}
}
}
func (pR *Room) startBattleUdpTunnel() {
defer func() {
if r := recover(); r != nil {
Logger.Error("`BattleUdpTunnel` recovery spot#1, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` stopped for (roomId=%d)@renderFrameId=%v", pR.Id, pR.RenderFrameId))
}()
pR.BattleUdpTunnelLock.Lock()
conn, err := net.ListenUDP("udp", &net.UDPAddr{
Port: 0,
IP: net.ParseIP(Conf.Sio.UdpHost),
})
if nil != err {
// No need to close the "conn" upon error here
pR.BattleUdpTunnelLock.Unlock()
panic(err)
}
pR.BattleUdpTunnel = conn
switch v := conn.LocalAddr().(type) {
case (*net.UDPAddr):
pR.BattleUdpTunnelAddr = &pb.PeerUdpAddr{
Ip: Conf.Sio.UdpHost,
Port: int32(v.Port),
AuthKey: 0, // To be determined for each specific player upon joining and sent to it by BattleColliderInfo
}
}
pR.BattleUdpTunnelLock.Unlock()
defer func() {
if r := recover(); r != nil {
Logger.Warn("`BattleUdpTunnel` recovery spot#2, recovered from: ", zap.Any("roomId", pR.Id), zap.Any("panic", r))
}
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` closed for (roomId=%d)@renderFrameId=%v", pR.Id, pR.RenderFrameId))
}()
Logger.Info(fmt.Sprintf("`BattleUdpTunnel` started for roomId=%d at %s", pR.Id, conn.LocalAddr().String()))
for {
message := make([]byte, 128)
rlen, remote, err := conn.ReadFromUDP(message[:]) // Would be unblocked when "conn.Close()" is called from another thread/goroutine, reference https://pkg.go.dev/net@go1.18.6#PacketConn
if nil != err {
// Should proceed to close the "conn" upon error here, if "conn" is already closed it'd just throw another error to be catched by "spot#2"
conn.Close()
panic(err)
}
pReq := new(pb.WsReq)
bytes := message[0:rlen]
if unmarshalErr := proto.Unmarshal(bytes, pReq); nil != unmarshalErr {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d failed to unmarshal %d bytes", pR.Id, rlen), zap.Error(unmarshalErr))
continue
}
playerId := pReq.PlayerId
//Logger.Info(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received decoded WsReq:", pR.Id), zap.Any("pReq", pReq))
if player, exists1 := pR.Players[playerId]; exists1 {
authKey := pReq.AuthKey
if authKey != player.BattleUdpTunnelAuthKey {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received %d bytes for playerId=%d from %s, but (incomingAuthKey:%d != playerBattleUdpTunnelAuthKey:%d)\n", pR.Id, rlen, playerId, remote, authKey, player.BattleUdpTunnelAuthKey))
continue
}
if _, existent := pR.PlayerDownsyncSessionDict[playerId]; existent {
player.BattleUdpTunnelAddr = remote
//Logger.Info(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d updated battleUdpAddr for playerId=%d to be %s\n", pR.Id, playerId, remote))
nowBattleState := atomic.LoadInt32(&pR.State)
if RoomBattleStateIns.IN_BATTLE == nowBattleState {
batch := pReq.InputFrameUpsyncBatch
if nil != batch && 0 < len(batch) {
peerJoinIndex := pReq.JoinIndex
// Broadcast to every other player in the same room/battle
for _, otherPlayer := range pR.PlayersArr {
if otherPlayer.JoinIndex == peerJoinIndex {
continue
}
_, wrerr := conn.WriteTo(bytes, otherPlayer.BattleUdpTunnelAddr)
if nil != wrerr {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d failed to forward upsync from (playerId:%d, joinIndex:%d, addr:%s) to (otherPlayerId:%d, otherPlayerJoinIndex:%d, otherPlayerAddr:%s)\n", pR.Id, playerId, peerJoinIndex, remote, otherPlayer.Id, otherPlayer.JoinIndex, otherPlayer.BattleUdpTunnelAddr))
}
}
pR.OnBattleCmdReceived(pReq, true) // To help advance "pR.LastAllConfirmedInputFrameId" asap, and even if "pR.LastAllConfirmedInputFrameId" is not advanced due to packet loss, these UDP packets would help prefill the "InputsBuffer" with correct player "future inputs (compared to ws session)" such that when "forceConfirmation" occurs we have as many correct predictions as possible
}
}
} else {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received validated %d bytes for playerId=%d from %s, but primary downsync session for it doesn't exist\n", pR.Id, rlen, playerId, remote))
}
} else {
Logger.Warn(fmt.Sprintf("`BattleUdpTunnel` for roomId=%d received invalid %d bytes for playerId=%d from %s, but it doesn't belong to this room!\n", pR.Id, rlen, playerId, remote))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ import (
"github.com/golang/protobuf/proto"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"net"
"net/http"
"strconv"
"sync/atomic"
@@ -46,6 +47,7 @@ func Serve(c *gin.Context) {
c.AbortWithStatus(http.StatusBadRequest)
return
}
boundRoomId := 0
expectedRoomId := 0
var err error
@@ -255,17 +257,25 @@ func Serve(c *gin.Context) {
SpaceOffsetX: pRoom.SpaceOffsetX,
SpaceOffsetY: pRoom.SpaceOffsetY,
RenderCacheSize: pRoom.RenderCacheSize,
CollisionMinStep: pRoom.CollisionMinStep,
RenderCacheSize: pRoom.RenderCacheSize,
CollisionMinStep: pRoom.CollisionMinStep,
BoundRoomCapacity: int32(pRoom.Capacity),
BattleUdpTunnel: &pb.PeerUdpAddr{
Ip: pRoom.BattleUdpTunnelAddr.Ip,
Port: pRoom.BattleUdpTunnelAddr.Port,
AuthKey: pThePlayer.BattleUdpTunnelAuthKey,
},
FrameDataLoggingEnabled: pRoom.FrameDataLoggingEnabled,
}
resp := &pb.WsResp{
Ret: int32(Constants.RetCode.Ok),
EchoedMsgId: int32(0),
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
BciFrame: bciFrame,
Ret: int32(Constants.RetCode.Ok),
EchoedMsgId: int32(0),
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
BciFrame: bciFrame,
PeerJoinIndex: pThePlayer.JoinIndex,
}
Logger.Debug("Sending downsync HeartbeatRequirements:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("resp", resp))
@@ -378,7 +388,7 @@ func Serve(c *gin.Context) {
startOrFeedHeartbeatWatchdog(conn)
case models.UPSYNC_MSG_ACT_PLAYER_CMD:
startOrFeedHeartbeatWatchdog(conn)
pRoom.OnBattleCmdReceived(pReq)
pRoom.OnBattleCmdReceived(pReq, false)
case models.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK:
res := pRoom.OnPlayerBattleColliderAcked(int32(playerId))
if false == res {
@@ -395,3 +405,118 @@ func Serve(c *gin.Context) {
startOrFeedHeartbeatWatchdog(conn)
go receivingLoopAgainstPlayer()
}
func HandleSecondaryWsSessionForPlayer(c *gin.Context) {
token, ok := c.GetQuery("intAuthToken")
if !ok {
Logger.Warn("Secondary ws session req must have intAuthToken param!")
c.AbortWithStatus(http.StatusBadRequest)
return
}
boundRoomId := 0
var err error = nil
if boundRoomIdStr, hasBoundRoomId := c.GetQuery("boundRoomId"); hasBoundRoomId {
boundRoomId, err = strconv.Atoi(boundRoomIdStr)
if err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}
} else {
Logger.Warn("Secondary ws session req must have boundRoomId param:", zap.Any("intAuthToken", token))
c.AbortWithStatus(http.StatusBadRequest)
return
}
var pRoom *models.Room = nil
// Deliberately querying playerId after querying room, because the former is against persistent storage and could be slow!
if tmpPRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]; !existent {
Logger.Warn("Secondary ws session failed to get:\n", zap.Any("intAuthToken", token), zap.Any("forBoundRoomId", boundRoomId))
c.AbortWithStatus(http.StatusBadRequest)
} else {
pRoom = tmpPRoom
}
// TODO: Wrap the following 2 stmts by sql transaction!
playerId, err := models.GetPlayerIdByToken(token)
if err != nil || playerId == 0 {
// TODO: Abort with specific message.
Logger.Warn("Secondary ws session playerLogin record not found:", zap.Any("intAuthToken", token))
c.AbortWithStatus(http.StatusBadRequest)
return
}
Logger.Info("Secondary ws session playerLogin record has been found:", zap.Any("playerId", playerId), zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId))
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
Logger.Error("Secondary ws session upgrade:", zap.Error(err), zap.Any("playerId", playerId))
c.AbortWithStatus(http.StatusBadRequest)
return
}
connHasBeenSignaledToClose := int32(0)
pConnHasBeenSignaledToClose := &connHasBeenSignaledToClose
signalToCloseConnOfThisPlayer := func(customRetCode int, customRetMsg string) {
if swapped := atomic.CompareAndSwapInt32(pConnHasBeenSignaledToClose, 0, 1); !swapped {
return
}
Logger.Warn("Secondary ws session signalToCloseConnOfThisPlayer:", zap.Any("playerId", playerId), zap.Any("customRetCode", customRetCode), zap.Any("customRetMsg", customRetMsg))
defer func() {
if r := recover(); r != nil {
Logger.Error("Secondary ws session recovered from: ", zap.Any("panic", r))
}
}()
closeMessage := websocket.FormatCloseMessage(customRetCode, customRetMsg)
err := conn.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(time.Millisecond*(ConstVals.Ws.WillKickIfInactiveFor)))
if err != nil {
Logger.Error("Secondary ws session unable to send the CloseFrame control message to player(client-side):", zap.Any("playerId", playerId), zap.Error(err))
}
time.AfterFunc(3*time.Second, func() {
// To actually terminates the underlying TCP connection which might be in `CLOSE_WAIT` state if inspected by `netstat`.
conn.Close()
})
}
onReceivedCloseMessageFromClient := func(code int, text string) error {
Logger.Warn("Secondary ws session triggered `onReceivedCloseMessageFromClient`:", zap.Any("code", code), zap.Any("playerId", playerId), zap.Any("message", text))
signalToCloseConnOfThisPlayer(code, text)
return nil
}
conn.SetCloseHandler(onReceivedCloseMessageFromClient)
pRoom.SetSecondarySession(int32(playerId), conn, signalToCloseConnOfThisPlayer)
}
func HandleUdpHolePunchingForPlayer(message []byte, peerAddr *net.UDPAddr) {
pReq := new(pb.HolePunchUpsync)
if unmarshalErr := proto.Unmarshal(message, pReq); nil != unmarshalErr {
Logger.Error("`GrandUdpServer` failed to unmarshal", zap.Error(unmarshalErr))
return
}
token := pReq.IntAuthToken
boundRoomId := pReq.BoundRoomId
pRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]
// Deliberately querying playerId after querying room, because the former is against persistent storage and could be slow!
if !existent {
Logger.Warn("`GrandUdpServer` failed to get:\n", zap.Any("intAuthToken", token), zap.Any("forBoundRoomId", boundRoomId))
return
}
// TODO: Wrap the following 2 stmts by sql transaction!
playerId, err := models.GetPlayerIdByToken(token)
if err != nil || playerId == 0 {
// TODO: Abort with specific message.
Logger.Warn("`GrandUdpServer` playerLogin record not found for:", zap.Any("intAuthToken", token))
return
}
Logger.Info("`GrandUdpServer` playerLogin record has been found:", zap.Any("playerId", playerId), zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId), zap.Any("peerAddr", peerAddr))
pRoom.UpdatePeerUdpAddrList(int32(playerId), peerAddr, pReq)
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
charts/UDPEssentials.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

BIN
charts/networkstats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

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

View File

@@ -198,3 +198,6 @@ window.getOrCreateAnimationClipForGid = function(gid, tiledMapInfo, tilesElListU
animationClip: animClip,
};
};
// Node.js, this is a workaround to avoid accessing the non-existent "TextDecoder class" from "jsexport.js".
window.fs = function() {};

View File

@@ -3,7 +3,7 @@
"_name": "Fireball1Explosion",
"_objFlags": 0,
"_native": "",
"_duration": 0.26666666666666666,
"_duration": 0.5,
"sample": 60,
"speed": 1,
"wrapMode": 1,
@@ -30,19 +30,19 @@
}
},
{
"frame": 0.15,
"frame": 0.16666666666666666,
"value": {
"__uuid__": "8b566f26-b34d-4da6-bdaa-078358a5b685"
}
},
{
"frame": 0.2,
"frame": 0.31666666666666665,
"value": {
"__uuid__": "6ec5f75d-307e-4292-b667-cbbb5a52c2f6"
}
},
{
"frame": 0.25,
"frame": 0.48333333333333334,
"value": {
"__uuid__": "d89977f1-d927-4a08-9591-9feb1daf68c8"
}

View File

@@ -0,0 +1,85 @@
{
"__type__": "cc.AnimationClip",
"_name": "Fireball2Explosion",
"_objFlags": 0,
"_native": "",
"_duration": 0.5,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "a1979f05-3ecc-4d70-9ea9-7822e35602c3"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "850884ca-2e6a-4d04-94d9-fd929ac33942"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "88b9c254-1fd8-451f-902e-4a43a7ef5d51"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "566342a3-cfde-44c9-afbe-7d9469653ccb"
}
},
{
"frame": 0.18333333333333332,
"value": {
"__uuid__": "d1620a98-de62-4069-8910-122f361d22a4"
}
},
{
"frame": 0.23333333333333334,
"value": {
"__uuid__": "2e9ed070-e592-4e77-8fa1-c5250deb006b"
}
},
{
"frame": 0.2833333333333333,
"value": {
"__uuid__": "a3e8357d-39da-42e8-b26b-f5ae7a68aed7"
}
},
{
"frame": 0.3333333333333333,
"value": {
"__uuid__": "3f3cb45c-732d-4bea-89b4-5495fb0d2c37"
}
},
{
"frame": 0.38333333333333336,
"value": {
"__uuid__": "d7aeb01a-4e04-4037-a2c4-ba72f45f69f3"
}
},
{
"frame": 0.43333333333333335,
"value": {
"__uuid__": "fe4a97a0-1207-4b81-a541-c2da0bf0a6f3"
}
},
{
"frame": 0.48333333333333334,
"value": {
"__uuid__": "97014ab9-8bdd-4b71-9f61-0639327f9159"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "14b92f5c-af81-416a-a162-e5822d20fe68",
"subMetas": {}
}

View File

@@ -0,0 +1,79 @@
{
"__type__": "cc.AnimationClip",
"_name": "Fireball3Explosion",
"_objFlags": 0,
"_native": "",
"_duration": 0.5,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "0e003318-f8c2-40f7-b144-140b5ca1e46a"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "9c2b0cc2-9a52-4052-b796-cd6c6bd940d4"
}
},
{
"frame": 0.11666666666666667,
"value": {
"__uuid__": "76fe0c09-d2d6-432d-bacb-20d297eb4966"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "0735a7ff-0e50-472a-b0f9-8e2cc97be7e7"
}
},
{
"frame": 0.2833333333333333,
"value": {
"__uuid__": "993199a8-54a9-40d1-8d2f-12bf16af934c"
}
},
{
"frame": 0.35,
"value": {
"__uuid__": "5d8d9ffc-b4d6-4518-9946-953929ec055c"
}
},
{
"frame": 0.38333333333333336,
"value": {
"__uuid__": "6501ae08-b0ff-43ad-b5c5-cb6dc67f989d"
}
},
{
"frame": 0.4166666666666667,
"value": {
"__uuid__": "616cfa00-1dba-4a71-8141-36774933b6a9"
}
},
{
"frame": 0.45,
"value": {
"__uuid__": "4b296e86-2e96-4276-b6de-6a6b22530344"
}
},
{
"frame": 0.48333333333333334,
"value": {
"__uuid__": "f9cc8e37-c9c2-4f20-9d7e-4533c4e859fe"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "0dbb90ed-a08a-448c-b06e-4831260e9213",
"subMetas": {}
}

View File

@@ -0,0 +1,97 @@
{
"__type__": "cc.AnimationClip",
"_name": "Fireball2",
"_objFlags": 0,
"_native": "",
"_duration": 0.21666666666666667,
"sample": 60,
"speed": 1,
"wrapMode": 2,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "db4c7e6f-9bee-4e7a-8628-d41b8bcaff42"
}
},
{
"frame": 0.016666666666666666,
"value": {
"__uuid__": "42796576-72b3-49c2-8c5a-ea946fbe1525"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "0aa5a52f-a92a-4a4a-b49b-aee2b5a3eb55"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "0a7b5e41-acdc-4af3-beff-0a42aca9f91a"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "de0b22b7-65ca-455f-bcd1-2ddd6cc114e2"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "e9ce1383-9e3d-4d44-9f80-ab5fa2224138"
}
},
{
"frame": 0.1,
"value": {
"__uuid__": "5b22df7e-414b-44a3-989f-640c5b9417b9"
}
},
{
"frame": 0.11666666666666667,
"value": {
"__uuid__": "f459615c-70a4-421b-b649-a28460332364"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "c2723b9d-fbd8-4524-a0dd-b110451e4e32"
}
},
{
"frame": 0.15,
"value": {
"__uuid__": "4286b3d1-fea2-41fd-8829-7635f546def4"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "4e0d6419-62df-4382-893e-dd7cc47f7770"
}
},
{
"frame": 0.18333333333333332,
"value": {
"__uuid__": "f0cd9259-b323-4fba-ad8b-02d5e56c2cd4"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "0193b66d-06bb-49f9-b2f5-51fff8b16015"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "d2c65ac4-a5b3-411e-8d2d-18d3980649d7",
"subMetas": {}
}

View File

@@ -0,0 +1,73 @@
{
"__type__": "cc.AnimationClip",
"_name": "Fireball3",
"_objFlags": 0,
"_native": "",
"_duration": 0.5666666666666667,
"sample": 60,
"speed": 1,
"wrapMode": 2,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "6af65d40-470c-47de-8b3d-f53c3923bf90"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "ebf64819-79a5-4366-bf70-08f3b1c6114c"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "e03d879b-5227-4c11-a4b9-0a426967d28a"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "a1aa0c83-4a34-43ae-9a8f-56189808df68"
}
},
{
"frame": 0.26666666666666666,
"value": {
"__uuid__": "3cc28dd0-2518-4162-a39d-4e4b19f9d60b"
}
},
{
"frame": 0.35,
"value": {
"__uuid__": "1b41f500-c55b-4cbf-a040-287b6cc0e958"
}
},
{
"frame": 0.43333333333333335,
"value": {
"__uuid__": "cfa24c51-0ad4-4e3b-b571-c5500002d6e9"
}
},
{
"frame": 0.5,
"value": {
"__uuid__": "d4a46a6a-401c-4694-a192-0a7b3ce6f603"
}
},
{
"frame": 0.55,
"value": {
"__uuid__": "c88c5293-9f21-4a1a-a6b6-649e403dc7a2"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "6aef5812-d16c-4da1-96a3-a38ac227c823",
"subMetas": {}
}

View File

@@ -18,61 +18,43 @@
}
},
{
"frame": 0.06666666666666667,
"frame": 0.05,
"value": {
"__uuid__": "dd9a00aa-ddbc-4b01-a7cb-3c43c3a655b6"
}
},
{
"frame": 0.11666666666666667,
"value": {
"__uuid__": "f66e83bd-1afc-4957-bb16-488d70566ed1"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "bd682c41-dc62-49ff-a96a-18b33e50a6de"
}
},
{
"frame": 0.23333333333333334,
"value": {
"__uuid__": "94ccab85-e32f-4e13-b0e5-72c798f78ad1"
}
},
{
"frame": 0.3,
"frame": 0.08333333333333333,
"value": {
"__uuid__": "e80d3a01-5048-42b7-a280-cb6aa01602c2"
}
},
{
"frame": 0.36666666666666664,
"frame": 0.11666666666666667,
"value": {
"__uuid__": "d899088c-be62-47b4-9ebf-0a89a2261565"
}
},
{
"frame": 0.4166666666666667,
"frame": 0.15,
"value": {
"__uuid__": "5b1e5aa7-fd82-47ae-a5b2-6d4983d848ed"
}
},
{
"frame": 0.48333333333333334,
"frame": 0.18333333333333332,
"value": {
"__uuid__": "c2945988-b4bb-4583-a5ef-2fa02b23a347"
}
},
{
"frame": 0.5666666666666667,
"frame": 0.23333333333333334,
"value": {
"__uuid__": "070ea1e3-9c07-4735-8b94-515ef70216ad"
}
},
{
"frame": 0.6666666666666666,
"frame": 0.2833333333333333,
"value": {
"__uuid__": "3b8bc5c0-26df-4218-b7dc-134a36080a35"
}

View File

@@ -0,0 +1,73 @@
{
"__type__": "cc.AnimationClip",
"_name": "Atk4",
"_objFlags": 0,
"_native": "",
"_duration": 1.0166666666666666,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "da597a30-22da-4053-b4ee-1cfa27980a75"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "b3604b4c-426f-4843-bb76-f09a9687950d"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "24b51487-6c91-42d9-bd12-afbbf70f2e4b"
}
},
{
"frame": 0.2833333333333333,
"value": {
"__uuid__": "c318ad71-7a5e-43b0-8098-b7a34a6e6fbe"
}
},
{
"frame": 0.38333333333333336,
"value": {
"__uuid__": "85d6d8d7-81cf-4369-a501-6ad72d70f5a2"
}
},
{
"frame": 0.5,
"value": {
"__uuid__": "42b76eaf-db36-4835-9072-893337c83425"
}
},
{
"frame": 0.6,
"value": {
"__uuid__": "152f23a1-f70f-4db6-bb28-68625aef930f"
}
},
{
"frame": 0.8833333333333333,
"value": {
"__uuid__": "9c907eb5-84ab-4fa9-9404-9085f29706cc"
}
},
{
"frame": 1,
"value": {
"__uuid__": "74f0ffc8-cc25-4fcf-a6d8-bf093daba9ca"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "2aef91f9-ef47-4bb4-bf43-5441723aa639",
"subMetas": {}
}

View File

@@ -0,0 +1,103 @@
{
"__type__": "cc.AnimationClip",
"_name": "Dying",
"_objFlags": 0,
"_native": "",
"_duration": 0.5333333333333333,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "700f93f9-ef84-4cb2-b759-f39ceac1c1d1"
}
},
{
"frame": 0.016666666666666666,
"value": {
"__uuid__": "5b4ea047-594e-4d0b-8e08-e24117bf1e67"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "a822576c-d2eb-4c17-8969-03dd1da5a93e"
}
},
{
"frame": 0.1,
"value": {
"__uuid__": "85e92afc-4359-4a8d-bdfa-958a6134cd6a"
}
},
{
"frame": 0.15,
"value": {
"__uuid__": "88d6e560-1b65-4d78-949c-cbc0e67d33cc"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "9ac16319-c1af-41d1-910b-99cbfd6230b2"
}
},
{
"frame": 0.23333333333333334,
"value": {
"__uuid__": "2a6b168a-458f-4d19-a985-9b00cc6e37e8"
}
},
{
"frame": 0.26666666666666666,
"value": {
"__uuid__": "f8482295-dc0d-4265-be56-0b0a9f6f6b9b"
}
},
{
"frame": 0.31666666666666665,
"value": {
"__uuid__": "0d4a314c-119a-46b9-8dce-dbaacf2523e5"
}
},
{
"frame": 0.36666666666666664,
"value": {
"__uuid__": "18d4ff6c-6b57-461b-8588-03521fafc9d1"
}
},
{
"frame": 0.4,
"value": {
"__uuid__": "f25280f2-442a-4ad7-914e-0f96cbf108f5"
}
},
{
"frame": 0.45,
"value": {
"__uuid__": "ccccb669-d44d-4a5c-89a1-9aba9476ce12"
}
},
{
"frame": 0.48333333333333334,
"value": {
"__uuid__": "4a45c23d-7bc8-4c5e-b761-ac1100b12a09"
}
},
{
"frame": 0.5166666666666667,
"value": {
"__uuid__": "1c4359c5-b303-403d-82ad-8d5e6ae6ec99"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "ac90c9b8-3b06-4866-89ce-2c953a9d5a9a",
"subMetas": {}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
"_name": "Walking",
"_objFlags": 0,
"_native": "",
"_duration": 1.5166666666666666,
"_duration": 0.6333333333333333,
"sample": 60,
"speed": 1.2,
"wrapMode": 2,
@@ -13,78 +13,78 @@
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "c3b14ecc-a6d9-4cb3-8637-ca7b407a0f5c"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "9435195e-4560-495e-b1ae-083c0c87e8a0"
}
},
{
"frame": 0.18333333333333332,
"frame": 0.06666666666666667,
"value": {
"__uuid__": "ec048360-7a17-4f22-ba52-eb86ec1acae8"
}
},
{
"frame": 0.2833333333333333,
"frame": 0.11666666666666667,
"value": {
"__uuid__": "82bb81e3-667c-4280-8710-211f4904ef2f"
}
},
{
"frame": 0.4,
"frame": 0.16666666666666666,
"value": {
"__uuid__": "c3b14ecc-a6d9-4cb3-8637-ca7b407a0f5c"
}
},
{
"frame": 0.21666666666666667,
"value": {
"__uuid__": "f958fb7f-ef5a-4918-81f3-564004572f45"
}
},
{
"frame": 0.5333333333333333,
"frame": 0.26666666666666666,
"value": {
"__uuid__": "8a0ecf92-db26-4206-9a80-20e749055def"
}
},
{
"frame": 0.65,
"frame": 0.31666666666666665,
"value": {
"__uuid__": "942f2e02-a700-4fbf-877e-08c93e4d4010"
}
},
{
"frame": 0.7666666666666667,
"frame": 0.36666666666666664,
"value": {
"__uuid__": "30546064-1a11-499e-8523-a82c83951c73"
}
},
{
"frame": 0.9,
"frame": 0.4166666666666667,
"value": {
"__uuid__": "515bb75f-7a1f-4500-8aa9-c895915ce19f"
}
},
{
"frame": 1.0333333333333334,
"frame": 0.4666666666666667,
"value": {
"__uuid__": "9100da6b-7582-4afb-9698-3d67d3b2012d"
}
},
{
"frame": 1.2166666666666666,
"frame": 0.5166666666666667,
"value": {
"__uuid__": "1257f72d-0cb3-4750-ae70-13c2d8eb2269"
}
},
{
"frame": 1.3833333333333333,
"frame": 0.5666666666666667,
"value": {
"__uuid__": "1d34b6db-27ba-4e26-864d-0f00d501765e"
}
},
{
"frame": 1.5,
"frame": 0.6166666666666667,
"value": {
"__uuid__": "c317a75a-52c0-4c38-9300-a064cbf4efb3"
}

View File

@@ -0,0 +1,61 @@
{
"__type__": "cc.AnimationClip",
"_name": "Dashing",
"_objFlags": 0,
"_native": "",
"_duration": 0.18333333333333332,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "ec69a078-4153-49e1-9450-656942c2a567"
}
},
{
"frame": 0.016666666666666666,
"value": {
"__uuid__": "bbf23710-9dc6-4bbb-9565-df8848819d07"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "cc7b4103-1d6b-44c1-8e0c-ee1c49052837"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "90409bfe-7b6c-4eab-953b-ea630585fad4"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "9614dc2a-9bfe-4b85-9aa6-d7d62feec82b"
}
},
{
"frame": 0.15,
"value": {
"__uuid__": "c326e3c0-140f-457b-a086-fe95c025d576"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "8e2d7c5b-452d-44db-b0b4-8ee03b36e7f2"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "337d57ad-118c-40e2-be90-2aa1505c152b",
"subMetas": {}
}

View File

@@ -0,0 +1,115 @@
{
"__type__": "cc.AnimationClip",
"_name": "Dying",
"_objFlags": 0,
"_native": "",
"_duration": 0.5333333333333333,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "6d1cd049-7a44-4dcb-9018-4f0fbbf3fdf8"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "50245b04-bcb1-4488-951c-49944c1037da"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "7a6721bb-2321-4947-832f-9a317565ea88"
}
},
{
"frame": 0.11666666666666667,
"value": {
"__uuid__": "c3553a29-e04a-42e2-8b46-82aa85706e26"
}
},
{
"frame": 0.15,
"value": {
"__uuid__": "e221838e-740f-45b1-8fd5-80d4ab8563c3"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "37ebbd1d-9a18-4514-8331-1358a59cab83"
}
},
{
"frame": 0.23333333333333334,
"value": {
"__uuid__": "b4a9ee91-4315-4fb9-9900-6d763406c81d"
}
},
{
"frame": 0.26666666666666666,
"value": {
"__uuid__": "e5388e53-5268-4f54-9a93-f6506db5b77b"
}
},
{
"frame": 0.3,
"value": {
"__uuid__": "078814c3-90e2-4b17-a90b-d2046df9a351"
}
},
{
"frame": 0.3333333333333333,
"value": {
"__uuid__": "c605bf48-9cc5-41f1-8ace-a273298f7b21"
}
},
{
"frame": 0.38333333333333336,
"value": {
"__uuid__": "5b5083ca-8fca-4827-9b76-eaa08685b031"
}
},
{
"frame": 0.4166666666666667,
"value": {
"__uuid__": "b454af6f-9e07-4b34-952b-eca69dc13d5e"
}
},
{
"frame": 0.45,
"value": {
"__uuid__": "af921d09-a72e-4b48-8585-ba72377ba410"
}
},
{
"frame": 0.48333333333333334,
"value": {
"__uuid__": "733d339e-ed74-49ab-8955-641d21528fcc"
}
},
{
"frame": 0.5,
"value": {
"__uuid__": "b7335bea-2985-4331-92c2-08c4c2a5ec86"
}
},
{
"frame": 0.5166666666666667,
"value": {
"__uuid__": "0ae606ea-93c0-4815-9e24-62c5fb59decc"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "657d4193-2224-44ea-94f7-0305a9f2b322",
"subMetas": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 314 KiB

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
{
"__type__": "cc.AnimationClip",
"_name": "Atk4",
"_objFlags": 0,
"_native": "",
"_duration": 0.5333333333333333,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "a9a10466-1e80-4fb8-9c32-2019ee2c988d"
}
},
{
"frame": 0.05,
"value": {
"__uuid__": "e0e3907f-520c-4c4c-991a-ec554e24f368"
}
},
{
"frame": 0.1,
"value": {
"__uuid__": "e4bec6fe-db19-4cf6-a8cc-bfcc3e892d5e"
}
},
{
"frame": 0.15,
"value": {
"__uuid__": "c36ceda7-2e5d-42f4-ae7b-02064348a1c2"
}
},
{
"frame": 0.18333333333333332,
"value": {
"__uuid__": "07004da9-abd4-4a05-baee-447235dcdf2d"
}
},
{
"frame": 0.23333333333333334,
"value": {
"__uuid__": "dd047451-9715-4e68-9ae5-4e4556007190"
}
},
{
"frame": 0.2833333333333333,
"value": {
"__uuid__": "7b2acb5e-3ee8-4c26-b950-f201346cefde"
}
},
{
"frame": 0.31666666666666665,
"value": {
"__uuid__": "b378b873-fae7-49dd-8581-15136046e2f1"
}
},
{
"frame": 0.36666666666666664,
"value": {
"__uuid__": "845b1de6-648f-422a-8289-98222175b787"
}
},
{
"frame": 0.4166666666666667,
"value": {
"__uuid__": "df09902a-52d8-4dec-9d05-62d3428c4625"
}
},
{
"frame": 0.4666666666666667,
"value": {
"__uuid__": "da55a31c-ce4a-4003-a119-8c76fd6d1a80"
}
},
{
"frame": 0.5166666666666667,
"value": {
"__uuid__": "bd3f63fb-6d6d-47d2-9d96-2b58292fccfa"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "168df303-4b6a-4376-940c-3d36fa9e98d8",
"subMetas": {}
}

View File

@@ -0,0 +1,55 @@
{
"__type__": "cc.AnimationClip",
"_name": "Dashing",
"_objFlags": 0,
"_native": "",
"_duration": 0.18333333333333332,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "b8c177cf-013e-4936-a031-2d3480cf975b"
}
},
{
"frame": 0.03333333333333333,
"value": {
"__uuid__": "ae2d8041-e7ee-4300-b3d6-3e85b146f33c"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "f0518811-8fc9-4f9c-9ec4-401abdb3917d"
}
},
{
"frame": 0.1,
"value": {
"__uuid__": "3117e445-fe0f-425f-83af-5b719bf8a009"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "f9d00d7d-2143-4893-be61-32cf1490c9f2"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "daff32df-5e22-4d4e-94d2-6e4522a02138"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "d7b6d7c4-d2b5-49c6-bbcb-d8d80f52ae7e",
"subMetas": {}
}

View File

@@ -0,0 +1,61 @@
{
"__type__": "cc.AnimationClip",
"_name": "Dying",
"_objFlags": 0,
"_native": "",
"_duration": 0.45,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "0b31e6af-6d24-4915-b87b-772c6eb10ca7"
}
},
{
"frame": 0.06666666666666667,
"value": {
"__uuid__": "c597fb09-4621-4d1f-abf9-6484405a6330"
}
},
{
"frame": 0.13333333333333333,
"value": {
"__uuid__": "8c8be852-b65d-41d8-800f-04cbb3cad094"
}
},
{
"frame": 0.2,
"value": {
"__uuid__": "f9522b47-812e-4020-845a-5d9f6d9aca90"
}
},
{
"frame": 0.26666666666666666,
"value": {
"__uuid__": "1ff63b81-49d8-4d68-9526-5f0dc4c88ef0"
}
},
{
"frame": 0.3333333333333333,
"value": {
"__uuid__": "4e96b6fd-2cd1-412b-98a8-7f22040af589"
}
},
{
"frame": 0.43333333333333335,
"value": {
"__uuid__": "a827896b-00b5-4385-9648-2c40414b29c3"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "86706adc-e079-4997-883b-3e269d223065",
"subMetas": {}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -0,0 +1,25 @@
{
"__type__": "cc.AnimationClip",
"_name": "OnWall",
"_objFlags": 0,
"_native": "",
"_duration": 0.016666666666666666,
"sample": 60,
"speed": 1,
"wrapMode": 1,
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "d5c1e6b4-1048-43e2-96f9-801dc23cf418"
}
}
]
}
}
},
"events": []
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "57358699-1d1b-44db-898c-df0c3ce9aab0",
"subMetas": {}
}

View File

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

View File

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

View File

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

View File

@@ -9,9 +9,9 @@ message PlayerDownsync {
int32 virtualGridX = 2;
int32 virtualGridY = 3;
int32 dirX = 4;
int32 dirY = 5; // "dirX" and "dirY" determines character facing
int32 dirY = 5;
int32 velX = 6;
int32 velY = 7; // "velX" and "velY" is used to record the accumulated effect by accelerations (including gravity)
int32 velY = 7; // "velX" and "velY" is used to record the accumulated effect by inertia and accelerations (including gravity)
int32 speed = 8; // this is the instantaneous scalar attribute of a character, different from but will be accounted in "velX" and "velY"
int32 battleState = 9;
int32 joinIndex = 10;
@@ -36,6 +36,10 @@ message PlayerDownsync {
int32 onWallNormX = 27;
int32 onWallNormY = 28;
bool capturedByInertia = 29; // like "inAir", its by design a standalone field only inferred by the calc result of "applyInputFrameDownsyncDynamicsOnSingleRenderFrame" instead of "characterState"
int32 revivalVirtualGridX = 30;
int32 revivalVirtualGridY = 31;
string name = 997;
string displayName = 998;
string avatar = 999;
@@ -51,6 +55,7 @@ message InputFrameDecoded {
message InputFrameUpsync {
int32 inputFrameId = 1;
uint64 encoded = 2;
int32 joinIndex = 3;
}
message InputFrameDownsync {
@@ -70,17 +75,9 @@ message WsReq {
int32 joinIndex = 4;
int32 ackingFrameId = 5;
int32 ackingInputFrameId = 6;
repeated InputFrameUpsync inputFrameUpsyncBatch = 7;
HeartbeatUpsync hb = 8;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
int32 authKey = 7;
repeated InputFrameUpsync inputFrameUpsyncBatch = 8;
HeartbeatUpsync hb = 9;
}
message InputsBufferSnapshot {
@@ -88,6 +85,7 @@ message InputsBufferSnapshot {
uint64 unconfirmedMask = 2;
repeated InputFrameDownsync toSendInputFrameDownsyncs = 3;
bool shouldForceResync = 4;
int32 peerJoinIndex = 5;
}
message MeleeBullet {
@@ -170,6 +168,18 @@ message FireballBullet {
int32 speed = 1005;
}
message HolePunchUpsync {
string intAuthToken = 1;
int32 boundRoomId = 2;
int32 authKey = 3;
}
message PeerUdpAddr {
string ip = 1;
int32 port = 2;
int32 authKey = 3;
}
message BattleColliderInfo {
string stageName = 1;
@@ -186,6 +196,8 @@ message BattleColliderInfo {
double spaceOffsetX = 11;
double spaceOffsetY = 12;
int32 collisionMinStep = 13;
int32 boundRoomCapacity = 14;
PeerUdpAddr battleUdpTunnel = 15;
bool frameDataLoggingEnabled = 1024;
}
@@ -202,4 +214,15 @@ message RoomDownsyncFrame {
repeated int32 speciesIdList = 1026;
int32 bulletLocalIdCounter = 1027;
repeated PeerUdpAddr peerUdpAddrList = 1028;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
int32 peerJoinIndex = 7;
}

View File

@@ -25,16 +25,19 @@
},
{
"__id__": 8
},
{
"__id__": 22
}
],
"_active": true,
"_components": [
{
"__id__": 22
"__id__": 29
}
],
"_prefab": {
"__id__": 23
"__id__": 30
},
"_opacity": 255,
"_color": {
@@ -324,8 +327,8 @@
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"g": 0,
"b": 0,
"a": 255
},
"_contentSize": {
@@ -482,6 +485,22 @@
},
{
"__uuid__": "e8247e2a-1b5b-4618-86f8-224b25246b55"
},
{
"__uuid__": "168df303-4b6a-4376-940c-3d36fa9e98d8"
},
null,
{
"__uuid__": "d7b6d7c4-d2b5-49c6-bbcb-d8d80f52ae7e"
},
{
"__uuid__": "57358699-1d1b-44db-898c-df0c3ce9aab0"
},
{
"__uuid__": "6e1139d4-03dd-4bd4-9510-606e94f629fe"
},
{
"__uuid__": "86706adc-e079-4997-883b-3e269d223065"
}
],
"playOnLoad": false,
@@ -646,13 +665,21 @@
{
"__uuid__": "9b500cb0-8048-4715-81db-cc975c914225"
},
null,
{
"__uuid__": "2aef91f9-ef47-4bb4-bf43-5441723aa639"
},
null,
{
"__uuid__": "38b2c892-347b-4009-93f8-65b2ab1614f0"
},
{
"__uuid__": "411f964a-4dd8-424c-b2e2-d92b10474ce2"
},
{
"__uuid__": "e906322d-a08b-4477-a2e9-98acd42fa034"
},
{
"__uuid__": "ac90c9b8-3b06-4866-89ce-2c953a9d5a9a"
}
],
"playOnLoad": false,
@@ -822,6 +849,16 @@
},
{
"__uuid__": "0abbd156-980e-475e-9994-3c958bd913fc"
},
{
"__uuid__": "337d57ad-118c-40e2-be90-2aa1505c152b"
},
null,
{
"__uuid__": "edd23b2f-1caa-4836-88a7-e4af1f26743e"
},
{
"__uuid__": "657d4193-2224-44ea-94f7-0305a9f2b322"
}
],
"playOnLoad": false,
@@ -877,6 +914,244 @@
"fileId": "7aN7Gcc/tBw5EGlTJVBj2+",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "HpBar",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 23
}
],
"_active": true,
"_components": [
{
"__id__": 26
},
{
"__id__": 27
}
],
"_prefab": {
"__id__": 28
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 50,
"height": 8
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
42.256,
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": ""
},
{
"__type__": "cc.Node",
"_name": "bar",
"_objFlags": 0,
"_parent": {
"__id__": 22
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 24
}
],
"_prefab": {
"__id__": 25
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 10,
"g": 252,
"b": 0,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 50,
"height": 8
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
-25,
0,
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": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 23
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "67e68bc9-dad5-4ad9-a2d8-7e03d458e32f"
},
"_type": 1,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "59bff7a2-23e1-4d69-bce7-afb37eae196a"
},
"fileId": "1b5Rz5KABPK5Nv1wogghs6",
"sync": false
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 22
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "88e79fd5-96b4-4a77-a1f4-312467171014"
},
"_type": 1,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.ProgressBar",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 22
},
"_enabled": true,
"_N$totalLength": 50,
"_N$barSprite": {
"__id__": 24
},
"_N$mode": 0,
"_N$progress": 1,
"_N$reverse": false,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "59bff7a2-23e1-4d69-bce7-afb37eae196a"
},
"fileId": "1cDdFO9Z5KYIcRR52ZmtqO",
"sync": false
},
{
"__type__": "b74b05YDqZFRo4OkZRFZX8k",
"_name": "",
@@ -895,6 +1170,9 @@
"coordLabel": {
"__id__": 3
},
"hpBar": {
"__id__": 27
},
"_id": ""
},
{

View File

@@ -8,7 +8,8 @@
"__id__": 1
},
"optimizationPolicy": 0,
"asyncLoadAssets": false
"asyncLoadAssets": false,
"readonly": false
},
{
"__type__": "cc.Node",
@@ -27,20 +28,19 @@
},
{
"__id__": 11
},
{
"__id__": 14
}
],
"_active": true,
"_level": 1,
"_components": [
{
"__id__": 14
},
{
"__id__": 15
"__id__": 17
}
],
"_prefab": {
"__id__": 16
"__id__": 18
},
"_opacity": 255,
"_color": {
@@ -52,25 +52,14 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1024,
"height": 1920
"width": 960,
"height": 640
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
@@ -86,18 +75,29 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "WhiteStars",
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_level": 2,
"_components": [
{
"__id__": 3
@@ -109,37 +109,26 @@
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"r": 0,
"g": 163,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 268,
"height": 112
"width": 1920,
"height": 1280
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
-16,
0,
0,
0,
0,
@@ -150,7 +139,19 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
@@ -160,11 +161,18 @@
"__id__": 2
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "1a2d934e-9d6d-45bf-83c6-564586cc8400"
"__uuid__": "637f31c2-c53e-4dec-ae11-d56c0c6177ad"
},
"_type": 0,
"_sizeMode": 1,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
@@ -174,12 +182,9 @@
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_id": ""
},
{
@@ -190,7 +195,7 @@
"asset": {
"__uuid__": "230eeb1f-e0f9-4a41-ab6c-05b3771cbf3e"
},
"fileId": "50Mjaee6xFXLrZ/mSBD3P5",
"fileId": "83iQr+5XNNF5E2qjV+WUp0",
"sync": false
},
{
@@ -202,7 +207,6 @@
},
"_children": [],
"_active": true,
"_level": 3,
"_components": [
{
"__id__": 6
@@ -229,17 +233,6 @@
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
@@ -255,7 +248,19 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
@@ -265,6 +270,13 @@
"__id__": 5
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "75a2c1e3-2c22-480c-9572-eb65f4a554e1"
},
@@ -279,12 +291,9 @@
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_id": ""
},
{
@@ -307,7 +316,6 @@
},
"_children": [],
"_active": true,
"_level": 2,
"_components": [
{
"__id__": 9
@@ -327,24 +335,13 @@
"_contentSize": {
"__type__": "cc.Size",
"width": 111.23,
"height": 200
"height": 252
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
@@ -360,7 +357,19 @@
0.66667,
0.66667
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Label",
@@ -370,6 +379,11 @@
"__id__": 8
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "3",
"_N$string": "3",
@@ -407,7 +421,6 @@
},
"_children": [],
"_active": true,
"_level": 2,
"_components": [
{
"__id__": 12
@@ -434,17 +447,6 @@
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
@@ -460,7 +462,19 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
@@ -470,6 +484,13 @@
"__id__": 11
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "637f31c2-c53e-4dec-ae11-d56c0c6177ad"
},
@@ -484,12 +505,9 @@
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_id": ""
},
{
@@ -503,6 +521,115 @@
"fileId": "21dxpL7zlKIIDhUt+GIMbg",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "WhiteStars",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
}
],
"_prefab": {
"__id__": 16
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 268,
"height": 112
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
-16,
0,
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": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 14
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "1a2d934e-9d6d-45bf-83c6-564586cc8400"
},
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "230eeb1f-e0f9-4a41-ab6c-05b3771cbf3e"
},
"fileId": "50Mjaee6xFXLrZ/mSBD3P5",
"sync": false
},
{
"__type__": "6a3d6Y6Ki1BiqAVSKIRdwRl",
"_name": "",
@@ -516,34 +643,6 @@
},
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"_spriteFrame": {
"__uuid__": "334d4f93-b007-49e8-9268-35891d4f4ebb"
},
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": null,
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"ver": "1.2.5",
"uuid": "3ed4c7bc-79d0-4075-a563-d5a58ae798f9",
"uuid": "dc804c5c-ff76-445e-ac69-52269055c3c5",
"optimizationPolicy": "AUTO",
"asyncLoadAssets": false,
"readonly": false,

View File

@@ -24,11 +24,11 @@
"_active": true,
"_components": [
{
"__id__": 12
"__id__": 20
}
],
"_prefab": {
"__id__": 13
"__id__": 21
},
"_opacity": 255,
"_color": {
@@ -90,12 +90,18 @@
},
{
"__id__": 7
},
{
"__id__": 11
},
{
"__id__": 15
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 11
"__id__": 19
},
"_opacity": 255,
"_color": {
@@ -211,38 +217,6 @@
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": null,
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "6dcd5722-8ef9-47fd-9520-861d2713e274"
},
"_id": ""
},
{
"__type__": "cc.Animation",
"_name": "",
@@ -265,6 +239,34 @@
"playOnLoad": false,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"_materials": [],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": null,
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "6dcd5722-8ef9-47fd-9520-861d2713e274"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -278,7 +280,7 @@
},
{
"__type__": "cc.Node",
"_name": "MeleeExplosion",
"_name": "Fireball2",
"_objFlags": 0,
"_parent": {
"__id__": 2
@@ -314,6 +316,258 @@
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
0.5,
0.5,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 7
},
"_enabled": true,
"_defaultClip": null,
"_clips": [
{
"__uuid__": "d2c65ac4-a5b3-411e-8d2d-18d3980649d7"
},
{
"__uuid__": "14b92f5c-af81-416a-a162-e5822d20fe68"
}
],
"playOnLoad": false,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 7
},
"_enabled": true,
"_materials": [],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": null,
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "725c90f9-56f8-48ea-9159-4d2949cd3ce0"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "d92d4831-cd65-4eb5-90bd-b77021aec35b"
},
"fileId": "b0ZpleOHlFqIjwc8HDI9df",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "Fireball3",
"_objFlags": 0,
"_parent": {
"__id__": 2
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 12
},
{
"__id__": 13
}
],
"_prefab": {
"__id__": 14
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 0,
"height": 0
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
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": ""
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 11
},
"_enabled": true,
"_defaultClip": null,
"_clips": [
{
"__uuid__": "6aef5812-d16c-4da1-96a3-a38ac227c823"
},
{
"__uuid__": "0dbb90ed-a08a-448c-b06e-4831260e9213"
}
],
"playOnLoad": false,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 11
},
"_enabled": true,
"_materials": [],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": null,
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "579bc0c1-f5e2-4a5d-889b-9d567e53b0e6"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "d92d4831-cd65-4eb5-90bd-b77021aec35b"
},
"fileId": "03W6UmKHVAz4hCpMvTCpP9",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "MeleeExplosion",
"_objFlags": 0,
"_parent": {
"__id__": 2
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 16
},
{
"__id__": 17
}
],
"_prefab": {
"__id__": 18
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 0,
"height": 0
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
@@ -348,7 +602,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 7
"__id__": 15
},
"_enabled": true,
"_defaultClip": null,
@@ -371,7 +625,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 7
"__id__": 15
},
"_enabled": true,
"_materials": [],

View File

@@ -21,20 +21,20 @@
"__id__": 2
},
{
"__id__": 10
"__id__": 5
}
],
"_active": true,
"_components": [
{
"__id__": 13
"__id__": 12
},
{
"__id__": 14
"__id__": 13
}
],
"_prefab": {
"__id__": 15
"__id__": 14
},
"_opacity": 255,
"_color": {
@@ -46,8 +46,72 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1024,
"height": 1920
"width": 960,
"height": 640
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
480,
320,
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": ""
},
{
"__type__": "cc.Node",
"_name": "Background",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
}
],
"_prefab": {
"__id__": 4
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1920,
"height": 1280
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -83,6 +147,51 @@
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "637f31c2-c53e-4dec-ae11-d56c0c6177ad"
},
"_type": 0,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "32b8e752-8362-4783-a4a6-1160af8b7109"
},
"fileId": "a7oR1cZvVO/pp9QJgLnJyt",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "modeButton",
@@ -92,20 +201,20 @@
},
"_children": [
{
"__id__": 3
"__id__": 6
}
],
"_active": true,
"_components": [
{
"__id__": 7
"__id__": 9
},
{
"__id__": 8
"__id__": 10
}
],
"_prefab": {
"__id__": 9
"__id__": 11
},
"_opacity": 255,
"_color": {
@@ -117,8 +226,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 280,
"height": 130
"width": 240,
"height": 100
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -130,7 +239,7 @@
"ctor": "Float64Array",
"array": [
0,
-564,
-90.495,
0,
0,
0,
@@ -159,20 +268,17 @@
"_name": "Label",
"_objFlags": 0,
"_parent": {
"__id__": 2
"__id__": 5
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
},
{
"__id__": 5
"__id__": 7
}
],
"_prefab": {
"__id__": 6
"__id__": 8
},
"_opacity": 255,
"_color": {
@@ -226,7 +332,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
"__id__": 6
},
"_enabled": true,
"_materials": [
@@ -235,8 +341,8 @@
}
],
"_useOriginalSize": false,
"_string": "gameRule.mode",
"_N$string": "gameRule.mode",
"_string": "1 v 1",
"_N$string": "1 v 1",
"_fontSize": 55,
"_lineHeight": 50,
"_enableWrapText": false,
@@ -251,17 +357,6 @@
"_N$cacheMode": 0,
"_id": ""
},
{
"__type__": "744dcs4DCdNprNhG0xwq6FK",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"_dataID": "gameRule.mode",
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -278,7 +373,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
"__id__": 5
},
"_enabled": true,
"_materials": [
@@ -312,7 +407,7 @@
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
"__id__": 5
},
"_enabled": true,
"_normalMaterial": null,
@@ -375,7 +470,7 @@
"hoverSprite": null,
"_N$disabledSprite": null,
"_N$target": {
"__id__": 2
"__id__": 5
},
"_id": ""
},
@@ -390,115 +485,6 @@
"fileId": "c54lqSflFD8ogSYAhsAkKh",
"sync": false
},
{
"__type__": "cc.Node",
"_name": "decoration",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 11
}
],
"_prefab": {
"__id__": 12
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 543,
"height": 117
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
-312,
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": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 10
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "153d890a-fc37-4d59-8779-93a8fb19fa85"
},
"_type": 0,
"_sizeMode": 1,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": {
"__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4"
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "32b8e752-8362-4783-a4a6-1160af8b7109"
},
"fileId": "1bbMLAzntHZpEudL8lM/Lx",
"sync": false
},
{
"__type__": "dd92bKVy8FJY7uq3ieoNZCZ",
"_name": "",
@@ -508,41 +494,36 @@
},
"_enabled": true,
"modeButton": {
"__id__": 8
"__id__": 10
},
"mapNode": null,
"_id": ""
},
{
"__type__": "cc.Sprite",
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": {
"__uuid__": "7838f276-ab48-445a-b858-937dd27d9520"
},
"_type": 0,
"_sizeMode": 0,
"_fillType": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_atlas": null,
"alignMode": 1,
"_target": null,
"_alignFlags": 18,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_verticalCenter": 0,
"_horizontalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_id": ""
},
{

View File

@@ -8,7 +8,8 @@
"__id__": 1
},
"optimizationPolicy": 0,
"asyncLoadAssets": false
"asyncLoadAssets": false,
"readonly": false
},
{
"__type__": "cc.Node",
@@ -20,18 +21,17 @@
"__id__": 2
},
{
"__id__": 6
"__id__": 5
}
],
"_active": true,
"_level": 1,
"_components": [
{
"__id__": 9
"__id__": 8
}
],
"_prefab": {
"__id__": 10
"__id__": 9
},
"_opacity": 255,
"_color": {
@@ -51,24 +51,12 @@
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"_zIndex": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
512,
0,
480,
480,
0,
0,
0,
@@ -78,7 +66,19 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
@@ -89,17 +89,13 @@
},
"_children": [],
"_active": true,
"_level": 0,
"_components": [
{
"__id__": 3
},
{
"__id__": 4
}
],
"_prefab": {
"__id__": 5
"__id__": 4
},
"_opacity": 255,
"_color": {
@@ -111,32 +107,20 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 303.5,
"height": 30
"width": 805.7,
"height": 35.28
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"_zIndex": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
210,
-150,
0,
0,
0,
@@ -146,7 +130,19 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Label",
@@ -156,32 +152,26 @@
"__id__": 2
},
"_enabled": true,
"_srcBlendFactor": 1,
"_dstBlendFactor": 771,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "login.tips.loginSuccess",
"_N$string": "login.tips.loginSuccess",
"_fontSize": 30,
"_lineHeight": 30,
"_string": "Logged in successfully, loading game resources...",
"_N$string": "Logged in successfully, loading game resources...",
"_fontSize": 28,
"_lineHeight": 28,
"_enableWrapText": true,
"_N$file": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_batchAsBitmap": false,
"_N$horizontalAlign": 1,
"_N$verticalAlign": 1,
"_N$fontFamily": "Arial",
"_N$overflow": 0,
"_id": ""
},
{
"__type__": "744dcs4DCdNprNhG0xwq6FK",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
},
"_enabled": true,
"_dataID": "login.tips.loginSuccess",
"_N$overflow": 3,
"_N$cacheMode": 0,
"_id": ""
},
{
@@ -204,14 +194,13 @@
},
"_children": [],
"_active": true,
"_level": 0,
"_components": [
{
"__id__": 7
"__id__": 6
}
],
"_prefab": {
"__id__": 8
"__id__": 7
},
"_opacity": 255,
"_color": {
@@ -231,24 +220,12 @@
"x": 0.5,
"y": 0.5
},
"_quat": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_skewX": 0,
"_skewY": 0,
"_zIndex": 0,
"groupIndex": 0,
"_id": "",
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
333,
0,
0,
0,
0,
@@ -258,16 +235,29 @@
1,
1
]
}
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 6
"__id__": 5
},
"_enabled": true,
"_materials": [],
"_srcBlendFactor": 770,
"_dstBlendFactor": 771,
"_spriteFrame": null,
@@ -282,7 +272,6 @@
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_state": 0,
"_atlas": null,
"_id": ""
},
@@ -307,7 +296,7 @@
"_enabled": true,
"alignMode": 0,
"_target": null,
"_alignFlags": 20,
"_alignFlags": 18,
"_left": 0,
"_right": 0,
"_top": 0,

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,34 +0,0 @@
{
"ver": "2.3.3",
"uuid": "825df908-a4cb-449d-9731-8ef53f3fd44f",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"MiniGame_Background": {
"ver": "1.0.4",
"uuid": "7838f276-ab48-445a-b858-937dd27d9520",
"rawTextureUuid": "825df908-a4cb-449d-9731-8ef53f3fd44f",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 750,
"height": 1624,
"rawWidth": 750,
"rawHeight": 1624,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -1,34 +0,0 @@
{
"ver": "2.3.3",
"uuid": "94b8bb09-e8ac-4402-a933-b79f01b5a813",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"MiniGame_Blackboard": {
"ver": "1.0.4",
"uuid": "334d4f93-b007-49e8-9268-35891d4f4ebb",
"rawTextureUuid": "94b8bb09-e8ac-4402-a933-b79f01b5a813",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1024,
"height": 1920,
"rawWidth": 1024,
"rawHeight": 1920,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,8 @@ window.ATK_CHARACTER_STATE = {
Atk5: [14, "Atk5"],
Dashing: [15, "Dashing"],
OnWall: [16, "OnWall"],
TurnAround1: [17, "TurnAround1"],
Dying: [18, "Dying"],
};
window.ATK_CHARACTER_STATE_ARR = [];
@@ -74,10 +76,10 @@ cc.Class({
onLoad() {
BaseCharacter.prototype.onLoad.call(this);
this.effAnimNode = this.animNode.getChildByName(this.speciesName);
this.animComp = this.effAnimNode.getComponent(dragonBones.ArmatureDisplay);
if (!this.animComp) {
this.animComp = this.effAnimNode.getComponent(cc.Animation);
}
//this.animComp = this.effAnimNode.getComponent(dragonBones.ArmatureDisplay);
//if (!this.animComp) {
this.animComp = this.effAnimNode.getComponent(cc.Animation);
//}
this.effAnimNode.active = true;
},
@@ -93,7 +95,7 @@ cc.Class({
} else if (0 < rdfPlayer.DirX) {
this.animNode.scaleX = (+1.0);
}
if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState) {
if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState || ATK_CHARACTER_STATE.TurnAround1[0] == newCharacterState) {
if (0 < rdfPlayer.OnWallNormX) {
this.animNode.scaleX = (-1.0);
} else {
@@ -106,13 +108,13 @@ cc.Class({
let playingAnimName = null;
let underlyingAnimationCtrl = null;
if (this.animComp instanceof dragonBones.ArmatureDisplay) {
underlyingAnimationCtrl = this.animComp._armature.animation; // ALWAYS use the dragonBones api instead of ccc's wrapper!
playingAnimName = underlyingAnimationCtrl.lastAnimationName;
} else {
underlyingAnimationCtrl = this.animComp.currentClip;
playingAnimName = (!underlyingAnimationCtrl ? null : underlyingAnimationCtrl.name);
}
//if (this.animComp instanceof dragonBones.ArmatureDisplay) {
// underlyingAnimationCtrl = this.animComp._armature.animation; // ALWAYS use the dragonBones api instead of ccc's wrapper!
// playingAnimName = underlyingAnimationCtrl.lastAnimationName;
//} else {
underlyingAnimationCtrl = this.animComp.currentClip;
playingAnimName = (!underlyingAnimationCtrl ? null : underlyingAnimationCtrl.name);
//}
// It turns out that "prevRdfPlayer.CharacterState" is not useful in this function :)
if (newAnimName == playingAnimName && window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
@@ -121,11 +123,11 @@ cc.Class({
return;
}
if (this.animComp instanceof dragonBones.ArmatureDisplay) {
this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig);
} else {
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName, chConfig);
}
//if (this.animComp instanceof dragonBones.ArmatureDisplay) {
// this._interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig);
//} else {
this._interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName, chConfig);
//}
},
_interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName, chConfig) {

View File

@@ -8,7 +8,7 @@ cc.Class({
},
speed: {
type: cc.Float,
default: 500
default: 100
},
},
@@ -29,6 +29,15 @@ cc.Class({
if (!selfPlayerRichInfo) return;
const selfPlayerNode = selfPlayerRichInfo.node;
if (!selfPlayerNode) return;
self.mapNode.setPosition(cc.v2().sub(selfPlayerNode.position));
const dst = cc.v2().sub(selfPlayerNode.position);
const pDiff = dst.sub(self.mapNode.position);
const stepLength = dt * self.speed;
if (stepLength > pDiff.mag()) {
self.mapNode.setPosition(dst);
} else {
pDiff.normalizeSelf();
const newMapPos = self.mapNode.position.add(pDiff.mul(dt * self.speed));
self.mapNode.setPosition(newMapPos);
}
}
});

View File

@@ -10,7 +10,11 @@ cc.Class({
coordLabel: {
type: cc.Label,
default: null
}
},
hpBar: {
type: cc.ProgressBar,
default: null
},
},
onLoad() {

View File

@@ -14,10 +14,6 @@ cc.Class({
type: cc.Node,
default: null
},
myAvatarNode: {
type: cc.Node,
default: null
},
exitBtnNode: {
type: cc.Node,
default: null
@@ -25,10 +21,10 @@ cc.Class({
},
// LIFE-CYCLE CALLBACKS:
onLoad() {
},
onLoad() {},
init() {
init(mapIns) {
this.mapIns = mapIns;
if (null != this.firstPlayerInfoNode) {
this.firstPlayerInfoNode.active = false;
}
@@ -58,9 +54,10 @@ cc.Class({
},
exitBtnOnClick(evt) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
window.closeWSConnection();
cc.director.loadScene('login');
this.mapIns.hideFindingPlayersGUI();
cc.log(`FindingPlayers.exitBtnOnClick`);
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
window.clearLocalStorageAndBackToLoginScene(false);
},
updatePlayersInfo(playerMetas) {
@@ -70,29 +67,20 @@ cc.Class({
const playerInfoNode = this.playersInfoNode[playerMeta.joinIndex];
if (null == playerInfoNode) {
cc.error("There's no playerInfoNode for joinIndex == ", joinIndex, ", as `this.playerInfoNode` is currently ", this.playersInfoNode);
}
}
playerInfoNode.active = true;
if (2 == playerMeta.joinIndex) {
if (null != this.findingAnimNode) {
if (null != this.findingAnimNode) {
this.findingAnimNode.active = false;
}
}
}
//显示自己的头像名称以及他人的头像名称
for (let i in playerMetas) {
const playerMeta = playerMetas[i];
console.log("Showing playerMeta:", playerMeta);
const playerInfoNode = this.playersInfoNode[playerMeta.joinIndex];
(() => { //远程加载头像
let remoteUrl = playerMeta.avatar;
if (remoteUrl == null || remoteUrl == '') {
cc.log(`No avatar to show for :`);
cc.log(playerMeta);
}
})();
function isEmptyString(str) {
return str == null || str == ''
}

View File

@@ -68,6 +68,8 @@ cc.Class({
// LIFE-CYCLE CALLBACKS:
onLoad() {
cc.view.setOrientation(cc.macro.ORIENTATION_AUTO);
cc.view.enableAutoFullScreen(true);
window.atFirstLocationHref = window.location.href.split('#')[0];
const self = this;

View File

@@ -2,6 +2,7 @@ const i18n = require('LanguageData');
i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field
const RingBuffer = require('./RingBuffer');
const NetworkDoctor = require('./NetworkDoctor');
const PriorityQueue = require("./PriorityQueue");
window.ALL_MAP_STATES = {
@@ -33,6 +34,23 @@ window.PlayerBattleState = {
EXPELLED_IN_DISMISSAL: 6
};
window.onUdpMessage = (args) => {
const self = window.mapIns;
const ui8Arr = args;
//cc.log(`#1 Js called back by CPP: onUdpMessage: args=${args}, typeof(args)=${typeof (args)}, argslen=${args.length}, ui8Arr=${ui8Arr}`);
const req = window.pb.protos.WsReq.decode(ui8Arr);
if (req) {
//cc.log(`#2 Js called back by CPP for upsync: onUdpMessage: ${JSON.stringify(req)}`);
if (req.act && window.UPSYNC_MSG_ACT_PLAYER_CMD == req.act) {
let effCnt = 0;
const peerJoinIndex = req.joinIndex;
if (peerJoinIndex == self.selfPlayerInfo.JoinIndex) return;
const batch = req.inputFrameUpsyncBatch;
self.onPeerInputFrameUpsync(peerJoinIndex, batch, true);
}
}
};
cc.Class({
extends: cc.Component,
@@ -96,16 +114,26 @@ cc.Class({
type: cc.Integer,
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
},
jigglingEps1D: {
type: cc.Float,
default: 1e-3
sendingQLabel: {
type: cc.Label,
default: null
},
bulletTriggerEnabled: {
default: false
inputFrameDownsyncQLabel: {
type: cc.Label,
default: null
},
closeOnForcedtoResyncNotSelf: {
default: true
peerInputFrameUpsyncQLabel: {
type: cc.Label,
default: null
},
rollbackFramesLabel: {
type: cc.Label,
default: null
},
skippedRenderFrameCntLabel: {
type: cc.Label,
default: null
}
},
_inputFrameIdDebuggable(inputFrameId) {
@@ -116,7 +144,7 @@ cc.Class({
return (confirmedList + 1) == (1 << this.playerRichInfoDict.size);
},
getOrPrefabInputFrameUpsync(inputFrameId) {
getOrPrefabInputFrameUpsync(inputFrameId, canConfirmSelf) {
// TODO: find some kind of synchronization mechanism against "onInputFrameDownsyncBatch"!
const self = this;
if (
@@ -129,34 +157,48 @@ cc.Class({
let previousSelfInput = null,
currSelfInput = null;
const joinIndex = self.selfPlayerInfo.JoinIndex;
const selfJoinIndexMask = (1 << (joinIndex - 1));
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameId);
const previousInputFrameDownsync = self.recentInputCache.GetByFrameId(inputFrameId - 1);
previousSelfInput = (null == previousInputFrameDownsync ? null : previousInputFrameDownsync.InputList[joinIndex - 1]);
if (null != existingInputFrame) {
if (
null != existingInputFrame
&&
(true != canConfirmSelf)
) {
// This could happen upon either [type#1] or [type#2] forceConfirmation, where "refRenderFrame" is accompanied by some "inputFrameDownsyncs". The check here also guarantees that we don't override history
//console.log(`noDelayInputFrameId=${inputFrameId} already exists in recentInputCache: recentInputCache=${self._stringifyRecentInputCache(false)}`);
return [previousSelfInput, existingInputFrame.InputList[joinIndex - 1]];
}
const lastAllConfirmedInputFrame = self.recentInputCache.GetByFrameId(self.lastAllConfirmedInputFrameId);
const prefabbedInputList = new Array(self.playerRichInfoDict.size).fill(0);
// the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "prefabbedInputList"
for (let k in prefabbedInputList) {
if (null != previousInputFrameDownsync) {
for (let k = 0; k < window.boundRoomCapacity; ++k) {
if (null != existingInputFrame) {
// When "null != existingInputFrame", it implies that "true == canConfirmSelf" here, we just have to assign "prefabbedInputList[(joinIndex-1)]" specifically and copy all others
prefabbedInputList[k] = existingInputFrame.InputList[k];
} else if (self.lastIndividuallyConfirmedInputFrameId[k] <= inputFrameId) {
prefabbedInputList[k] = self.lastIndividuallyConfirmedInputList[k];
// Don't predict "btnA & btnB"!
prefabbedInputList[k] = (prefabbedInputList[k] & 15);
} else if (null != previousInputFrameDownsync) {
// When "self.lastIndividuallyConfirmedInputFrameId[k] > inputFrameId", don't use it to predict a historical input!
prefabbedInputList[k] = previousInputFrameDownsync.InputList[k];
// Don't predict "btnA & btnB"!
prefabbedInputList[k] = (prefabbedInputList[k] & 15);
}
if (0 <= self.lastAllConfirmedInputFrameId && inputFrameId - 1 > self.lastAllConfirmedInputFrameId) {
prefabbedInputList[k] = lastAllConfirmedInputFrame.InputList[k];
}
// Don't predict "btnA & btnB"!
prefabbedInputList[k] = (prefabbedInputList[k] & 15);
}
let initConfirmedList = 0;
if (null != existingInputFrame) {
// When "null != existingInputFrame", it implies that "true == canConfirmSelf" here
initConfirmedList = (existingInputFrame.ConfirmedList | selfJoinIndexMask);
}
currSelfInput = self.ctrl.getEncodedInput(); // When "null == existingInputFrame", it'd be safe to say that the realtime "self.ctrl.getEncodedInput()" is for the requested "inputFrameId"
//console.log(`@rdf.Id=${self.renderFrameId}, currSelfInput=${currSelfInput}`);
prefabbedInputList[(joinIndex - 1)] = currSelfInput;
while (self.recentInputCache.EdFrameId <= inputFrameId) {
// Fill the gap
const prefabbedInputFrameDownsync = gopkgs.NewInputFrameDownsync(self.recentInputCache.EdFrameId, prefabbedInputList.slice(), (1 << (joinIndex - 1)));
// [WARNING] Do not blindly use "selfJoinIndexMask" here, as the "actuallyUsedInput for self" couldn't be confirmed while prefabbing, otherwise we'd have confirmed a wrong self input by "_markConfirmationIfApplicable()"!
const prefabbedInputFrameDownsync = gopkgs.NewInputFrameDownsync(self.recentInputCache.EdFrameId, prefabbedInputList.slice(), initConfirmedList);
// console.log(`Prefabbed inputFrameId=${prefabbedInputFrameDownsync.InputFrameId}`);
self.recentInputCache.Put(prefabbedInputFrameDownsync);
}
@@ -185,6 +227,7 @@ cc.Class({
// Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly.
batchInputFrameIdSt = self.recentInputCache.StFrameId;
}
self.networkDoctor.logSending(batchInputFrameIdSt, latestLocalInputFrameId);
for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.GetByFrameId(i);
if (null == inputFrameDownsync) {
@@ -206,7 +249,11 @@ cc.Class({
joinIndex: self.selfPlayerInfo.JoinIndex,
ackingInputFrameId: self.lastAllConfirmedInputFrameId,
inputFrameUpsyncBatch: inputFrameUpsyncBatch,
authKey: self.selfPlayerInfo.udpTunnelAuthKey,
}).finish();
if (cc.sys.isNative) {
DelayNoMore.UdpSession.broadcastInputFrameUpsync(reqData, window.boundRoomCapacity, self.selfPlayerInfo.JoinIndex);
}
window.sendSafely(reqData);
self.lastUpsyncInputFrameId = latestLocalInputFrameId;
if (self.lastUpsyncInputFrameId >= self.recentInputCache.EdFrameId) {
@@ -309,7 +356,7 @@ cc.Class({
const newFireball = newFireballNode.getComponent("Fireball");
newFireballNode.setPosition(cc.v2(Number.MAX_VALUE, Number.MAX_VALUE));
safelyAddChild(self.node, newFireballNode);
setLocalZOrder(newFireballNode, 5);
setLocalZOrder(newFireballNode, 10);
newFireball.lastUsed = -1;
newFireball.bulletLocalId = -1;
const initLookupKey = -(k + 1); // there's definitely no suck "bulletLocalId"
@@ -323,9 +370,10 @@ cc.Class({
self.lastUpsyncInputFrameId = -1;
self.chaserRenderFrameId = -1; // at any moment, "chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch"
self.lastIndividuallyConfirmedInputFrameId = new Array(window.boundRoomCapacity).fill(-1);
self.lastIndividuallyConfirmedInputList = new Array(window.boundRoomCapacity).fill(0);
self.recentRenderCache = new RingBuffer(self.renderCacheSize);
self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others".
self.recentInputCache = gopkgs.NewRingBufferJs((self.renderCacheSize >> 1) + 1);
self.gopkgsCollisionSys = gopkgs.NewCollisionSpaceJs((self.spaceOffsetX << 1), (self.spaceOffsetY << 1), self.collisionMinStep, self.collisionMinStep);
@@ -343,14 +391,15 @@ cc.Class({
self.othersForcedDownsyncRenderFrameDict = new Map();
self.rdfIdToActuallyUsedInput = new Map();
self.networkDoctor = new NetworkDoctor(20);
self.skipRenderFrameFlag = false;
self.allowRollbackOnPeerUpsync = true;
self.countdownNanos = null;
if (self.countdownLabel) {
self.countdownLabel.string = "";
}
if (self.findingPlayerNode) {
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.init();
}
if (self.playersInfoNode) {
safelyAddChild(self.widgetsAboveAllNode, self.playersInfoNode);
}
@@ -410,11 +459,16 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
const self = this;
window.mapIns = self;
window.forceBigEndianFloatingNumDecoding = self.forceBigEndianFloatingNumDecoding;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
console.warn("+++++++ Map onLoad()");
@@ -452,7 +506,7 @@ cc.Class({
self.findingPlayerNode.width = self.canvasNode.width;
self.findingPlayerNode.height = self.canvasNode.height;
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.init();
findingPlayerScriptIns.init(self);
self.playersInfoNode = cc.instantiate(self.playersInfoPrefab);
@@ -474,7 +528,7 @@ cc.Class({
console.log(`Received parsedBattleColliderInfo via ws`);
// TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE".
Object.assign(self, parsedBattleColliderInfo);
self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis;
self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance;
const tiledMapIns = self.node.getComponent(cc.TiledMap);
@@ -482,7 +536,7 @@ cc.Class({
const fullPathOfTmxFile = cc.js.formatStr("map/%s/map", parsedBattleColliderInfo.stageName);
cc.loader.loadRes(fullPathOfTmxFile, cc.TiledMapAsset, (err, tmxAsset) => {
if (null != err) {
console.error(err);
console.error(`Error occurred when loading tiled stage ${parsedBattleColliderInfo.stageName}`, err);
return;
}
@@ -531,10 +585,6 @@ cc.Class({
const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter);
self.gopkgsCollisionSysMap[collisionBarrierIndex] = newBarrierCollider;
}
self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
Object.assign(self.selfPlayerInfo, {
Id: self.selfPlayerInfo.playerId
});
self.initDebugDrawers();
const reqData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
@@ -608,7 +658,7 @@ cc.Class({
const jsPlayersArr = new Array(pbRdf.playersArr.length).fill(null);
for (let k = 0; k < pbRdf.playersArr.length; ++k) {
const pbPlayer = pbRdf.playersArr[k];
const jsPlayer = gopkgs.NewPlayerDownsyncJs(pbPlayer.id, pbPlayer.virtualGridX, pbPlayer.virtualGridY, pbPlayer.dirX, pbPlayer.dirY, pbPlayer.velX, pbPlayer.velY, pbPlayer.framesToRecover, pbPlayer.framesInChState, pbPlayer.activeSkillId, pbPlayer.activeSkillHit, pbPlayer.framesInvinsible, pbPlayer.speed, pbPlayer.battleState, pbPlayer.characterState, pbPlayer.joinIndex, pbPlayer.hp, pbPlayer.maxHp, pbPlayer.colliderRadius, pbPlayer.inAir, pbPlayer.onWall, pbPlayer.onWallNormX, pbPlayer.onWallNormY, pbPlayer.bulletTeamId, pbPlayer.chCollisionTeamId);
const jsPlayer = gopkgs.NewPlayerDownsyncJs(pbPlayer.id, pbPlayer.virtualGridX, pbPlayer.virtualGridY, pbPlayer.dirX, pbPlayer.dirY, pbPlayer.velX, pbPlayer.velY, pbPlayer.framesToRecover, pbPlayer.framesInChState, pbPlayer.activeSkillId, pbPlayer.activeSkillHit, pbPlayer.framesInvinsible, pbPlayer.speed, pbPlayer.battleState, pbPlayer.characterState, pbPlayer.joinIndex, pbPlayer.hp, pbPlayer.maxHp, pbPlayer.colliderRadius, pbPlayer.inAir, pbPlayer.onWall, pbPlayer.onWallNormX, pbPlayer.onWallNormY, pbPlayer.capturedByInertia, pbPlayer.bulletTeamId, pbPlayer.chCollisionTeamId, pbPlayer.revivalVirtualGridX, pbPlayer.revivalVirtualGridY);
jsPlayersArr[k] = jsPlayer;
}
const jsMeleeBulletsArr = new Array(pbRdf.meleeBullets.length).fill(null);
@@ -685,20 +735,27 @@ cc.Class({
self.hideFindingPlayersGUI();
console.warn('On battle resynced! renderFrameId=', rdf.Id);
}
self.renderFrameId = rdf.Id;
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId".
self.chaserRenderFrameId = rdf.Id;
self.networkDoctor.logRollbackFrames(0);
const canvasNode = self.canvasNode;
self.ctrl = canvasNode.getComponent("TouchEventsManager");
self.enableInputControls();
self.transitToState(ALL_MAP_STATES.VISUAL);
const selfPlayerRichInfo = self.playerRichInfoDict.get(self.selfPlayerInfo.Id);
const newMapPos = cc.v2().sub(selfPlayerRichInfo.node.position);
self.node.setPosition(newMapPos);
self.battleState = ALL_BATTLE_STATES.IN_BATTLE;
}
// [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!
if (self.countdownToBeginGameNode && self.countdownToBeginGameNode.parent) {
self.countdownToBeginGameNode.parent.removeChild(self.countdownToBeginGameNode);
self.countdownToBeginGameNode.setPosition(cc.v2(Number.MAX_VALUE, Number.MAX_VALUE));
}
if (null != self.musicEffectManagerScriptIns) {
@@ -782,6 +839,19 @@ cc.Class({
return true;
},
_markConfirmationIfApplicable() {
const self = this;
let newAllConfirmedCnt = 0;
while (self.recentInputCache.StFrameId <= self.lastAllConfirmedInputFrameId && self.lastAllConfirmedInputFrameId < self.recentInputCache.EdFrameId) {
const inputFrameDownsync = self.recentInputCache.GetByFrameId(self.lastAllConfirmedInputFrameId);
if (null == inputFrameDownsync) break;
if (self._allConfirmed(inputFrameDownsync.ConfirmedList)) break;
++self.lastAllConfirmedInputFrameId;
++newAllConfirmedCnt;
}
return newAllConfirmedCnt;
},
onInputFrameDownsyncBatch(batch /* []*pb.InputFrameDownsync */ ) {
// TODO: find some kind of synchronization mechanism against "getOrPrefabInputFrameUpsync"!
if (null == batch) {
@@ -795,6 +865,7 @@ cc.Class({
return;
}
self.networkDoctor.logInputFrameDownsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
let firstPredictedYetIncorrectInputFrameId = null;
for (let k in batch) {
const inputFrameDownsync = batch[k];
@@ -802,8 +873,9 @@ cc.Class({
if (inputFrameDownsyncId <= self.lastAllConfirmedInputFrameId) {
continue;
}
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
// [WARNING] Now that "inputFrameDownsyncId > self.lastAllConfirmedInputFrameId", we should make an update immediately because unlike its backend counterpart "Room.LastAllConfirmedInputFrameId", the frontend "mapIns.lastAllConfirmedInputFrameId" might inevitably get gaps among discrete values due to "either type#1 or type#2 forceConfirmation" -- and only "onInputFrameDownsyncBatch" can catch this!
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
const localInputFrame = self.recentInputCache.GetByFrameId(inputFrameDownsyncId);
if (null != localInputFrame
&&
@@ -813,16 +885,29 @@ cc.Class({
) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1;
const inputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameDownsync.inputFrameId, inputFrameDownsync.inputList, inputFrameDownsync.confirmedList); // "battle.InputFrameDownsync" in "jsexport"
for (let j in self.playerRichInfoArr) {
const jj = parseInt(j);
if (inputFrameDownsync.inputFrameId > self.lastIndividuallyConfirmedInputFrameId[jj]) {
self.lastIndividuallyConfirmedInputFrameId[jj] = inputFrameDownsync.inputFrameId;
self.lastIndividuallyConfirmedInputList[jj] = inputFrameDownsync.inputList[jj];
}
}
//console.log(`Confirmed inputFrameId=${inputFrameDownsync.inputFrameId}`);
const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.SetByFrameId(inputFrameDownsyncLocal, inputFrameDownsync.inputFrameId);
if (window.RING_BUFF_FAILED_TO_SET == ret) {
throw `Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`;
}
}
self._markConfirmationIfApplicable();
self._handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, false);
},
_handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP) {
if (null == firstPredictedYetIncorrectInputFrameId) return;
const self = this;
const renderFrameId1 = gopkgs.ConvertToFirstUsedRenderFrameId(firstPredictedYetIncorrectInputFrameId) - 1;
if (renderFrameId1 >= self.chaserRenderFrameId) return;
@@ -838,11 +923,88 @@ cc.Class({
--------------------------------------------------------
*/
// The actual rollback-and-chase would later be executed in update(dt).
console.warn(`Mismatched input detected, resetting chaserRenderFrameId: ${self.chaserRenderFrameId}->${renderFrameId1} by firstPredictedYetIncorrectInputFrameId: ${firstPredictedYetIncorrectInputFrameId}
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}]`);
batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}]
fromUDP=${fromUDP}`);
self.chaserRenderFrameId = renderFrameId1;
let rollbackFrames = (self.renderFrameId - self.chaserRenderFrameId);
if (0 > rollbackFrames) {
rollbackFrames = 0;
}
self.networkDoctor.logRollbackFrames(rollbackFrames);
},
onPeerInputFrameUpsync(peerJoinIndex, batch, fromUDP) {
// TODO: find some kind of synchronization mechanism against "getOrPrefabInputFrameUpsync"!
// See `<proj-root>/ConcerningEdgeCases.md` for why this method exists.
if (null == batch) {
return;
}
const self = this;
if (!self.recentInputCache) {
return;
}
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState) {
return;
}
let effCnt = 0;
//console.log(`Received peer inputFrameUpsync batch w/ inputFrameId in [${batch[0].inputFrameId}, ${batch[batch.length - 1].inputFrameId}] for prediction assistance`);
let firstPredictedYetIncorrectInputFrameId = null;
const renderedInputFrameIdUpper = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
for (let k in batch) {
const inputFrame = batch[k]; // could be either "pb.InputFrameDownsync" or "pb.InputFrameUpsync", depending on "fromUDP"
const inputFrameId = inputFrame.inputFrameId;
const peerEncodedInput = (true == fromUDP ? inputFrame.encoded : inputFrame.inputList[peerJoinIndex - 1]);
if (false == self.allowRollbackOnPeerUpsync && inputFrameId <= renderedInputFrameIdUpper) {
// [WARNING] Avoid obfuscating already rendered history, even at "inputFrameId == renderedInputFrameIdUpper", due to the use of "INPUT_SCALE_FRAMES" some previous render frames might already be rendered with "inputFrameId"!
continue;
}
if (inputFrameId <= self.lastAllConfirmedInputFrameId) {
// [WARNING] Don't reject it by "inputFrameId <= self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex-1]", the arrival of UDP packets might not reserve their sending order!
continue;
}
const peerJoinIndexMask = (1 << (peerJoinIndex - 1));
self.getOrPrefabInputFrameUpsync(inputFrameId, false); // Make sure that inputFrame exists locally
const existingInputFrame = self.recentInputCache.GetByFrameId(inputFrameId);
if (0 < (existingInputFrame.ConfirmedList & peerJoinIndexMask)) {
continue;
}
if (inputFrameId > self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1]) {
self.lastIndividuallyConfirmedInputFrameId[peerJoinIndex - 1] = inputFrameId;
self.lastIndividuallyConfirmedInputList[peerJoinIndex - 1] = peerEncodedInput;
}
effCnt += 1;
// the returned "gopkgs.NewInputFrameDownsync.InputList" is immutable, thus we can only modify the values in "newInputList" and "newConfirmedList"!
let newInputList = existingInputFrame.InputList.slice();
newInputList[peerJoinIndex - 1] = peerEncodedInput;
let newConfirmedList = (existingInputFrame.ConfirmedList | peerJoinIndex);
const newInputFrameDownsyncLocal = gopkgs.NewInputFrameDownsync(inputFrameId, newInputList, newConfirmedList);
//console.log(`Updated encoded input of peerJoinIndex=${peerJoinIndex} to ${peerEncodedInput} for inputFrameId=${inputFrameId}/renderedInputFrameIdUpper=${renderedInputFrameIdUpper} from ${JSON.stringify(inputFrame)}; newInputFrameDownsyncLocal=${self.gopkgsInputFrameDownsyncStr(newInputFrameDownsyncLocal)}; existingInputFrame=${self.gopkgsInputFrameDownsyncStr(existingInputFrame)}`);
self.recentInputCache.SetByFrameId(newInputFrameDownsyncLocal, inputFrameId);
if (self.allowRollbackOnPeerUpsync) {
// Reaching here implies that "true == self.allowRollbackOnPeerUpsync".
// Shall we update the "chaserRenderFrameId" if the rendered history was wrong? It doesn't seem to impact eventual correctness if we allow the update of "chaserRenderFrameId" upon "inputFrameId <= renderedInputFrameIdUpper" here, however UDP upsync doesn't reserve order from a same sender and there might be multiple other senders, hence it might result in unnecessarily frequent chasing.
if (
null == firstPredictedYetIncorrectInputFrameId
&&
existingInputFrame.InputList[peerJoinIndex - 1] != peerEncodedInput
) {
firstPredictedYetIncorrectInputFrameId = inputFrameId;
}
}
}
if (0 < effCnt) {
//self._markConfirmationIfApplicable();
self.networkDoctor.logPeerInputFrameUpsync(batch[0].inputFrameId, batch[batch.length - 1].inputFrameId);
}
if (true == self.allowRollbackOnPeerUpsync) {
self._handleIncorrectlyRenderedPrediction(firstPredictedYetIncorrectInputFrameId, batch, fromUDP);
}
},
onPlayerAdded(rdf /* pb.RoomDownsyncFrame */ ) {
@@ -860,8 +1022,7 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState) {
return;
}
self._stringifyRdfIdToActuallyUsedInput();
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED);
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
self.battleState = ALL_BATTLE_STATES.IN_SETTLEMENT;
self.countdownNanos = null;
if (self.musicEffectManagerScriptIns) {
@@ -909,19 +1070,34 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
update(dt) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) {
const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt;
if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) {
// [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience!
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
/*
[WARNING] Different devices might differ in the rate of calling "update(dt)", and the game engine is responsible of keeping this rate statistically constant.
Significantly different rates of calling "update(dt)" among players in a same battle would result in frequent [type#1 forceConfirmation], if you have any doubt on troubles caused by this, sample the FPS curve from all players in that battle.
Kindly note that Significantly different network bandwidths or delay fluctuations would result in frequent [type#1 forceConfirmation] too, but CAUSE FROM DIFFERENT LOCAL "update(dt)" RATE SHOULD BE THE FIRST TO INVESTIGATE AND ELIMINATE -- because we have control on it, but no one has control on the internet.
*/
if (self.skipRenderFrameFlag) {
self.networkDoctor.logSkippedRenderFrameCnt();
self.skipRenderFrameFlag = false;
return;
}
try {
let st = performance.now();
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);
[prevSelfInput, currSelfInput] = self.getOrPrefabInputFrameUpsync(noDelayInputFrameId, true);
}
const delayedInputFrameId = gopkgs.ConvertToDelayedInputFrameId(self.renderFrameId);
if (null == self.recentInputCache.GetByFrameId(delayedInputFrameId)) {
// Possible edge case after resync, kindly note that it's OK to prefab a "future inputFrame" here, because "sendInputFrameUpsyncBatch" would be capped by "noDelayInputFrameId from self.renderFrameId".
self.getOrPrefabInputFrameUpsync(delayedInputFrameId, false);
}
let t0 = performance.now();
@@ -945,6 +1121,12 @@ batchInputFrameIdRange=[${batch[0].inputFrameId}, ${batch[batch.length - 1].inpu
// Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const latestRdfResults = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.gopkgsCollisionSys, self.gopkgsCollisionSysMap, false);
let rollbackFrames = (self.renderFrameId - self.chaserRenderFrameId);
if (0 > rollbackFrames) {
rollbackFrames = 0;
}
self.networkDoctor.logRollbackFrames(rollbackFrames);
let prevRdf = latestRdfResults[0],
rdf = latestRdfResults[1];
/*
@@ -969,9 +1151,13 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
}
self.applyRoomDownsyncFrameDynamics(rdf, prevRdf);
self.showDebugBoundaries(rdf);
if (self.showNetworkDoctorInfo) {
self.showNetworkDoctorLabels();
}
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
let t3 = performance.now();
self.skipRenderFrameFlag = self.networkDoctor.isTooFast(self);
} catch (err) {
console.error("Error during Map.update", err);
self.onBattleStopped(); // TODO: Popup to ask player to refresh browser
@@ -995,6 +1181,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
logout(byClick /* The case where this param is "true" will be triggered within `ConfirmLogout.js`.*/ , shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
const self = this;
const localClearance = () => {
window.closeWSConnection(constants.RET_CODE.BATTLE_STOPPED, "");
window.clearLocalStorageAndBackToLoginScene(shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage);
}
@@ -1061,8 +1248,10 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
hideFindingPlayersGUI(rdf) {
const self = this;
if (null == self.findingPlayerNode.parent) return;
self.findingPlayerNode.parent.removeChild(self.findingPlayerNode);
// [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!
if (self.findingPlayerNode && self.findingPlayerNode.parent) {
self.findingPlayerNode.setPosition(cc.v2(Number.MAX_VALUE, Number.MAX_VALUE));
}
},
onBattleReadyToStart(rdf /* pb.RoomDownsyncFrame */ ) {
@@ -1106,6 +1295,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
playerRichInfo.node.setPosition(wx, wy);
playerRichInfo.scriptIns.updateSpeed(currPlayerDownsync.Speed);
playerRichInfo.scriptIns.updateCharacterAnim(currPlayerDownsync, prevRdfPlayer, false, chConfig);
playerRichInfo.scriptIns.hpBar.progress = (currPlayerDownsync.Hp * 1.0) / currPlayerDownsync.MaxHp;
}
// Move all to infinitely far away first
@@ -1196,7 +1386,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
throw `Couldn't find renderFrame for i=${i} to rollback (are you using Firefox?), self.renderFrameId=${self.renderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, might've been interruptted by onRoomDownsyncFrame`;
}
const j = gopkgs.ConvertToDelayedInputFrameId(i);
const delayedInputFrame = self.getOrPrefabInputFrameUpsync(j);
const delayedInputFrame = self.recentInputCache.GetByFrameId(j);
if (self.frameDataLoggingEnabled) {
const actuallyUsedInputClone = delayedInputFrame.InputList.slice();
@@ -1242,7 +1432,7 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
const selfPlayerId = self.selfPlayerInfo.Id;
if (selfPlayerId == playerId) {
self.selfPlayerInfo.JoinIndex = immediatePlayerInfo.JoinIndex;
self.selfPlayerInfo.JoinIndex = immediatePlayerInfo.JoinIndex; // Update here in case of any change during WAITING phase
nodeAndScriptIns[1].showArrowTipNode();
}
}
@@ -1319,6 +1509,21 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
return s.join(',');
},
gopkgsInputFrameDownsyncStr(inputFrameDownsync) {
if (null == inputFrameDownsync) return "{}";
const self = this;
let s = [];
s.push(`InputFrameId:${inputFrameDownsync.InputFrameId}`);
let ss = [];
for (let k = 0; k < window.boundRoomCapacity; ++k) {
ss.push(`"${inputFrameDownsync.InputList[k]}"`);
}
s.push(`InputList:[${ss.join(',')}]`);
s.push(`ConfirmedList:${inputFrameDownsync.ConfirmedList}`);
return `{${s.join(',')}}`;
},
_stringifyRdfIdToActuallyUsedInput() {
const self = this;
let s = [];
@@ -1445,4 +1650,44 @@ actuallyUsedinputList:{${self.inputFrameDownsyncStr(actuallyUsedInputClone)}}`);
}
}
},
showNetworkDoctorLabels() {
const self = this;
const [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = self.networkDoctor.stats();
if (self.sendingQLabel) {
self.sendingQLabel.string = `${sendingFps} fps sending`;
if (sendingFps < self.networkDoctor.inputRateThreshold) {
self.sendingQLabel.node.color = cc.Color.RED;
} else {
self.sendingQLabel.node.color = cc.Color.WHITE;
}
}
if (self.inputFrameDownsyncQLabel) {
self.inputFrameDownsyncQLabel.string = `${srvDownsyncFps} fps srv-downsync`;
if (srvDownsyncFps < self.networkDoctor.inputRateThreshold) {
self.inputFrameDownsyncQLabel.node.color = cc.Color.RED;
} else {
self.inputFrameDownsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.peerInputFrameUpsyncQLabel) {
self.peerInputFrameUpsyncQLabel.string = `${peerUpsyncFps} fps peer-upsync`;
if (peerUpsyncFps > self.networkDoctor.peerUpsyncFps) {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.RED;
} else {
self.peerInputFrameUpsyncQLabel.node.color = cc.Color.WHITE;
}
}
if (self.rollbackFramesLabel) {
self.rollbackFramesLabel.string = `rollbackFrames: ${rollbackFrames}`
if (rollbackFrames > self.networkDoctor.rollbackFramesThreshold) {
self.rollbackFramesLabel.node.color = cc.Color.RED;
} else {
self.rollbackFramesLabel.node.color = cc.Color.WHITE;
}
}
if (self.skippedRenderFrameCntLabel) {
self.skippedRenderFrameCntLabel.string = `${skippedRenderFrameCnt} frames skipped`
}
},
});

View File

@@ -0,0 +1,110 @@
const RingBuffer = require('./RingBuffer');
var NetworkDoctor = function(capacity) {
this.reset(capacity);
};
NetworkDoctor.prototype.reset = function(capacity) {
this.sendingQ = new RingBuffer(capacity);
this.inputFrameDownsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncQ = new RingBuffer(capacity);
this.peerInputFrameUpsyncCnt = 0;
this.immediateRollbackFrames = 0;
this.skippedRenderFrameCnt = 0;
this.inputRateThreshold = gopkgs.ConvertToNoDelayInputFrameId(60);
this.peerUpsyncThreshold = 8;
this.rollbackFramesThreshold = 8; // Roughly the minimum "TurnAroundFramesToRecover".
};
NetworkDoctor.prototype.logSending = function(stFrameId, edFrameId) {
this.sendingQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logInputFrameDownsync = function(stFrameId, edFrameId) {
this.inputFrameDownsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
};
NetworkDoctor.prototype.logPeerInputFrameUpsync = function(stFrameId, edFrameId) {
const firstPopped = this.peerInputFrameUpsyncQ.put({
i: stFrameId,
j: edFrameId,
t: Date.now()
});
if (null != firstPopped) {
this.peerInputFrameUpsyncCnt -= (firstPopped.j - firstPopped.i + 1);
}
this.peerInputFrameUpsyncCnt += (edFrameId - stFrameId + 1);
};
NetworkDoctor.prototype.logRollbackFrames = function(x) {
this.immediateRollbackFrames = x;
};
NetworkDoctor.prototype.stats = function() {
let sendingFps = 0,
srvDownsyncFps = 0,
peerUpsyncFps = 0,
rollbackFrames = this.immediateRollbackFrames;
if (1 < this.sendingQ.cnt) {
const st = this.sendingQ.getByFrameId(this.sendingQ.stFrameId);
const ed = this.sendingQ.getByFrameId(this.sendingQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
sendingFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.inputFrameDownsyncQ.cnt) {
const st = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.stFrameId);
const ed = this.inputFrameDownsyncQ.getByFrameId(this.inputFrameDownsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
srvDownsyncFps = Math.round((ed.j - st.i) * 1000 / elapsedMillis);
}
if (1 < this.peerInputFrameUpsyncQ.cnt) {
const st = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.stFrameId);
const ed = this.peerInputFrameUpsyncQ.getByFrameId(this.peerInputFrameUpsyncQ.edFrameId - 1);
const elapsedMillis = ed.t - st.t;
peerUpsyncFps = Math.round(this.peerInputFrameUpsyncCnt * 1000 / elapsedMillis);
}
return [sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, this.skippedRenderFrameCnt];
};
NetworkDoctor.prototype.logSkippedRenderFrameCnt = function() {
this.skippedRenderFrameCnt += 1;
}
NetworkDoctor.prototype.isTooFast = function(mapIns) {
const [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;
} 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))) {
// first comparison condition is to avoid numeric overflow
console.log(`Game logic ticking too fast, selfInputFrameIdFront=${selfInputFrameIdFront}, minInputFrameIdFront=${minInputFrameIdFront}, inputFrameUpsyncDelayTolerance=${mapIns.inputFrameUpsyncDelayTolerance}`);
return true;
}
}
}
return false;
};
module.exports = NetworkDoctor;

View File

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

View File

@@ -11,9 +11,13 @@ cc.Class({
},
onLoad() {
cc.game.setFrameRate(60);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true);
const self = this;
window.mapIns = self;
self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true;
const mapNode = self.node;
const canvasNode = mapNode.parent;
@@ -94,7 +98,7 @@ cc.Class({
const p2Vpos = gopkgs.WorldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y);
const colliderRadiusV = gopkgs.WorldToVirtualGridPos(12.0, 0);
const speciesIdList = [4096, 1];
const speciesIdList = [4096, 0];
const chConfigsOrderedByJoinIndex = gopkgs.GetCharacterConfigsOrderedByJoinIndex(speciesIdList);
const startRdf = window.pb.protos.RoomDownsyncFrame.create({
@@ -105,6 +109,8 @@ cc.Class({
joinIndex: 1,
virtualGridX: p1Vpos[0],
virtualGridY: p1Vpos[1],
revivalVirtualGridX: p1Vpos[0],
revivalVirtualGridY: p1Vpos[1],
speed: chConfigsOrderedByJoinIndex[0].Speed,
colliderRadius: colliderRadiusV[0],
characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0],
@@ -115,12 +121,16 @@ cc.Class({
velY: 0,
inAir: true,
onWall: false,
hp: 100,
maxHp: 100,
}),
window.pb.protos.PlayerDownsync.create({
id: 11,
joinIndex: 2,
virtualGridX: p2Vpos[0],
virtualGridY: p2Vpos[1],
revivalVirtualGridX: p2Vpos[0],
revivalVirtualGridY: p2Vpos[1],
speed: chConfigsOrderedByJoinIndex[1].Speed,
colliderRadius: colliderRadiusV[0],
characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0],
@@ -131,6 +141,8 @@ cc.Class({
velY: 0,
inAir: true,
onWall: false,
hp: 100,
maxHp: 100,
}),
],
speciesIdList: speciesIdList,
@@ -140,6 +152,35 @@ cc.Class({
Id: 10,
JoinIndex: 1,
};
if (cc.sys.isNative) {
window.onUdpMessage = (args) => {
const len = args.length;
const ui8Arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
ui8Arr[i] = args.charCodeAt(i);
}
cc.log(`#1 Js called back by CPP: onUdpMessage: args=${args}, typeof(args)=${typeof (args)}, argslen=${args.length}, ui8Arr=${ui8Arr}`);
const echoed = window.pb.protos.HolePunchUpsync.decode(ui8Arr);
cc.log(`#2 Js called back by CPP: onUdpMessage: ${JSON.stringify(echoed)}`);
};
const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + self.selfPlayerInfo.JoinIndex);
const holePunchData = window.pb.protos.HolePunchUpsync.encode({
boundRoomId: 22,
intAuthToken: "foobar",
authKey: Math.floor(Math.random() * 65535),
}).finish()
//const res2 = DelayNoMore.UdpSession.punchToServer("127.0.0.1", 3000, holePunchData, 19999, holePunchData);
const res3 = DelayNoMore.UdpSession.upsertPeerUdpAddr([window.pb.protos.PeerUdpAddr.create({
ip: "192.168.31.194",
port: 6789,
authKey: 123456,
}), window.pb.protos.PeerUdpAddr.create({
ip: "192.168.1.101",
port: 8771,
authKey: 654321,
})], 2, self.selfPlayerInfo.JoinIndex);
//const res4 = DelayNoMore.UdpSession.closeUdpSession();
}
self.onRoomDownsyncFrame(startRdf);
self.battleState = ALL_BATTLE_STATES.IN_BATTLE;
@@ -150,19 +191,13 @@ cc.Class({
update(dt) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) {
const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt;
if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) {
// [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience!
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
return;
}
try {
let st = performance.now();
let prevSelfInput = null,
currSelfInput = null;
const noDelayInputFrameId = gopkgs.ConvertToNoDelayInputFrameId(self.renderFrameId); // It's important that "inputDelayFrames == 0" here
if (gopkgs.ShouldGenerateInputFrameUpsync(self.renderFrameId)) {
const prevAndCurrInputs = self.getOrPrefabInputFrameUpsync(noDelayInputFrameId);
const prevAndCurrInputs = self.getOrPrefabInputFrameUpsync(noDelayInputFrameId, true);
prevSelfInput = prevAndCurrInputs[0];
currSelfInput = prevAndCurrInputs[1];
}

View File

@@ -11,14 +11,6 @@ cc.Class({
type: cc.Object,
default: null
},
myAvatarNode: {
type: cc.Node,
default: null,
},
myNameNode: {
type: cc.Node,
default: null,
},
rankingNodes: {
type: [cc.Node],
default: [],
@@ -46,22 +38,6 @@ cc.Class({
showPlayerInfo(playerRichInfoDict) {
this.showRanking(playerRichInfoDict);
this.showMyAvatar();
this.showMyName();
},
showMyName() {
const selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
let name = 'No name';
if (null == selfPlayerInfo.displayName || "" == selfPlayerInfo.displayName) {
name = selfPlayerInfo.name;
} else {
name = selfPlayerInfo.displayName;
}
if (!this.myNameNode) return;
const myNameNodeLabel = this.myNameNode.getComponent(cc.Label);
if (!myNameNodeLabel || null == name) return;
myNameNodeLabel.string = name;
},
showRanking(playerRichInfoDict) {
@@ -115,42 +91,6 @@ cc.Class({
}
},
showMyAvatar() {
const self = this;
const selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
let remoteUrl = selfPlayerInfo.avatar;
if (remoteUrl == null || remoteUrl == '') {
cc.log(`No avatar to show for myself, check storage.`);
return;
} else {
cc.loader.load({
url: remoteUrl,
type: 'jpg'
}, function(err, texture) {
if (err != null || texture == null) {
console.log(err);
} else {
const sf = new cc.SpriteFrame();
sf.setTexture(texture);
self.myAvatarNode.getComponent(cc.Sprite).spriteFrame = sf;
}
});
}
},
showRibbon(winnerInfo, ribbonNode) {
const selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
const texture = (selfPlayerInfo.playerId == winnerInfo.id) ? "textures/resultPanel/WinRibbon" : "textures/resultPanel/loseRibbon";
cc.loader.loadRes(texture, cc.SpriteFrame, function(err, spriteFrame) {
if (err) {
console.log(err);
return;
}
ribbonNode.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});
},
onClose(evt) {
if (this.node.parent) {
this.node.parent.removeChild(this.node);

View File

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

View File

@@ -169,30 +169,24 @@ cc.Class({
if (self.btnA) {
self.btnA.on(cc.Node.EventType.TOUCH_START, function(evt) {
self._triggerEdgeBtnA(true);
evt.target.runAction(cc.scaleTo(0.1, 0.3));
});
self.btnA.on(cc.Node.EventType.TOUCH_END, function(evt) {
self._triggerEdgeBtnA(false);
evt.target.runAction(cc.scaleTo(0.1, 1.0));
});
self.btnA.on(cc.Node.EventType.TOUCH_CANCEL, function(evt) {
self._triggerEdgeBtnA(false);
evt.target.runAction(cc.scaleTo(0.1, 1.0));
});
}
if (self.btnB) {
self.btnB.on(cc.Node.EventType.TOUCH_START, function(evt) {
self._triggerEdgeBtnB(true);
evt.target.runAction(cc.scaleTo(0.1, 0.3));
});
self.btnB.on(cc.Node.EventType.TOUCH_END, function(evt) {
self._triggerEdgeBtnB(false);
evt.target.runAction(cc.scaleTo(0.1, 1.0));
});
self.btnB.on(cc.Node.EventType.TOUCH_CANCEL, function(evt) {
self._triggerEdgeBtnB(false);
evt.target.runAction(cc.scaleTo(0.1, 1.0));
});
}
@@ -500,13 +494,23 @@ cc.Class({
this.cachedBtnALevel = this.realtimeBtnALevel;
this.btnAEdgeTriggerLock = true;
}
if (rising) {
this.btnA.runAction(cc.scaleTo(0.1, 0.3));
} else {
this.btnA.runAction(cc.scaleTo(0.1, 0.5));
}
},
_triggerEdgeBtnB(rising) {
_triggerEdgeBtnB(rising, evt) {
this.realtimeBtnBLevel = (rising ? 1 : 0);
if (!this.btnBEdgeTriggerLock && (1 - this.realtimeBtnBLevel) == this.cachedBtnBLevel) {
this.cachedBtnBLevel = this.realtimeBtnBLevel;
this.btnBEdgeTriggerLock = true;
}
if (rising) {
this.btnB.runAction(cc.scaleTo(0.1, 0.3));
} else {
this.btnB.runAction(cc.scaleTo(0.1, 0.5));
}
},
});

View File

@@ -11,6 +11,8 @@ window.DOWNSYNC_MSG_ACT_HB_REQ = 1;
window.DOWNSYNC_MSG_ACT_INPUT_BATCH = 2;
window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED = 3;
window.DOWNSYNC_MSG_ACT_FORCED_RESYNC = 4;
window.DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH = 5;
window.DOWNSYNC_MSG_ACT_PEER_UDP_ADDR = 6;
window.sendSafely = function(msgStr) {
/**
@@ -32,7 +34,7 @@ window.closeWSConnection = function(code, reason) {
console.log(`"window.clientSession" is already closed or destroyed.`);
return;
}
console.log(`Closing "window.clientSession" from the client-side.`);
console.log(`Closing "window.clientSession" from the client-side with code=${code}.`);
window.clientSession.close(code, reason);
}
@@ -42,12 +44,24 @@ window.getBoundRoomIdFromPersistentStorage = function() {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
return null;
}
return cc.sys.localStorage.getItem("boundRoomId");
const boundRoomIdStr = cc.sys.localStorage.getItem("boundRoomId");
return (null == boundRoomIdStr ? null : parseInt(boundRoomIdStr));
};
window.getBoundRoomCapacityFromPersistentStorage = function() {
const boundRoomIdExpiresAt = parseInt(cc.sys.localStorage.getItem("boundRoomIdExpiresAt"));
if (!boundRoomIdExpiresAt || Date.now() >= boundRoomIdExpiresAt) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
return null;
}
const boundRoomCapacityStr = cc.sys.localStorage.getItem("boundRoomCapacity");
return (null == boundRoomCapacityStr ? null : parseInt(boundRoomCapacityStr));
};
window.clearBoundRoomIdInBothVolatileAndPersistentStorage = function() {
window.boundRoomId = null;
cc.sys.localStorage.removeItem("boundRoomId");
cc.sys.localStorage.removeItem("boundRoomCapacity");
cc.sys.localStorage.removeItem("boundRoomIdExpiresAt");
};
@@ -56,17 +70,51 @@ window.clearSelfPlayer = function() {
};
window.boundRoomId = getBoundRoomIdFromPersistentStorage();
window.boundRoomCapacity = getBoundRoomCapacityFromPersistentStorage();
window.handleHbRequirements = function(resp) {
console.log(`Handle hb requirements #1`);
if (constants.RET_CODE.OK != resp.ret) return;
if (null == window.boundRoomId) {
// The assignment of "window.mapIns" is inside "Map.onLoad", which precedes "initPersistentSessionClient".
window.mapIns.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); // This field is kept for distinguishing "self" and "others".
window.mapIns.selfPlayerInfo.Id = window.mapIns.selfPlayerInfo.playerId;
window.mapIns.selfPlayerInfo.JoinIndex = resp.peerJoinIndex;
console.log(`Handle hb requirements #2`);
if (null == window.boundRoomId || null == window.boundRoomCapacity) {
window.boundRoomId = resp.bciFrame.boundRoomId;
window.boundRoomCapacity = resp.bciFrame.boundRoomCapacity;
cc.sys.localStorage.setItem('boundRoomId', window.boundRoomId);
cc.sys.localStorage.setItem('boundRoomCapacity', window.boundRoomCapacity);
cc.sys.localStorage.setItem('boundRoomIdExpiresAt', Date.now() + 10 * 60 * 1000); // Temporarily hardcoded, for `boundRoomId` only.
}
console.log(`Handle hb requirements #3`);
if (window.handleBattleColliderInfo) {
window.handleBattleColliderInfo(resp.bciFrame);
}
console.log(`Handle hb requirements #4`);
if (!cc.sys.isNative) {
console.log(`Handle hb requirements #5, web`);
window.initSecondarySession(null, window.boundRoomId);
} else {
console.log(`Handle hb requirements #5, native, bciFrame.battleUdpTunnel=${resp.bciFrame.battleUdpTunnel}, selfPlayerInfo=${JSON.stringify(window.mapIns.selfPlayerInfo)}`);
const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + window.mapIns.selfPlayerInfo.JoinIndex);
window.mapIns.selfPlayerInfo.udpTunnelAuthKey = resp.bciFrame.battleUdpTunnel.authKey;
const intAuthToken = window.mapIns.selfPlayerInfo.intAuthToken;
const authKey = Math.floor(Math.random() * 65535);
window.mapIns.selfPlayerInfo.authKey = authKey;
const holePunchData = window.pb.protos.HolePunchUpsync.encode({
boundRoomId: window.boundRoomId,
intAuthToken: intAuthToken,
authKey: authKey,
}).finish();
const udpTunnelHolePunchData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
playerId: window.mapIns.selfPlayerInfo.Id,
act: window.UPSYNC_MSG_ACT_PLAYER_CMD,
authKey: resp.bciFrame.battleUdpTunnel.authKey,
}).finish();
const res2 = DelayNoMore.UdpSession.punchToServer(backendAddress.HOST, 3000, holePunchData, resp.bciFrame.battleUdpTunnel.port, udpTunnelHolePunchData);
}
};
function _uint8ToBase64(uint8Arr) {
@@ -124,14 +172,13 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
urlToConnect = urlToConnect + "&expectedRoomId=" + expectedRoomId;
} else {
window.boundRoomId = getBoundRoomIdFromPersistentStorage();
window.boundRoomCapacity = getBoundRoomCapacityFromPersistentStorage();
if (null != window.boundRoomId) {
console.log("initPersistentSessionClient with boundRoomId == " + boundRoomId);
urlToConnect = urlToConnect + "&boundRoomId=" + window.boundRoomId;
}
}
const currentHistoryState = window.history && window.history.state ? window.history.state : {};
const clientSession = new WebSocket(urlToConnect);
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
@@ -176,6 +223,19 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
}
mapIns.onRoomDownsyncFrame(resp.rdf, resp.inputFrameDownsyncBatch);
break;
case window.DOWNSYNC_MSG_ACT_PEER_UDP_ADDR:
console.warn(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR resp=${JSON.stringify(resp, null, 2)}`);
if (cc.sys.isNative) {
const peerJoinIndex = resp.peerJoinIndex;
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);
}
}
break;
default:
break;
}
@@ -192,13 +252,21 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
clientSession.onclose = function(evt) {
// [WARNING] The callback "onclose" might be called AFTER the webpage is refreshed with "1001 == evt.code".
console.warn(`The WS clientSession is closed: evt=${JSON.stringify(evt)}, evt.code=${evt.code}`);
if (cc.sys.isNative) {
if (mapIns.frameDataLoggingEnabled) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}
`);
}
DelayNoMore.UdpSession.closeUdpSession();
}
switch (evt.code) {
case constants.RET_CODE.CLIENT_MISMATCHED_RENDER_FRAME:
break;
case constants.RET_CODE.BATTLE_STOPPED:
// deliberately do nothing
if (mapIns.frameDataLoggingEnabled) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}`);
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}
`);
}
break;
case constants.RET_CODE.PLAYER_NOT_ADDABLE_TO_ROOM:
@@ -213,9 +281,10 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
case constants.RET_CODE.MYSQL_ERROR:
case constants.RET_CODE.PLAYER_NOT_FOUND:
case constants.RET_CODE.PLAYER_CHEATING:
case 1006: // Peer(i.e. the backend) gone unexpectedly
case 1006: // Peer(i.e. the backend) gone unexpectedly, but not working for "cc.sys.isNative"
if (mapIns.frameDataLoggingEnabled) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}`);
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}
`);
}
window.clearLocalStorageAndBackToLoginScene(true);
break;
@@ -227,16 +296,68 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
window.clearLocalStorageAndBackToLoginScene = function(shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
console.warn("+++++++ Calling `clearLocalStorageAndBackToLoginScene`");
window.clearSelfPlayer();
if (true != shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
if (window.mapIns && window.mapIns.musicEffectManagerScriptIns) {
window.mapIns.musicEffectManagerScriptIns.stopAllMusic();
}
window.closeWSConnection();
window.clearSelfPlayer();
if (true != shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
cc.director.loadScene('login');
};
// For secondary ws session
window.initSecondarySession = function(onopenCb, boundRoomId) {
if (window.secondarySession && window.secondarySession.readyState == WebSocket.OPEN) {
if (null != onopenCb) {
onopenCb();
}
return;
}
const selfPlayerStr = cc.sys.localStorage.getItem("selfPlayer");
const selfPlayer = null == selfPlayerStr ? null : JSON.parse(selfPlayerStr);
const intAuthToken = null == selfPlayer ? "" : selfPlayer.intAuthToken;
let urlToConnect = backendAddress.PROTOCOL.replace('http', 'ws') + '://' + backendAddress.HOST + ":" + backendAddress.PORT + "/tsrhtSecondary?isSecondary=true&intAuthToken=" + intAuthToken + "&boundRoomId=" + boundRoomId;
const clientSession = new WebSocket(urlToConnect);
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
clientSession.onopen = function(evt) {
console.warn("The secondary WS clientSession is opened.");
window.secondarySession = clientSession;
if (null == onopenCb) return;
onopenCb();
};
clientSession.onmessage = function(evt) {
if (null == evt || null == evt.data) {
return;
}
try {
const resp = window.pb.protos.WsResp.decode(new Uint8Array(evt.data));
//console.log(`Got non-empty onmessage decoded: resp.act=${resp.act}`);
switch (resp.act) {
case window.DOWNSYNC_MSG_ACT_PEER_INPUT_BATCH:
mapIns.onPeerInputFrameUpsync(resp.peerJoinIndex, resp.inputFrameDownsyncBatch, false);
break;
default:
break;
}
} catch (e) {
console.error("Secondary ws session, unexpected error when parsing data of:", evt.data, e);
}
};
clientSession.onerror = function(evt) {
console.error("Secondary ws session, error caught on the WS clientSession: ", evt);
};
clientSession.onclose = function(evt) {
// [WARNING] The callback "onclose" might be called AFTER the webpage is refreshed with "1001 == evt.code".
console.warn(`Secondary ws session is closed: evt=${JSON.stringify(evt)}, evt.code=${evt.code}`);
};
};

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.5",
"uuid": "d41313ca-b2c3-4436-a05f-7e0eb290b1e6",
"uuid": "171e2c96-28b4-4225-bdcc-5e464f07d91a",
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": true,

View File

@@ -0,0 +1,81 @@
{
"do_default": {
"exclude_from_template": [
"frameworks/runtime-src"
]
},
"do_add_native_support": {
"append_from_template": {
"from": "frameworks/runtime-src",
"to": "frameworks/runtime-src",
"exclude": [
"proj.win32/Debug.win32",
"proj.win32/Release.win32",
"proj.win32/DelayNoMore.sdf"
]
},
"append_file": [{
"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/send_ring_buff.cpp",
"to": "frameworks/runtime-src/Classes/send_ring_buff.cpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session.hpp",
"to": "frameworks/runtime-src/Classes/udp_session.hpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session.cpp",
"to": "frameworks/runtime-src/Classes/udp_session.cpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session_bridge.hpp",
"to": "frameworks/runtime-src/Classes/udp_session_bridge.hpp"
}, {
"from": "frameworks/runtime-src/Classes/udp_session_bridge.cpp",
"to": "frameworks/runtime-src/Classes/udp_session_bridge.cpp"
}],
"project_rename": {
"src_project_name": "DelayNoMore",
"files": [
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.filters",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.user",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.sln"
]
},
"project_replace_project_name": {
"src_project_name": "DelayNoMore",
"files": [
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.filters",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.vcxproj.user",
"frameworks/runtime-src/proj.win32/PROJECT_NAME.sln",
"frameworks/runtime-src/proj.win32/main.cpp",
"frameworks/runtime-src/proj.android-studio/settings.gradle",
"frameworks/runtime-src/proj.android-studio/app/res/values/strings.xml",
"frameworks/runtime-src/Classes/AppDelegate.cpp"
]
},
"project_replace_package_name": {
"src_package_name": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.android-studio/app/build.gradle",
"frameworks/runtime-src/proj.android-studio/app/AndroidManifest.xml"
]
},
"project_replace_mac_bundleid": {
"src_bundle_id": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.ios_mac/mac/Info.plist"
]
},
"project_replace_ios_bundleid": {
"src_bundle_id": "org.genxium.delaynomore",
"files": [
"frameworks/runtime-src/proj.ios_mac/ios/Info.plist"
]
}
}
}

View File

@@ -0,0 +1,93 @@
/****************************************************************************
Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
http://www.cocos.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated engine source code (the "Software"), a limited,
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
to use Cocos Creator solely to develop games on your target platforms. You shall
not use Cocos Creator software for developing other software or tools that's
used for developing games. You are not granted to publish, distribute,
sublicense, and/or sell copies of Cocos Creator.
The software or tools in this License Agreement are licensed, not sold.
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
****************************************************************************/
#include "AppDelegate.h"
#include "cocos2d.h"
//#include "cocos/audio/include/AudioEngine.h"
#include "cocos/scripting/js-bindings/manual/jsb_module_register.hpp"
#include "cocos/scripting/js-bindings/manual/jsb_global.h"
#include "cocos/scripting/js-bindings/jswrapper/SeApi.h"
#include "cocos/scripting/js-bindings/event/EventDispatcher.h"
#include "cocos/scripting/js-bindings/manual/jsb_classtype.hpp"
#include "udp_session_bridge.hpp"
USING_NS_CC;
AppDelegate::AppDelegate(int width, int height) : Application("Cocos Game", width, height)
{
}
AppDelegate::~AppDelegate()
{
}
bool AppDelegate::applicationDidFinishLaunching()
{
se::ScriptEngine *se = se::ScriptEngine::getInstance();
jsb_set_xxtea_key("");
jsb_init_file_operation_delegate();
#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0)
// Enable debugger here
jsb_enable_debugger("0.0.0.0", 6086, false);
#endif
se->setExceptionCallback([](const char *location, const char *message, const char *stack) {
// Send exception information to server like Tencent Bugly.
});
jsb_register_all_modules();
se->addRegisterCallback(registerUdpSession);
se->start();
se::AutoHandleScope hs;
jsb_run_script("jsb-adapter/jsb-builtin.js");
jsb_run_script("main.js");
se->addAfterCleanupHook([]() {
JSBClassType::destroy();
});
return true;
}
// This function will be called when the app is inactive. When comes a phone call,it's be invoked too
void AppDelegate::applicationDidEnterBackground()
{
EventDispatcher::dispatchEnterBackgroundEvent();
// Ensure that handle AudioEngine enter background after all enter background events are handled
//AudioEngine::onEnterBackground();
}
// this function will be called when the app is active again
void AppDelegate::applicationWillEnterForeground()
{
// Ensure that handle AudioEngine enter foreground before all enter foreground events are handled
//AudioEngine::onEnterForeground();
EventDispatcher::dispatchEnterForegroundEvent();
}

View File

@@ -0,0 +1,102 @@
#include <string.h>
#include "send_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 (int 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 NULL;
}
// 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;
}
}

Some files were not shown because too many files have changed in this diff Show More