Compare commits

..

48 Commits
v1.0.1 ... main

Author SHA1 Message Date
Wing
7e89d703ba Fixed typo. 2023-11-28 08:37:11 +08:00
yflu
dc1e6d3e09 Updated frontend "onPeerInputFrameUpsync" handling for force confirmation. 2023-04-18 07:03:22 +08:00
genxium
28e5c18f00 Fixed typo. 2023-04-14 07:28:01 +08:00
genxium
a241912e7a Updated README. 2023-03-30 15:47:02 +08:00
genxium
c582071f4f Fixes for BackendDynamics disabled. 2023-03-30 15:05:34 +08:00
genxium
59d6300880 Updated README. 2023-03-18 13:22:58 +08:00
yflu
6713feded1 Added back important comment. 2023-03-16 17:35:47 +08:00
genxium
da1204dc63 Minor update. 2023-03-10 16:20:40 +08:00
genxium
ea14ced958 Improved stability. 2023-03-10 15:40:45 +08:00
genxium
b9beee549f Simplified resolv_tailored. 2023-03-02 10:22:27 +08:00
genxium
04d8013cbb Preparing for go2cs transpiling. 2023-03-01 18:20:54 +08:00
genxium
6b503ec95d Updated README. 2023-03-01 10:53:22 +08:00
genxium
71f2a1ecdf Updated documentation. 2023-03-01 07:03:49 +08:00
genxium
de9f3c9090 Minor update. 2023-02-27 14:58:21 +08:00
genxium
96e355eab3 Fixed frontend rollback upon UpdateOnDynamics. 2023-02-27 14:35:19 +08:00
genxium
16e1d8a913 Minor fix. 2023-02-27 12:02:01 +08:00
genxium
04b033be7e Fixed frame data logging. 2023-02-27 11:09:22 +08:00
genxium
7fd96b335a Minor fix. 2023-02-26 23:33:47 +08:00
genxium
8cd5f1d475 Fixed resync. 2023-02-26 23:25:47 +08:00
genxium
21806a3754 Minor updates. 2023-02-26 20:57:40 +08:00
genxium
e213fdfb04 Removed redundant collision related codes. 2023-02-26 20:39:30 +08:00
genxium
b9827f8430 Refined experimental toggle for in-place inputsBuffer update upon dynamics. 2023-02-26 14:59:27 +08:00
genxium
91d16b1cc4 Added an experimental toggle for in-place inputsBuffer update upon dynamics. 2023-02-25 23:40:57 +08:00
genxium
b19868920a Minor update. 2023-02-25 23:27:01 +08:00
genxium
be5200663c Updated input prediction approach upon dynamics. 2023-02-25 23:05:25 +08:00
genxium
7b878ff947 Fixes for backend Golang select-multi-channel implementation. 2023-02-21 22:07:48 +08:00
genxium
c78c480f99 Enhanced UDP session resource management. 2023-02-21 15:21:15 +08:00
genxium
b50874f5c4 Enhanced RecvRingBuff in cpp. 2023-02-21 11:54:06 +08:00
genxium
f1db2972fd Updates for RecvRingBuff. 2023-02-20 08:53:06 +08:00
genxium
16c27b0ce0 Minor fix. 2023-02-19 21:26:49 +08:00
genxium
a44535cad2 Fixes for revival dynamics. 2023-02-19 21:05:25 +08:00
genxium
8b5a96e825 Enhanced exception handling on frontend. 2023-02-19 13:42:25 +08:00
genxium
618531f5c6 Minor fix. 2023-02-18 13:21:34 +08:00
genxium
8345d55e76 Updated collision constant setup. 2023-02-18 11:46:01 +08:00
genxium
a48e2f3cc0 Fixed bullet cloning. 2023-02-18 11:13:33 +08:00
yflu
83419a6f23 Fixed debug boundary drawing. 2023-02-17 22:44:21 +08:00
genxium
b19549b0a8 Enhanced inplace rdf update. 2023-02-17 18:54:51 +08:00
genxium
60866674b5 Fixed multiplayer mode for new dynamics calculation. 2023-02-17 15:38:37 +08:00
genxium
a4941c1273 Minor fix. 2023-02-17 14:35:42 +08:00
genxium
2b304eaa75 A temp dirty commit having mysterious left moving players. 2023-02-17 12:26:07 +08:00
genxium
c6b98855af Reduced externalization cost. 2023-02-16 22:23:12 +08:00
genxium
4e0928cb1b Drafted inplace memory version of game dynamics. 2023-02-16 17:13:09 +08:00
genxium
fb42533f55 Temp update to remove use of MakeFullWrapper for reducing redundant CPU time in profiling. 2023-02-16 10:14:37 +08:00
genxium
9dff989e02 Drafted inplace_ring_buff. 2023-02-15 21:34:27 +08:00
genxium
5b7f35b874 Applied ringbuff to resolv_tailored for reducing memory usage. 2023-02-15 15:38:20 +08:00
genxium
2d179d0cdf Minor update. 2023-02-14 23:27:21 +08:00
genxium
f4b303eb91 Started preparation of in-place clone utilities in jsexport. 2023-02-14 15:38:21 +08:00
genxium
5d92b339f6 Enhanced logging efficiency. 2023-02-13 15:37:13 +08:00
58 changed files with 2259 additions and 2307 deletions

View File

@@ -1,15 +1,21 @@
Please refer to [DelayNoMoreUnity](https://github.com/genxium/DelayNoMoreUnity) for a Unity rebuild with .net backend.
# Preface # 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). This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
[Demo recorded over INTERNET (Phone-Wifi v.s. PC-Wifi UDP holepunched) using an input delay of 6 frames](https://pan.baidu.com/s/1UArwqDShLoPjYppjjqsTqQ?pwd=10wc), and it feels SMOOTH when playing! [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!
![Merged_cut_annotated_spedup](./charts/Merged_cut_annotated_spedup.gif) ![Merged_cut_annotated_spedup](./charts/Merged_cut_annotated_spedup.gif)
(battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1RL-9M-cK8cFS_Q8afMTrJA?pwd=ryzv)) (battle between 2 celluar 4G users using Android phones, [original video here](https://pan.baidu.com/s/1m50d-VZxEGT3IgeZtww49g?pwd=eqx1))
![Phone4g_battle_spedup](./charts/Phone4g_battle_spedup.gif) ![Phone4g_battle_spedup](./charts/Phone4g_battle_spedup.gif)
**Since v1.0.13, smoothness in worst cases (e.g. turn-around on ground, in air and after dashing) is drastically improved due to update of prediction approach. The gifs and corresponding screenrecordings above are not updated because there's no big difference when network is good -- however, `input delay` is now set to `4 frames` -- while `input delay = 6 frames` was used in the screenrecordings -- and smoothness is even better now (well there's [a new screenrecording for PcWifi vs Android4g here](https://pan.baidu.com/s/1iNrQ2l_wqbWkURMIfyG88w?pwd=fe2f)).** Key changes are listed below.
- [change#1](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/jsexport/battle/battle.go#L647)
- [change#2](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/frontend/assets/scripts/Map.js#L1446)
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**. 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). - 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 (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw). - If UDP hole-punching is not working, e.g. for Symmetric NAT like in 4G/5G cellular network, the frontends will use backend as a UDP tunnel (or relay, whatever you like to call it). This video shows how the UDP tunnel performs for [Phone-4G v.s. PC-Wifi (merged view@v0.9.34, excellent synchronization)](https://pan.baidu.com/s/1yeIrN5TSf6_av_8-N3vdVg?pwd=7tzw).
@@ -17,9 +23,9 @@ As lots of feedbacks ask for a discussion on using UDP instead, I tried to summa
# Notable Features # Notable Features
- Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.9.14/battle_srv/models/room.go#L786) - Backend dynamics toggle via [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L147)
- Recovery upon reconnection (only if backend dynamics is ON) - Recovery upon reconnection (only if backend dynamics is ON)
- Automatically correction for "slow ticker", especially "active slow ticker" which is well-known to be a headache for input synchronization - Automatic correction for "slow ticker", especially "active slow ticker" which is well-known to be a headache for input synchronization
- Frame data logging toggle for both frontend & backend, useful for debugging out of sync entities when developing new features - Frame data logging toggle for both frontend & backend, useful for debugging out of sync entities when developing new features
_(how input delay roughly works)_ _(how input delay roughly works)_
@@ -92,7 +98,14 @@ The easy way is to try out 2 players with test accounts on a same machine.
- Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene. - Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene.
- Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start. - Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start.
- Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync. - Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync.
![screenshot-2](./charts/screenshot-2.png)
![How-to-play-1](./charts/How-to-play-1.png)
![How-to-play-2](./charts/How-to-play-2.png)
![How-to-play-3](./charts/How-to-play-3.png)
![How-to-play-4](./charts/How-to-play-4.png)
## 2 Troubleshooting ## 2 Troubleshooting
@@ -107,9 +120,9 @@ Just restart your `redis-server` process.
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. 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". 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 [prediction mismatch on the frontend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/frontend/assets/scripts/Map.js#L968).
- 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#1 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L1315).
- Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/v0.9.19/battle_srv/models/room.go#L1259). - Detection of [type#2 forceConfirmation on the backend](https://github.com/genxium/DelayNoMore/blob/c582071f4f2e3dd7e83d65562c7c99981252c358/battle_srv/models/room.go#L1328).
There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`. There's also some useful information displayed on the frontend when `true == Map.showNetworkDoctorInfo`.
![networkstats](./charts/networkstats.png) ![networkstats](./charts/networkstats.png)
@@ -120,6 +133,24 @@ When building for native platforms, it's much more convenient to trigger the Coc
``` ```
shell> cd <proj-root> shell> cd <proj-root>
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=true" shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=true"
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.win32 && MSBUILD DelayNoMore.vcxproj -property:Configuration=Debug
```
or
```
shell> cd <proj-root>
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=win32;debug=false"
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.win32 && MSBUILD DelayNoMore.vcxproj -property:Configuration=Release
```
for release.
If `MSBUILD` command is not yet added to `PATH`, Use `Get-Command MSBUILD` in `Developer Command Prompt for VS 2017/2019` to see where the command should come from and add it to `PATH`.
Similarly for Android release build
```
shell> cd <proj-root>
shell> /path/to/CocosCreator.exe --path ./frontend --build "platform=android;debug=false"
shell> cd ./frontend/build/jsb-link/frameworks/runtime-src/proj.android-studio && ./gradlew assembleRelease
``` ```
### 2.4 CococCreator native build reloading ### 2.4 CococCreator native build reloading

View File

@@ -4,9 +4,24 @@ ROOT_DIR=.
GOPROXY=https://goproxy.io GOPROXY=https://goproxy.io
all: help all: help
# To install `gojson` executable
# ```
# go install github.com/ChimeraCoder/gojson/gojson@latest
# ```
#
# OS detection reference https://stackoverflow.com/a/12099167
gen-constants: gen-constants:
gojson -pkg common -name constants -input common/constants.json -o common/constants_struct.go gojson -pkg common -name constants -input common/constants.json -o common/constants_struct.go
ifeq ($(OS),Windows_NT)
sed -i 's/int64/int/g' common/constants_struct.go sed -i 's/int64/int/g' common/constants_struct.go
else
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
sed -i '' -e 's/int64/int/g' common/constants_struct.go
else
sed -i 's/int64/int/g' common/constants_struct.go
endif
endif
run-test: build run-test: build
ServerEnv=TEST ./$(PROJECTNAME) ServerEnv=TEST ./$(PROJECTNAME)

View File

@@ -85,7 +85,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently) c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently)
return return
} }
Logger.Info("A new SmsCaptcha record is needed for: ", zap.String("key", redisKey))
pass := false pass := false
var succRet int var succRet int
if Conf.General.ServerEnv == SERVER_ENV_TEST { if Conf.General.ServerEnv == SERVER_ENV_TEST {
@@ -93,32 +92,28 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
if nil == err && nil != player { if nil == err && nil != player {
pass = true pass = true
succRet = Constants.RetCode.IsTestAcc succRet = Constants.RetCode.IsTestAcc
Logger.Info("A new SmsCaptcha record is needed for: ", zap.String("key", redisKey), zap.Any("player", player))
} }
} }
if !pass { /*
player, err := models.GetPlayerByName(req.Num) // Real phonenum is not supported yet!
if nil == err && nil != player { if !pass {
pass = true if RE_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.IsBotAcc succRet = Constants.RetCode.Ok
} pass = true
} }
if req.CountryCode == "86" {
if !pass { if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
if RE_PHONE_NUM.MatchString(req.Num) { succRet = Constants.RetCode.Ok
succRet = Constants.RetCode.Ok pass = true
pass = true } else {
} succRet = Constants.RetCode.InvalidRequestParam
if req.CountryCode == "86" { pass = false
if RE_CHINA_PHONE_NUM.MatchString(req.Num) { }
succRet = Constants.RetCode.Ok }
pass = true
} else {
succRet = Constants.RetCode.InvalidRequestParam
pass = false
} }
} */
}
if !pass { if !pass {
c.Set(api.RET, Constants.RetCode.InvalidRequestParam) c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
return return
@@ -481,16 +476,6 @@ func (p *playerController) maybeCreateNewPlayer(req smsCaptchaReq) (*models.Play
Logger.Info("Got a test env player:", zap.Any("phonenum", req.Num), zap.Any("playerId", player.Id)) Logger.Info("Got a test env player:", zap.Any("phonenum", req.Num), zap.Any("playerId", player.Id))
return player, nil return player, nil
} }
} else {
botPlayer, err := models.GetPlayerByName(req.Num)
if err != nil {
Logger.Error("Seeking bot player error:", zap.Error(err))
return nil, err
}
if botPlayer != nil {
Logger.Info("Got a bot player:", zap.Any("phonenum", req.Num), zap.Any("playerId", botPlayer.Id))
return botPlayer, nil
}
} }
bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.Sms, extAuthID) bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.Sms, extAuthID)

View File

@@ -74,7 +74,7 @@ func Test_SMSCaptchaGet_illegalPhone(t *testing.T) {
func Test_SMSCaptchaGet_testAcc(t *testing.T) { func Test_SMSCaptchaGet_testAcc(t *testing.T) {
player, err := getTestPlayer() player, err := getTestPlayer()
if err == nil && player != nil { if nil == err && nil != player {
resp := mustDoSmsCaptchaGetReq(fakeSMSCaptchReq(player.Name), t) resp := mustDoSmsCaptchaGetReq(fakeSMSCaptchReq(player.Name), t)
if resp.Ret != Constants.RetCode.IsTestAcc { if resp.Ret != Constants.RetCode.IsTestAcc {
t.Fail() t.Fail()

View File

@@ -44,10 +44,10 @@
"PASSWORD_RESET_CODE_GENERATION_PER_EMAIL_TOO_FREQUENTLY": 2020, "PASSWORD_RESET_CODE_GENERATION_PER_EMAIL_TOO_FREQUENTLY": 2020,
"TRADE_CREATION_TOO_FREQUENTLY": 2021, "TRADE_CREATION_TOO_FREQUENTLY": 2021,
"MAP_NOT_UNLOCKED": 2022, "MAP_NOT_UNLOCKED": 2022,
"GET_SMS_CAPTCHA_RESP_ERROR_CODE": 2023, "GET_SMS_CAPTCHA_RESP_ERROR_CODE": 2023,
"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 2024, "SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 2024,
"SMS_CAPTCHA_NOT_MATCH": 2025, "SMS_CAPTCHA_NOT_MATCH": 2025,
"SAME_PLAYER_ALREADY_IN_SAME_ROOM": 2026,
"NOT_IMPLEMENTED_YET": 65535 "NOT_IMPLEMENTED_YET": 65535
}, },

View File

@@ -17,6 +17,7 @@ type constants struct {
RetCode struct { RetCode struct {
ActiveWatchdog int `json:"ACTIVE_WATCHDOG"` ActiveWatchdog int `json:"ACTIVE_WATCHDOG"`
BattleStopped int `json:"BATTLE_STOPPED"` BattleStopped int `json:"BATTLE_STOPPED"`
ClientMismatchedRenderFrame int `json:"CLIENT_MISMATCHED_RENDER_FRAME"`
Duplicated int `json:"DUPLICATED"` Duplicated int `json:"DUPLICATED"`
FailedToCreate int `json:"FAILED_TO_CREATE"` FailedToCreate int `json:"FAILED_TO_CREATE"`
FailedToDelete int `json:"FAILED_TO_DELETE"` FailedToDelete int `json:"FAILED_TO_DELETE"`
@@ -51,6 +52,7 @@ type constants struct {
PlayerNotAddableToRoom int `json:"PLAYER_NOT_ADDABLE_TO_ROOM"` PlayerNotAddableToRoom int `json:"PLAYER_NOT_ADDABLE_TO_ROOM"`
PlayerNotFound int `json:"PLAYER_NOT_FOUND"` PlayerNotFound int `json:"PLAYER_NOT_FOUND"`
PlayerNotReaddableToRoom int `json:"PLAYER_NOT_READDABLE_TO_ROOM"` PlayerNotReaddableToRoom int `json:"PLAYER_NOT_READDABLE_TO_ROOM"`
SamePlayerAlreadyInSameRoom int `json:"SAME_PLAYER_ALREADY_IN_SAME_ROOM"`
SendEmailTimeout int `json:"SEND_EMAIL_TIMEOUT"` SendEmailTimeout int `json:"SEND_EMAIL_TIMEOUT"`
SmsCaptchaNotMatch int `json:"SMS_CAPTCHA_NOT_MATCH"` SmsCaptchaNotMatch int `json:"SMS_CAPTCHA_NOT_MATCH"`
SmsCaptchaRequestedTooFrequently int `json:"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY"` SmsCaptchaRequestedTooFrequently int `json:"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY"`

View File

@@ -3,6 +3,7 @@ module battle_srv
go 1.18 go 1.18
require ( require (
dnmshared v0.0.0
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414 github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be
@@ -19,14 +20,12 @@ require (
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713 github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713
go.uber.org/zap v1.9.1 go.uber.org/zap v1.9.1
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
jsexport v0.0.0
dnmshared v0.0.0 resolv v0.0.0
jsexport v0.0.0
resolv v0.0.0
) )
require ( require (
github.com/ChimeraCoder/gojson v1.0.0 // indirect github.com/ChimeraCoder/gojson v1.1.0 // indirect
github.com/fatih/color v1.7.0 // indirect github.com/fatih/color v1.7.0 // indirect
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/githubnemo/CompileDaemon v1.0.0 // indirect github.com/githubnemo/CompileDaemon v1.0.0 // indirect
@@ -44,11 +43,11 @@ require (
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
replace ( replace (
dnmshared => ../dnmshared dnmshared => ../dnmshared
jsexport => ../jsexport jsexport => ../jsexport
resolv => ../resolv_tailored resolv => ../resolv_tailored
) )

View File

@@ -2,6 +2,8 @@ github.com/ByteArena/box2d v1.0.2 h1:f7f9KEQWhCs1n516DMLzi5w6u0MeeE78Mes4fWMcj9k
github.com/ByteArena/box2d v1.0.2/go.mod h1:LzEuxY9iCz+tskfWCY3o0ywYBRafDDugdSj+/YGI6sE= github.com/ByteArena/box2d v1.0.2/go.mod h1:LzEuxY9iCz+tskfWCY3o0ywYBRafDDugdSj+/YGI6sE=
github.com/ChimeraCoder/gojson v1.0.0 h1:gAYKGTV+xfQ4+l/4C/nazPbiQDUidG0G3ukAJnE7LNE= github.com/ChimeraCoder/gojson v1.0.0 h1:gAYKGTV+xfQ4+l/4C/nazPbiQDUidG0G3ukAJnE7LNE=
github.com/ChimeraCoder/gojson v1.0.0/go.mod h1:nYbTQlu6hv8PETM15J927yM0zGj3njIldp72UT1MqSw= github.com/ChimeraCoder/gojson v1.0.0/go.mod h1:nYbTQlu6hv8PETM15J927yM0zGj3njIldp72UT1MqSw=
github.com/ChimeraCoder/gojson v1.1.0 h1:/6S8djl/jColpJGTYniA3xrqJWuKeyEozzPtpr5L4Pw=
github.com/ChimeraCoder/gojson v1.1.0/go.mod h1:nYbTQlu6hv8PETM15J927yM0zGj3njIldp72UT1MqSw=
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414 h1:VHdYPA0V0YgL97gdjHevN6IVPRX+fOoNMqcYvUAzwNU= github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414 h1:VHdYPA0V0YgL97gdjHevN6IVPRX+fOoNMqcYvUAzwNU=
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM= github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM=
github.com/Pallinder/go-randomdata v0.0.0-20180616180521-15df0648130a h1:0OnS8GR4uI3nau+f/juCZlAq+zCrsHXRJlENrUQ4eU8= github.com/Pallinder/go-randomdata v0.0.0-20180616180521-15df0648130a h1:0OnS8GR4uI3nau+f/juCZlAq+zCrsHXRJlENrUQ4eU8=
@@ -92,3 +94,5 @@ gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2G
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@@ -25,9 +25,16 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"net" "net"
// _ "net/http/pprof"
) )
func main() { func main() {
/*
// Only used for profiling
go func() {
http.ListenAndServe("0.0.0.0:6060", nil)
}()
*/
MustParseConfig() MustParseConfig()
MustParseConstants() MustParseConstants()
storage.Init() storage.Init()

View File

@@ -47,11 +47,10 @@ type Player struct {
TutorialStage int `db:"tutorial_stage"` TutorialStage int `db:"tutorial_stage"`
// other in-battle info fields // other in-battle info fields
LastReceivedInputFrameId int32 LastConsecutiveRecvInputFrameId int32
LastUdpReceivedInputFrameId int32 LastSentInputFrameId int32
LastSentInputFrameId int32 AckingFrameId int32
AckingFrameId int32 AckingInputFrameId int32
AckingInputFrameId int32
UdpAddr *PeerUdpAddr UdpAddr *PeerUdpAddr
BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend BattleUdpTunnelAddr *net.UDPAddr // This addr is used by backend only, not visible to frontend
@@ -78,13 +77,14 @@ func getPlayer(cond sq.Eq) (*Player, error) {
return nil, err return nil, err
} }
rows, err := storage.MySQLManagerIns.Queryx(query, args...) rows, err := storage.MySQLManagerIns.Queryx(query, args...)
if err != nil { if nil != err {
return nil, err return nil, err
} }
cols, err := rows.Columns() cols, err := rows.Columns()
if nil != err { if nil != err {
panic(err) panic(err)
} }
cnt := 0
for rows.Next() { for rows.Next() {
// TODO: Do it more elegantly, but by now I don't have time to learn reflection of Golang // TODO: Do it more elegantly, but by now I don't have time to learn reflection of Golang
vals := rowValues(rows, cols) vals := rowValues(rows, cols)
@@ -106,9 +106,14 @@ func getPlayer(cond sq.Eq) (*Player, error) {
} }
} }
Logger.Debug("Queried player from db", zap.Any("cond", cond), zap.Any("p", p), zap.Any("pd", pd), zap.Any("cols", cols), zap.Any("rowValues", vals)) Logger.Debug("Queried player from db", zap.Any("cond", cond), zap.Any("p", p), zap.Any("pd", pd), zap.Any("cols", cols), zap.Any("rowValues", vals))
cnt++
}
if 0 < cnt {
p.PlayerDownsync = pd
return &p, nil
} else {
return nil, nil
} }
p.PlayerDownsync = pd
return &p, nil
} }
func (p *Player) Insert(tx *sqlx.Tx) error { func (p *Player) Insert(tx *sqlx.Tx) error {

View File

@@ -135,9 +135,9 @@ type Room struct {
CurDynamicsRenderFrameId int32 // [WARNING] The dynamics of backend is ALWAYS MOVING FORWARD BY ALL-CONFIRMED INPUTFRAMES (either by upsync or forced), i.e. no rollback; Moreover when "true == BackendDynamicsEnabled" we always have "Room.CurDynamicsRenderFrameId >= Room.RenderFrameId" because each "all-confirmed inputFrame" is applied on "all applicable renderFrames" in one-go hence often sees a future "renderFrame" earlier CurDynamicsRenderFrameId int32 // [WARNING] The dynamics of backend is ALWAYS MOVING FORWARD BY ALL-CONFIRMED INPUTFRAMES (either by upsync or forced), i.e. no rollback; Moreover when "true == BackendDynamicsEnabled" we always have "Room.CurDynamicsRenderFrameId >= Room.RenderFrameId" because each "all-confirmed inputFrame" is applied on "all applicable renderFrames" in one-go hence often sees a future "renderFrame" earlier
EffectivePlayerCount int32 EffectivePlayerCount int32
DismissalWaitGroup sync.WaitGroup DismissalWaitGroup sync.WaitGroup
InputsBuffer *battle.RingBuffer // Indices are STRICTLY consecutive InputsBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId, player.LastUdpReceivedInputFrameId] InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputFrameId, LastIndividuallyConfirmedInputList, player.LastConsecutiveRecvInputFrameId]
RenderFrameBuffer *battle.RingBuffer // Indices are STRICTLY consecutive RenderFrameBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
LatestPlayerUpsyncedInputFrameId int32 LatestPlayerUpsyncedInputFrameId int32
LastAllConfirmedInputFrameId int32 LastAllConfirmedInputFrameId int32
LastAllConfirmedInputFrameIdWithChange int32 LastAllConfirmedInputFrameIdWithChange int32
@@ -156,28 +156,35 @@ type Room struct {
TmxPointsMap StrToVec2DListMap TmxPointsMap StrToVec2DListMap
TmxPolygonsMap StrToPolygon2DListMap TmxPolygonsMap StrToPolygon2DListMap
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
LastIndividuallyConfirmedInputList []uint64 LastIndividuallyConfirmedInputFrameId []int32
LastIndividuallyConfirmedInputList []uint64
BattleUdpTunnelLock sync.Mutex BattleUdpTunnelLock sync.Mutex
BattleUdpTunnelAddr *pb.PeerUdpAddr BattleUdpTunnelAddr *pb.PeerUdpAddr
BattleUdpTunnel *net.UDPConn BattleUdpTunnel *net.UDPConn
collisionHolder *resolv.Collision
effPushbacks []*battle.Vec2D
hardPushbackNormsArr [][]*battle.Vec2D
jumpedOrNotList []bool
dynamicRectangleColliders []*resolv.Object
} }
func (pR *Room) updateScore() { func (pR *Room) updateScore() {
pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State) pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State)
} }
func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) bool { func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) int {
playerId := pPlayerFromDbInit.Id playerId := pPlayerFromDbInit.Id
// TODO: Any thread-safety concern for accessing "pR" here? // TODO: Any thread-safety concern for accessing "pR" here?
if RoomBattleStateIns.IDLE != pR.State && RoomBattleStateIns.WAITING != pR.State { if RoomBattleStateIns.IDLE != pR.State && RoomBattleStateIns.WAITING != pR.State {
Logger.Warn("AddPlayerIfPossible error, roomState:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount)) Logger.Warn("AddPlayerIfPossible error, roomState:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
return false return Constants.RetCode.PlayerNotAddableToRoom
} }
if _, existent := pR.Players[playerId]; existent { if _, existent := pR.Players[playerId]; existent {
Logger.Warn("AddPlayerIfPossible error, existing in the room.PlayersDict:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount)) Logger.Warn("AddPlayerIfPossible error, existing in the room.PlayersDict:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
return false return Constants.RetCode.SamePlayerAlreadyInSameRoom
} }
defer pR.onPlayerAdded(playerId, speciesId) defer pR.onPlayerAdded(playerId, speciesId)
@@ -188,8 +195,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, se
pPlayerFromDbInit.AckingFrameId = -1 pPlayerFromDbInit.AckingFrameId = -1
pPlayerFromDbInit.AckingInputFrameId = -1 pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.LastReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pPlayerFromDbInit.LastConsecutiveRecvInputFrameId = 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.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -204,19 +210,19 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, se
}) })
newWatchdog.Stop() newWatchdog.Stop()
pR.PlayerActiveWatchdogDict[playerId] = newWatchdog pR.PlayerActiveWatchdogDict[playerId] = newWatchdog
return true return Constants.RetCode.Ok
} }
func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) bool { func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *websocket.Conn, signalToCloseConnOfThisPlayer SignalToCloseConnCbType) int {
playerId := pTmpPlayerInstance.Id playerId := pTmpPlayerInstance.Id
// TODO: Any thread-safety concern for accessing "pR" and "pEffectiveInRoomPlayerInstance" here? // TODO: Any thread-safety concern for accessing "pR" and "pEffectiveInRoomPlayerInstance" here?
if RoomBattleStateIns.PREPARE != pR.State && RoomBattleStateIns.WAITING != pR.State && RoomBattleStateIns.IN_BATTLE != pR.State && RoomBattleStateIns.IN_SETTLEMENT != pR.State && RoomBattleStateIns.IN_DISMISSAL != pR.State { if RoomBattleStateIns.PREPARE != pR.State && RoomBattleStateIns.WAITING != pR.State && RoomBattleStateIns.IN_BATTLE != pR.State && RoomBattleStateIns.IN_SETTLEMENT != pR.State && RoomBattleStateIns.IN_DISMISSAL != pR.State {
Logger.Warn("ReAddPlayerIfPossible error due to roomState:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount)) Logger.Warn("ReAddPlayerIfPossible error due to roomState:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
return false return Constants.RetCode.PlayerNotReaddableToRoom
} }
if _, existent := pR.Players[playerId]; !existent { if _, existent := pR.Players[playerId]; !existent {
Logger.Warn("ReAddPlayerIfPossible error due to player nonexistent for room:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount)) Logger.Warn("ReAddPlayerIfPossible error due to player nonexistent for room:", zap.Any("playerId", playerId), zap.Any("roomId", pR.Id), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount))
return false return Constants.RetCode.PlayerNotReaddableToRoom
} }
/* /*
* WARNING: The "pTmpPlayerInstance *Player" used here is a temporarily constructed * WARNING: The "pTmpPlayerInstance *Player" used here is a temporarily constructed
@@ -231,7 +237,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pEffectiveInRoomPlayerInstance.AckingFrameId = -1 pEffectiveInRoomPlayerInstance.AckingFrameId = -1
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1 pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
// [WARNING] DON'T reset "player.LastReceivedInputFrameId" & "player.LastUdpReceivedInputFrameId" upon reconnection! // [WARNING] DON'T reset "player.LastConsecutiveRecvInputFrameId" & "pR.LastIndividuallyConfirmedInputFrameId[...]" upon reconnection!
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
@@ -245,7 +251,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
}) // For ReAdded player the new watchdog starts immediately }) // 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)) 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))
return true return Constants.RetCode.Ok
} }
func (pR *Room) ChooseStage() error { func (pR *Room) ChooseStage() error {
@@ -400,6 +406,9 @@ func (pR *Room) rdfIdToActuallyUsedInputString() string {
} }
fireballsStrBldr := make([]string, 0, len(rdf.FireballBullets)) fireballsStrBldr := make([]string, 0, len(rdf.FireballBullets))
for _, fireball := range rdf.FireballBullets { for _, fireball := range rdf.FireballBullets {
if 0 > fireball.BattleAttr.BulletLocalId {
break
}
fireballsStrBldr = append(fireballsStrBldr, pR.fireballDownsyncStr(fireball)) fireballsStrBldr = append(fireballsStrBldr, pR.fireballDownsyncStr(fireball))
} }
s = append(s, fmt.Sprintf("rdfId:%d\nplayers:[%v]\nfireballs:[%v]\nactuallyUsedinputList:{%v}", rdfId, strings.Join(playersStrBldr, ","), strings.Join(fireballsStrBldr, ","), pR.inputFrameDownsyncStr(pR.rdfIdToActuallyUsedInput[rdfId]))) s = append(s, fmt.Sprintf("rdfId:%d\nplayers:[%v]\nfireballs:[%v]\nactuallyUsedinputList:{%v}", rdfId, strings.Join(playersStrBldr, ","), strings.Join(fireballsStrBldr, ","), pR.inputFrameDownsyncStr(pR.rdfIdToActuallyUsedInput[rdfId])))
@@ -420,11 +429,10 @@ func (pR *Room) StartBattle() {
// Initialize the "collisionSys" as well as "RenderFrameBuffer" // Initialize the "collisionSys" as well as "RenderFrameBuffer"
pR.CurDynamicsRenderFrameId = 0 pR.CurDynamicsRenderFrameId = 0
kickoffFrameJs := &battle.RoomDownsyncFrame{ kickoffFrameJs := battle.NewPreallocatedRoomDownsyncFrame(len(pR.Players), 64, 64)
Id: pR.RenderFrameId, battle.CloneRoomDownsyncFrame(pR.RenderFrameId, toJsPlayers(pR.Players), 0, make([]*battle.MeleeBullet, 0), make([]*battle.FireballBullet, 0), kickoffFrameJs)
PlayersArr: toJsPlayers(pR.Players), kickoffFrameJs.CountdownNanos = pR.BattleDurationNanos
CountdownNanos: pR.BattleDurationNanos,
}
pR.RenderFrameBuffer.Put(kickoffFrameJs) pR.RenderFrameBuffer.Put(kickoffFrameJs)
// Refresh "Colliders" // Refresh "Colliders"
@@ -478,7 +486,7 @@ func (pR *Room) StartBattle() {
*/ */
totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt
nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling
toSleepNanos := int64(0) toSleepNanos := int64(pR.dilutedRollbackEstimatedDtNanos >> 1) // Sleep half-frame time by default
if nextRenderFrameId > pR.RenderFrameId { if nextRenderFrameId > pR.RenderFrameId {
if 0 == pR.RenderFrameId { if 0 == pR.RenderFrameId {
// It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames // It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames
@@ -510,7 +518,7 @@ func (pR *Room) StartBattle() {
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano() pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation) elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation // don't sleep if "nextRenderFrame == pR.RenderFrameId" toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation
if elapsedInCalculation > pR.RollbackEstimatedDtNanos { if elapsedInCalculation > pR.RollbackEstimatedDtNanos {
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos)) Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos))
} }
@@ -539,13 +547,13 @@ func (pR *Room) StartBattle() {
} }
select { select {
// [WARNING] DON'T put a "default" block here! Otherwise "for { select {... default: } }" pattern would NEVER block on empty channel and thus consume a lot of CPU time unnecessarily!
case inputsBufferSnapshot := <-playerDownsyncChan: case inputsBufferSnapshot := <-playerDownsyncChan:
pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync) 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)) //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: case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan:
pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex) 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)) //Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId))
default:
} }
} }
} }
@@ -772,7 +780,7 @@ func (pR *Room) OnDismissed() {
// Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference. // Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference.
pR.BulletBattleLocalIdCounter = 0 pR.BulletBattleLocalIdCounter = 0
pR.CollisionMinStep = 8 // the approx minimum distance a player can move per frame in world coordinate pR.CollisionMinStep = 16 // the approx minimum distance a player can move per frame in world coordinate
pR.Players = make(map[int32]*Player) pR.Players = make(map[int32]*Player)
pR.PlayersArr = make([]*Player, pR.Capacity) pR.PlayersArr = make([]*Player, pR.Capacity)
pR.SpeciesIdList = make([]int32, pR.Capacity) pR.SpeciesIdList = make([]int32, pR.Capacity)
@@ -796,9 +804,13 @@ func (pR *Room) OnDismissed() {
pR.PlayerSecondarySignalToCloseDict = make(map[int32]SignalToCloseConnCbType) pR.PlayerSecondarySignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.JoinIndexBooleanArr = make([]bool, pR.Capacity) pR.JoinIndexBooleanArr = make([]bool, pR.Capacity)
pR.RenderCacheSize = 1024 pR.RenderCacheSize = 1024
pR.RenderFrameBuffer = battle.NewRingBuffer(pR.RenderCacheSize) pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
pR.InputsBuffer = battle.NewRingBuffer((pR.RenderCacheSize >> 1) + 1) pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync) pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity)
for i := 0; i < pR.Capacity; i++ {
pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
}
pR.LastIndividuallyConfirmedInputList = make([]uint64, pR.Capacity) pR.LastIndividuallyConfirmedInputList = make([]uint64, pR.Capacity)
pR.LatestPlayerUpsyncedInputFrameId = -1 pR.LatestPlayerUpsyncedInputFrameId = -1
@@ -810,6 +822,24 @@ func (pR *Room) OnDismissed() {
pR.CurDynamicsRenderFrameId = 0 pR.CurDynamicsRenderFrameId = 0
pR.NstDelayFrames = 24 pR.NstDelayFrames = 24
pR.collisionHolder = resolv.NewCollision()
pR.effPushbacks = make([]*battle.Vec2D, pR.Capacity)
for i := 0; i < pR.Capacity; i++ {
pR.effPushbacks[i] = &battle.Vec2D{X: 0, Y: 0}
}
pR.hardPushbackNormsArr = make([][]*battle.Vec2D, pR.Capacity)
for i := 0; i < pR.Capacity; i++ {
pR.hardPushbackNormsArr[i] = make([]*battle.Vec2D, 5)
for j := 0; j < len(pR.hardPushbackNormsArr[i]); j++ {
pR.hardPushbackNormsArr[i][j] = &battle.Vec2D{X: 0, Y: 0}
}
}
pR.jumpedOrNotList = make([]bool, pR.Capacity)
pR.dynamicRectangleColliders = make([]*resolv.Object, 64)
for i := 0; i < len(pR.dynamicRectangleColliders); i++ {
pR.dynamicRectangleColliders[i] = battle.GenerateRectCollider(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, nil, "")
}
serverFps := 60 serverFps := 60
pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript
pR.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for logging FAST FRAME pR.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for logging FAST FRAME
@@ -1167,9 +1197,10 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#1: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false))) Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#1: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
continue continue
} }
if clientInputFrameId < player.LastReceivedInputFrameId { if clientInputFrameId < player.LastConsecutiveRecvInputFrameId {
// [WARNING] It's important for correctness that we use "player.LastReceivedInputFrameId" instead of "player.LastUdpReceivedInputFrameId" here! // [WARNING] It's important for correctness that we use "player.LastConsecutiveRecvInputFrameId" instead of "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" 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))) //Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId))
continue continue
} }
if clientInputFrameId > pR.InputsBuffer.EdFrameId { if clientInputFrameId > pR.InputsBuffer.EdFrameId {
@@ -1182,22 +1213,22 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1)) targetInputFrameDownsync.ConfirmedList |= uint64(1 << uint32(player.JoinIndex-1))
if false == fromUDP { 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"! [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"!
Moreover, only ws session upsyncs should advance "player.LastReceivedInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId". Moreover, only ws session upsyncs should advance "player.LastConsecutiveRecvInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
Kindly note that the updates of "player.LastReceivedInputFrameId" could be discrete before and after reconnection. Kindly note that the updates of "player.LastConsecutiveRecvInputFrameId" could be discrete before and after reconnection.
*/ */
player.LastReceivedInputFrameId = clientInputFrameId player.LastConsecutiveRecvInputFrameId = clientInputFrameId
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId { if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
} }
} }
if clientInputFrameId > player.LastUdpReceivedInputFrameId { if clientInputFrameId > pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] {
// No need to update "player.LastUdpReceivedInputFrameId" only when "true == fromUDP", we should keep "player.LastUdpReceivedInputFrameId >= player.LastReceivedInputFrameId" at any moment. // No need to update "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" only when "true == fromUDP", we should keep "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] >= player.LastConsecutiveRecvInputFrameId" at any moment.
player.LastUdpReceivedInputFrameId = clientInputFrameId pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] = 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. // 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 pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
} }
@@ -1227,7 +1258,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
shouldBreakConfirmation = true // Could be an `ACTIVE SLOW TICKER` here, but no action needed for now shouldBreakConfirmation = true // Could be an `ACTIVE SLOW TICKER` here, but no action needed for now
break break
} }
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v, skipping UNCONFIRMED BUT INACTIVE player(id:%v, joinIndex:%v) while checking inputFrameId=[%v, %v): InputsBuffer=%v", pR.Id, player.Id, player.JoinIndex, inputFrameId1, pR.InputsBuffer.EdFrameId, pR.InputsBufferString(false))) //Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v, skipping UNCONFIRMED BUT INACTIVE player(id:%v, joinIndex:%v) while checking inputFrameId=[%v, %v): InputsBuffer=%v", pR.Id, player.Id, player.JoinIndex, inputFrameId1, pR.InputsBuffer.EdFrameId, pR.InputsBufferString(false)))
} }
} }
@@ -1251,7 +1282,7 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
snapshotStFrameId := (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount) snapshotStFrameId := (pR.LastAllConfirmedInputFrameId - newAllConfirmedCount)
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1 refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
refSnapshotStFrameId := battle.ConvertToDelayedInputFrameId(refRenderFrameIdIfNeeded) refSnapshotStFrameId := battle.ConvertToDelayedInputFrameId(refRenderFrameIdIfNeeded)
if refSnapshotStFrameId < snapshotStFrameId { if pR.BackendDynamicsEnabled && refSnapshotStFrameId < snapshotStFrameId {
snapshotStFrameId = refSnapshotStFrameId snapshotStFrameId = refSnapshotStFrameId
} }
Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v returning newAllConfirmedCount=%d: InputsBuffer=%v", pR.Id, newAllConfirmedCount, pR.InputsBufferString(false))) Logger.Debug(fmt.Sprintf("markConfirmationIfApplicable for roomId=%v returning newAllConfirmedCount=%d: InputsBuffer=%v", pR.Id, newAllConfirmedCount, pR.InputsBufferString(false)))
@@ -1281,7 +1312,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1) pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
} }
if 0 < unconfirmedMask { if 0 < unconfirmedMask {
Logger.Info(fmt.Sprintf("[type#1 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, oldLastAllConfirmedInputFrameId:%d, newLastAllConfirmedInputFrameId:%d, InputFrameUpsyncDelayTolerance:%d, unconfirmedMask=%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, oldLastAllConfirmedInputFrameId, pR.LastAllConfirmedInputFrameId, pR.InputFrameUpsyncDelayTolerance, unconfirmedMask)) Logger.Info(fmt.Sprintf("[type#1 forceConfirmation] For roomId=%d@renderFrameId=%d, curDynamicsRenderFrameId=%d, LatestPlayerUpsyncedInputFrameId:%d, LastAllConfirmedInputFrameId:%d -> %d, InputFrameUpsyncDelayTolerance:%d, unconfirmedMask=%d; there's a slow ticker suspect, forcing all-confirmation", pR.Id, pR.RenderFrameId, pR.CurDynamicsRenderFrameId, pR.LatestPlayerUpsyncedInputFrameId, oldLastAllConfirmedInputFrameId, pR.LastAllConfirmedInputFrameId, pR.InputFrameUpsyncDelayTolerance, unconfirmedMask))
} }
} else { } else {
// Type#2 helps resolve the edge case when all players are disconnected temporarily // Type#2 helps resolve the edge case when all players are disconnected temporarily
@@ -1305,7 +1336,7 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
func (pR *Room) produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(unconfirmedMask uint64, snapshotStFrameId, snapshotEdFrameId int32) *pb.InputsBufferSnapshot { func (pR *Room) produceInputsBufferSnapshotWithCurDynamicsRenderFrameAsRef(unconfirmedMask uint64, snapshotStFrameId, snapshotEdFrameId int32) *pb.InputsBufferSnapshot {
// [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked! // [WARNING] This function MUST BE called while "pR.InputsBufferLock" is locked!
refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1 refRenderFrameIdIfNeeded := pR.CurDynamicsRenderFrameId - 1
if 0 > refRenderFrameIdIfNeeded { if pR.BackendDynamicsEnabled && 0 > refRenderFrameIdIfNeeded {
return nil return nil
} }
// Duplicate downsynced inputFrameIds will be filtered out by frontend. // Duplicate downsynced inputFrameIds will be filtered out by frontend.
@@ -1356,8 +1387,7 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
} }
} }
nextRenderFrame := battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr) battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame.Id, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr, pR.RenderFrameBuffer, pR.collisionHolder, pR.effPushbacks, pR.hardPushbackNormsArr, pR.jumpedOrNotList, pR.dynamicRectangleColliders, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, false, MAGIC_JOIN_INDEX_INVALID) // DON'T mutate inputs upon dynamics on backend to avoid complicating the edge cases
pR.RenderFrameBuffer.Put(nextRenderFrame)
pR.CurDynamicsRenderFrameId++ pR.CurDynamicsRenderFrameId++
} }
} }
@@ -1508,6 +1538,7 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
break break
} }
} }
} }
for _, player := range pR.PlayersArr { for _, player := range pR.PlayersArr {
@@ -1524,11 +1555,16 @@ func (pR *Room) downsyncToAllPlayers(inputsBufferSnapshot *pb.InputsBufferSnapsh
if playerDownsyncChan, existent := pR.PlayerDownsyncChanDict[player.Id]; existent { if playerDownsyncChan, existent := pR.PlayerDownsyncChanDict[player.Id]; existent {
playerDownsyncChan <- (*inputsBufferSnapshot) playerDownsyncChan <- (*inputsBufferSnapshot)
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d, playerDownsyncChan:%p)#1", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, player.Id, playerDownsyncChan)) //Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot{refRenderFrameId:%d, unconfirmedMask:%v} to {roomId: %d, playerId:%d, playerDownsyncChan:%p}#1", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, player.Id, playerDownsyncChan))
} else { } else {
Logger.Warn(fmt.Sprintf("playerDownsyncChan for (roomId: %d, playerId:%d) is gone", pR.Id, player.Id)) Logger.Warn(fmt.Sprintf("playerDownsyncChan for (roomId: %d, playerId:%d) is gone", pR.Id, player.Id))
} }
} }
/*
toSendInputFrameDownsyncs := inputsBufferSnapshot.ToSendInputFrameDownsyncs
toSendInputFrameIdSt, toSendInputFrameIdEd := toSendInputFrameDownsyncs[0].InputFrameId, toSendInputFrameDownsyncs[len(toSendInputFrameDownsyncs)-1].InputFrameId+1
Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot{refRenderFrameId:%d, unconfirmedMask:%v, inputFrameIdRange:[%d, %d)} to {roomId: %d}", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, toSendInputFrameIdSt, toSendInputFrameIdEd, pR.Id))
*/
} }
func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRenderFrameId int32, unconfirmedMask uint64, toSendInputFrameDownsyncsSnapshot []*pb.InputFrameDownsync, shouldForceResync bool) { func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRenderFrameId int32, unconfirmedMask uint64, toSendInputFrameDownsyncsSnapshot []*pb.InputFrameDownsync, shouldForceResync bool) {
@@ -1571,9 +1607,10 @@ func (pR *Room) downsyncToSinglePlayer(playerId int32, player *Player, refRender
pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame) pbRefRenderFrame := toPbRoomDownsyncFrame(refRenderFrame)
pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList pbRefRenderFrame.SpeciesIdList = pR.SpeciesIdList
pR.sendSafely(pbRefRenderFrame, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId, false, MAGIC_JOIN_INDEX_DEFAULT) 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 { 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)) 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 {
//Logger.Debug(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)))
} }
} else { } else {
pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT) pR.sendSafely(nil, toSendInputFrameDownsyncsSnapshot, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId, false, MAGIC_JOIN_INDEX_DEFAULT)
@@ -1771,7 +1808,7 @@ func (pR *Room) startBattleUdpTunnel() {
} }
_, wrerr := conn.WriteTo(bytes, otherPlayer.BattleUdpTunnelAddr) _, wrerr := conn.WriteTo(bytes, otherPlayer.BattleUdpTunnelAddr)
if nil != wrerr { 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)) //Logger.Debug(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 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

View File

@@ -188,34 +188,33 @@ func Serve(c *gin.Context) {
}() }()
Logger.Debug("Acquired RoomHeapMux for player:", zap.Any("playerId", playerId)) Logger.Debug("Acquired RoomHeapMux for player:", zap.Any("playerId", playerId))
// Logger.Info("The RoomHeapManagerIns has:", zap.Any("addr", fmt.Sprintf("%p", models.RoomHeapManagerIns)), zap.Any("size", len(*(models.RoomHeapManagerIns)))) // Logger.Info("The RoomHeapManagerIns has:", zap.Any("addr", fmt.Sprintf("%p", models.RoomHeapManagerIns)), zap.Any("size", len(*(models.RoomHeapManagerIns))))
playerSuccessfullyAddedToRoom := false playerRoomRelation := Constants.RetCode.UnknownError
if 0 < boundRoomId { if 0 < boundRoomId {
if tmpPRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]; existent { if tmpPRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]; existent {
pRoom = tmpPRoom pRoom = tmpPRoom
res := pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) playerRoomRelation = pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer)
if !res { if Constants.RetCode.Ok != playerRoomRelation {
Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forBoundRoomId", boundRoomId)) Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forBoundRoomId", boundRoomId))
} else {
playerSuccessfullyAddedToRoom = true
} }
} }
} else if 0 < expectedRoomId { } else if 0 < expectedRoomId {
if tmpRoom, existent := (*models.RoomMapManagerIns)[int32(expectedRoomId)]; existent { if tmpRoom, existent := (*models.RoomMapManagerIns)[int32(expectedRoomId)]; existent {
pRoom = tmpRoom pRoom = tmpRoom
playerRoomRelation = pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer)
if pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) { if Constants.RetCode.Ok != playerRoomRelation {
playerSuccessfullyAddedToRoom = true playerRoomRelation = pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer)
} else if pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer) { }
playerSuccessfullyAddedToRoom = true if Constants.RetCode.Ok != playerRoomRelation {
} else { Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forExpectedRoomId", expectedRoomId))
Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forExpectedRoomId", expectedRoomId))
playerSuccessfullyAddedToRoom = false
} }
} }
} }
if false == playerSuccessfullyAddedToRoom { if Constants.RetCode.SamePlayerAlreadyInSameRoom == playerRoomRelation {
signalToCloseConnOfThisPlayer(playerRoomRelation, fmt.Sprintf("playerId == %v is already in a room, this account is possibly stolen!", playerId))
}
if Constants.RetCode.Ok != playerRoomRelation {
defer func() { defer func() {
if pRoom != nil { if pRoom != nil {
heap.Push(models.RoomHeapManagerIns, pRoom) heap.Push(models.RoomHeapManagerIns, pRoom)
@@ -229,9 +228,9 @@ func Serve(c *gin.Context) {
} else { } else {
pRoom = tmpRoom pRoom = tmpRoom
Logger.Info("Successfully popped:\n", zap.Any("roomId", pRoom.Id), zap.Any("forPlayerId", playerId)) Logger.Info("Successfully popped:\n", zap.Any("roomId", pRoom.Id), zap.Any("forPlayerId", playerId))
res := pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer) playerRoomRelation = pRoom.AddPlayerIfPossible(pPlayer, speciesId, conn, signalToCloseConnOfThisPlayer)
if !res { if Constants.RetCode.Ok != playerRoomRelation {
signalToCloseConnOfThisPlayer(Constants.RetCode.PlayerNotAddableToRoom, fmt.Sprintf("AddPlayerIfPossible returns false for roomId == %v, playerId == %v!", pRoom.Id, playerId)) signalToCloseConnOfThisPlayer(playerRoomRelation, fmt.Sprintf("AddPlayerIfPossible returns false for roomId == %v, playerId == %v!", pRoom.Id, playerId))
} }
} }
} }

BIN
charts/How-to-play-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
charts/How-to-play-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
charts/How-to-play-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
charts/How-to-play-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -12,8 +12,9 @@ func NormVec2D(dx, dy float64) Vec2D {
} }
func ConvexPolygonStr(body *resolv.ConvexPolygon) string { func ConvexPolygonStr(body *resolv.ConvexPolygon) string {
var s []string = make([]string, len(body.Points)) var s []string = make([]string, body.Points.Cnt)
for i, p := range body.Points { for i := int32(0); i < body.Points.Cnt; i++ {
p := body.GetPointByOffset(i)
s[i] = fmt.Sprintf("[%.2f, %.2f]", p[0]+body.X, p[1]+body.Y) s[i] = fmt.Sprintf("[%.2f, %.2f]", p[0]+body.X, p[1]+body.Y)
} }

View File

@@ -109,6 +109,7 @@ var constants = {
"GET_SMS_CAPTCHA_RESP_ERROR_CODE": 2023, "GET_SMS_CAPTCHA_RESP_ERROR_CODE": 2023,
"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 2024, "SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 2024,
"SMS_CAPTCHA_NOT_MATCH": 2025, "SMS_CAPTCHA_NOT_MATCH": 2025,
"SAME_PLAYER_ALREADY_IN_SAME_ROOM": 2026,
"NOT_IMPLEMENTED_YET": 65535 "NOT_IMPLEMENTED_YET": 65535
}, },

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?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="137"> <map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="9" nextobjectid="138">
<tileset firstgid="1" source="tiles0.tsx"/> <tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/> <tileset firstgid="65" source="tiles1.tsx"/>
<tileset firstgid="129" source="tiles2.tsx"/> <tileset firstgid="129" source="tiles2.tsx"/>
<layer id="6" name="Ground" width="128" height="64"> <layer id="8" name="Ground" width="128" height="64">
<data encoding="base64" compression="zlib"> <data encoding="base64" compression="zlib">
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= eJzt2j1uwkAQgFEESkNHlKRHyk2iNHRcIPc/RlCEJWSxtrHHmRX7itfw651v1zTsNpvNDgAAAAAAAOAffPMnu0Nm/+xryNbyDFpeuxm0vXYzaHvtZtD22s1g3trfJlpyXa9Br9G/vv6f+lej5v5Dfcee1z9+7V23qP4vF4c7+o1LMmbwbOb0Xyrqc/R//v737g+Hhd+v//z+Ucbu56Xua+wD/esytX3UPqhxBjX3Pwa9Rv98t2vfX2X3n7sH5u6P1vt/9XTP9R/vHCcqvb823QyyW2T131515//n4lSwDVA6oxGfPee7Wz///f63vwG3j0X1z6R/uf+p1/ue6B4fK3jknqP/cJ/9yOymGOsfvZ/0f6z/+9XQ3Jac/6E9sPb5n7I/9S/3j/zN1b8+2f+7rkV2h8z+58bp37Y1+x8qlz37WmR30F9//fXXX38AAAAAAAAAAAAg1y8jXMSV
</data> </data>
</layer> </layer>
<objectgroup id="1" name="PlayerStartingPos"> <objectgroup id="1" name="PlayerStartingPos">
@@ -84,12 +84,12 @@
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="84" x="640" y="224" width="16" height="800"> <object id="84" x="640" y="224" width="16" height="416">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="85" x="1680" y="224" width="16" height="800"> <object id="85" x="1680" y="224" width="16" height="416">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
@@ -149,17 +149,12 @@
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="113" x="640" y="1008" width="1056" height="16">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
<object id="114" x="640" y="224" width="1056" height="16"> <object id="114" x="640" y="224" width="1056" height="16">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="119" x="656" y="592" width="1024" height="416"> <object id="119" x="656" y="592" width="512" height="48">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
@@ -169,5 +164,10 @@
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
</object> </object>
<object id="137" x="1168" y="592" width="512" height="48">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
</object>
</objectgroup> </objectgroup>
</map> </map>

View File

@@ -22,6 +22,9 @@
}, },
{ {
"__id__": 5 "__id__": 5
},
{
"__id__": 11
} }
], ],
"_active": true, "_active": true,
@@ -59,7 +62,7 @@
"ctor": "Float64Array", "ctor": "Float64Array",
"array": [ "array": [
480, 480,
320, 480,
0, 0,
0, 0,
0, 0,
@@ -192,102 +195,28 @@
"fileId": "ab6G+s0otA4rXhUsO3czRN", "fileId": "ab6G+s0otA4rXhUsO3czRN",
"sync": false "sync": false
}, },
{
"__type__": "cc.Node",
"_name": "VerticalLayout",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 6
},
{
"__id__": 12
}
],
"_active": true,
"_components": [
{
"__id__": 53
},
{
"__id__": 54
}
],
"_prefab": {
"__id__": 55
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 960,
"height": 265
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
128,
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", "__type__": "cc.Node",
"_name": "exitButton", "_name": "exitButton",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 5 "__id__": 1
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{
"__id__": 6
},
{ {
"__id__": 7 "__id__": 7
}, },
{ {
"__id__": 8 "__id__": 9
},
{
"__id__": 10
} }
], ],
"_prefab": { "_prefab": {
"__id__": 11 "__id__": 10
}, },
"_opacity": 255, "_opacity": 255,
"_color": { "_color": {
@@ -312,7 +241,7 @@
"ctor": "Float64Array", "ctor": "Float64Array",
"array": [ "array": [
-379.577, -379.577,
100.5, 140,
0, 0,
0, 0,
0, 0,
@@ -341,7 +270,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 6 "__id__": 5
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -375,7 +304,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 6 "__id__": 5
}, },
"_enabled": true, "_enabled": true,
"_normalMaterial": null, "_normalMaterial": null,
@@ -384,7 +313,7 @@
"zoomScale": 1.2, "zoomScale": 1.2,
"clickEvents": [ "clickEvents": [
{ {
"__id__": 9 "__id__": 8
} }
], ],
"_N$interactable": true, "_N$interactable": true,
@@ -440,7 +369,7 @@
"hoverSprite": null, "hoverSprite": null,
"_N$disabledSprite": null, "_N$disabledSprite": null,
"_N$target": { "_N$target": {
"__id__": 6 "__id__": 5
}, },
"_id": "" "_id": ""
}, },
@@ -459,7 +388,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 6 "__id__": 5
}, },
"_enabled": true, "_enabled": true,
"alignMode": 1, "alignMode": 1,
@@ -489,15 +418,86 @@
"asset": { "asset": {
"__uuid__": "dc804c5c-ff76-445e-ac69-52269055c3c5" "__uuid__": "dc804c5c-ff76-445e-ac69-52269055c3c5"
}, },
"fileId": "1cUg34ZtdK9JfdGIG+lpdF", "fileId": "3cdlb7LxhMzLDzxdQv8Z/x",
"sync": false "sync": false
}, },
{
"__type__": "cc.Node",
"_name": "VerticalLayout",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 12
}
],
"_active": true,
"_components": [
{
"__id__": 53
},
{
"__id__": 54
}
],
"_prefab": {
"__id__": 55
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 960,
"height": 137
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
-13.057,
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", "__type__": "cc.Node",
"_name": "HorizontalLayout", "_name": "HorizontalLayout",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 5 "__id__": 11
}, },
"_children": [ "_children": [
{ {
@@ -545,7 +545,7 @@
"ctor": "Float64Array", "ctor": "Float64Array",
"array": [ "array": [
0, 0,
-64, 0,
0, 0,
0, 0,
0, 0,
@@ -1953,7 +1953,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 5 "__id__": 11
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1983,13 +1983,13 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 5 "__id__": 11
}, },
"_enabled": true, "_enabled": true,
"_layoutSize": { "_layoutSize": {
"__type__": "cc.Size", "__type__": "cc.Size",
"width": 960, "width": 960,
"height": 265 "height": 137
}, },
"_resize": 1, "_resize": 1,
"_N$layoutType": 2, "_N$layoutType": 2,
@@ -2040,7 +2040,7 @@
"__id__": 43 "__id__": 43
}, },
"exitBtnNode": { "exitBtnNode": {
"__id__": 6 "__id__": 5
}, },
"_id": "" "_id": ""
}, },

View File

@@ -400,7 +400,7 @@
"ctor": "Float64Array", "ctor": "Float64Array",
"array": [ "array": [
0, 0,
0, 32,
0, 0,
0, 0,
0, 0,
@@ -536,7 +536,7 @@
"array": [ "array": [
0, 0,
0, 0,
216.65450766436658, 210.43877906529718,
0, 0,
0, 0,
0, 0,
@@ -2129,7 +2129,7 @@
"_left": 0, "_left": 0,
"_right": 0, "_right": 0,
"_top": 0, "_top": 0,
"_bottom": 640, "_bottom": 672,
"_verticalCenter": 0, "_verticalCenter": 0,
"_horizontalCenter": 0, "_horizontalCenter": 0,
"_isAbsLeft": true, "_isAbsLeft": true,

View File

@@ -461,7 +461,7 @@
"array": [ "array": [
0, 0,
0, 0,
216.65450766436658, 209.57814771583418,
0, 0,
0, 0,
0, 0,

View File

@@ -72,11 +72,14 @@
"__id__": 3 "__id__": 3
}, },
{ {
"__id__": 10 "__id__": 9
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{
"__id__": 50
},
{ {
"__id__": 51 "__id__": 51
}, },
@@ -88,9 +91,6 @@
}, },
{ {
"__id__": 54 "__id__": 54
},
{
"__id__": 55
} }
], ],
"_prefab": null, "_prefab": null,
@@ -156,9 +156,6 @@
}, },
{ {
"__id__": 5 "__id__": 5
},
{
"__id__": 6
} }
], ],
"_prefab": null, "_prefab": null,
@@ -220,34 +217,6 @@
"_tmxFile": null, "_tmxFile": null,
"_id": "c8MqKDLJdKz7VhPwMjScDw" "_id": "c8MqKDLJdKz7VhPwMjScDw"
}, },
{
"__type__": "09e1b/tEy5K2qaPIpqHDbae",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"BGMEffect": {
"__uuid__": "64a79efa-97de-4cb5-b2a9-01500c60573a"
},
"crashedByTrapBullet": {
"__uuid__": "1d604e42-8cee-466f-884d-e74cae21ce3b"
},
"highScoreTreasurePicked": {
"__uuid__": "0164d22c-d965-461f-867e-b30e2d56cc5c"
},
"treasurePicked": {
"__uuid__": "7704b97e-6367-420c-b7af-d0750a2bbb30"
},
"countDown10SecToEnd": {
"__uuid__": "261d1d7d-a5cc-4cb7-a737-194427055fd4"
},
"mapNode": {
"__id__": 3
},
"_id": "3crA1nz5xPSLAnCSLQIPOq"
},
{ {
"__type__": "b3810kDSWtD14RhiYzulYVI", "__type__": "b3810kDSWtD14RhiYzulYVI",
"_name": "", "_name": "",
@@ -266,7 +235,7 @@
"__uuid__": "d92d4831-cd65-4eb5-90bd-b77021aec35b" "__uuid__": "d92d4831-cd65-4eb5-90bd-b77021aec35b"
}, },
"joystickInputControllerNode": { "joystickInputControllerNode": {
"__id__": 7 "__id__": 6
}, },
"confirmLogoutPrefab": null, "confirmLogoutPrefab": null,
"simplePressToGoDialogPrefab": null, "simplePressToGoDialogPrefab": null,
@@ -279,19 +248,19 @@
"forceBigEndianFloatingNumDecoding": false, "forceBigEndianFloatingNumDecoding": false,
"renderFrameIdLagTolerance": 4, "renderFrameIdLagTolerance": 4,
"inputFrameFrontLabel": { "inputFrameFrontLabel": {
"__id__": 14 "__id__": 13
}, },
"sendingQLabel": { "sendingQLabel": {
"__id__": 16 "__id__": 15
}, },
"inputFrameDownsyncQLabel": { "inputFrameDownsyncQLabel": {
"__id__": 18 "__id__": 17
}, },
"peerInputFrameUpsyncQLabel": { "peerInputFrameUpsyncQLabel": {
"__id__": 20 "__id__": 19
}, },
"rollbackFramesLabel": { "rollbackFramesLabel": {
"__id__": 22 "__id__": 21
}, },
"skippedRenderFrameCntLabel": null, "skippedRenderFrameCntLabel": null,
"_id": "e5xQdv12xLoIRr0b36Pie+" "_id": "e5xQdv12xLoIRr0b36Pie+"
@@ -301,17 +270,17 @@
"_name": "JoystickContainer", "_name": "JoystickContainer",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 8 "__id__": 7
}, },
"_children": [ "_children": [
{ {
"__id__": 45 "__id__": 44
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 50 "__id__": 49
} }
], ],
"_prefab": null, "_prefab": null,
@@ -367,26 +336,26 @@
"_name": "Interactive", "_name": "Interactive",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 9 "__id__": 8
}, },
"_children": [ "_children": [
{ {
"__id__": 7 "__id__": 6
}, },
{ {
"__id__": 34 "__id__": 33
}, },
{ {
"__id__": 36 "__id__": 35
}, },
{ {
"__id__": 40 "__id__": 39
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 44 "__id__": 43
} }
], ],
"_prefab": null, "_prefab": null,
@@ -413,7 +382,7 @@
"ctor": "Float64Array", "ctor": "Float64Array",
"array": [ "array": [
0, 0,
0, 32,
0, 0,
0, 0,
0, 0,
@@ -442,20 +411,20 @@
"_name": "WidgetsAboveAll", "_name": "WidgetsAboveAll",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 10 "__id__": 9
}, },
"_children": [ "_children": [
{ {
"__id__": 12 "__id__": 11
}, },
{ {
"__id__": 8 "__id__": 7
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 33 "__id__": 32
} }
], ],
"_prefab": null, "_prefab": null,
@@ -515,13 +484,13 @@
}, },
"_children": [ "_children": [
{ {
"__id__": 9 "__id__": 8
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 11 "__id__": 10
} }
], ],
"_prefab": null, "_prefab": null,
@@ -549,7 +518,7 @@
"array": [ "array": [
0, 0,
0, 0,
216.65450766436658, 210.4441731196186,
0, 0,
0, 0,
0, 0,
@@ -577,7 +546,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 10 "__id__": 9
}, },
"_enabled": true, "_enabled": true,
"_cullingMask": 4294967295, "_cullingMask": 4294967295,
@@ -613,35 +582,35 @@
"_name": "DebugInfo", "_name": "DebugInfo",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 9 "__id__": 8
}, },
"_children": [ "_children": [
{ {
"__id__": 13 "__id__": 12
}, },
{ {
"__id__": 15 "__id__": 14
}, },
{ {
"__id__": 17 "__id__": 16
}, },
{ {
"__id__": 19 "__id__": 18
}, },
{ {
"__id__": 21 "__id__": 20
}, },
{ {
"__id__": 23 "__id__": 22
}, },
{ {
"__id__": 25 "__id__": 24
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 32 "__id__": 31
} }
], ],
"_prefab": null, "_prefab": null,
@@ -697,13 +666,13 @@
"_name": "inputFrameFront", "_name": "inputFrameFront",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 14 "__id__": 13
} }
], ],
"_prefab": null, "_prefab": null,
@@ -759,7 +728,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 13 "__id__": 12
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -789,13 +758,13 @@
"_name": "sendingQ", "_name": "sendingQ",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 16 "__id__": 15
} }
], ],
"_prefab": null, "_prefab": null,
@@ -851,7 +820,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 15 "__id__": 14
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -881,13 +850,13 @@
"_name": "inputFrameDownsyncQ", "_name": "inputFrameDownsyncQ",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 18 "__id__": 17
} }
], ],
"_prefab": null, "_prefab": null,
@@ -943,7 +912,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 17 "__id__": 16
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -973,13 +942,13 @@
"_name": "peerInputFrameUpsyncQ", "_name": "peerInputFrameUpsyncQ",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 20 "__id__": 19
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1035,7 +1004,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 19 "__id__": 18
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1065,13 +1034,13 @@
"_name": "rollbackFrames", "_name": "rollbackFrames",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 22 "__id__": 21
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1127,7 +1096,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 21 "__id__": 20
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1157,13 +1126,13 @@
"_name": "skippedCnt", "_name": "skippedCnt",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 24 "__id__": 23
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1219,7 +1188,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 23 "__id__": 22
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1251,23 +1220,23 @@
"_name": "RoomIdIndicator", "_name": "RoomIdIndicator",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 12 "__id__": 11
}, },
"_children": [ "_children": [
{ {
"__id__": 26 "__id__": 25
}, },
{ {
"__id__": 28 "__id__": 27
} }
], ],
"_active": false, "_active": false,
"_components": [ "_components": [
{ {
"__id__": 30 "__id__": 29
}, },
{ {
"__id__": 31 "__id__": 30
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1323,13 +1292,13 @@
"_name": "label", "_name": "label",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 25 "__id__": 24
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 27 "__id__": 26
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1385,7 +1354,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 26 "__id__": 25
}, },
"_enabled": true, "_enabled": true,
"_materials": [], "_materials": [],
@@ -1413,13 +1382,13 @@
"_name": "BoundRoomIdLabel", "_name": "BoundRoomIdLabel",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 25 "__id__": 24
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 29 "__id__": 28
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1475,7 +1444,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 28 "__id__": 27
}, },
"_enabled": true, "_enabled": true,
"_materials": [], "_materials": [],
@@ -1503,7 +1472,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 25 "__id__": 24
}, },
"_enabled": true, "_enabled": true,
"_layoutSize": { "_layoutSize": {
@@ -1536,7 +1505,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 25 "__id__": 24
}, },
"_enabled": true, "_enabled": true,
"_materials": [], "_materials": [],
@@ -1566,7 +1535,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 12 "__id__": 11
}, },
"_enabled": true, "_enabled": true,
"_layoutSize": { "_layoutSize": {
@@ -1599,7 +1568,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 9 "__id__": 8
}, },
"_enabled": true, "_enabled": true,
"alignMode": 1, "alignMode": 1,
@@ -1626,13 +1595,13 @@
"_name": "CountdownSeconds", "_name": "CountdownSeconds",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 8 "__id__": 7
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 35 "__id__": 34
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1688,7 +1657,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 34 "__id__": 33
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1718,11 +1687,11 @@
"_name": "BtnA", "_name": "BtnA",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 8 "__id__": 7
}, },
"_children": [ "_children": [
{ {
"__id__": 37 "__id__": 36
} }
], ],
"_active": true, "_active": true,
@@ -1780,16 +1749,16 @@
"_name": "Background", "_name": "Background",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 36 "__id__": 35
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 38 "__id__": 37
}, },
{ {
"__id__": 39 "__id__": 38
} }
], ],
"_prefab": null, "_prefab": null,
@@ -1845,7 +1814,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 37 "__id__": 36
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -1879,7 +1848,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 37 "__id__": 36
}, },
"_enabled": true, "_enabled": true,
"alignMode": 0, "alignMode": 0,
@@ -1906,11 +1875,11 @@
"_name": "BtnB", "_name": "BtnB",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 8 "__id__": 7
}, },
"_children": [ "_children": [
{ {
"__id__": 41 "__id__": 40
} }
], ],
"_active": true, "_active": true,
@@ -1968,16 +1937,16 @@
"_name": "Background", "_name": "Background",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 40 "__id__": 39
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 42 "__id__": 41
}, },
{ {
"__id__": 43 "__id__": 42
} }
], ],
"_prefab": null, "_prefab": null,
@@ -2033,7 +2002,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 41 "__id__": 40
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -2067,7 +2036,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 41 "__id__": 40
}, },
"_enabled": true, "_enabled": true,
"alignMode": 0, "alignMode": 0,
@@ -2094,7 +2063,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 8 "__id__": 7
}, },
"_enabled": true, "_enabled": true,
"alignMode": 1, "alignMode": 1,
@@ -2121,20 +2090,20 @@
"_name": "JoystickBG", "_name": "JoystickBG",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 7 "__id__": 6
}, },
"_children": [ "_children": [
{ {
"__id__": 46 "__id__": 45
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 48 "__id__": 47
}, },
{ {
"__id__": 49 "__id__": 48
} }
], ],
"_prefab": null, "_prefab": null,
@@ -2190,13 +2159,13 @@
"_name": "Joystick", "_name": "Joystick",
"_objFlags": 0, "_objFlags": 0,
"_parent": { "_parent": {
"__id__": 45 "__id__": 44
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 47 "__id__": 46
} }
], ],
"_prefab": null, "_prefab": null,
@@ -2252,7 +2221,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 46 "__id__": 45
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -2286,7 +2255,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 45 "__id__": 44
}, },
"_enabled": true, "_enabled": true,
"_materials": [ "_materials": [
@@ -2320,7 +2289,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 45 "__id__": 44
}, },
"_enabled": true, "_enabled": true,
"alignMode": 0, "alignMode": 0,
@@ -2347,7 +2316,7 @@
"_name": "", "_name": "",
"_objFlags": 0, "_objFlags": 0,
"node": { "node": {
"__id__": 7 "__id__": 6
}, },
"_enabled": true, "_enabled": true,
"alignMode": 0, "alignMode": 0,
@@ -2449,16 +2418,16 @@
}, },
"_enabled": true, "_enabled": true,
"translationListenerNode": { "translationListenerNode": {
"__id__": 7 "__id__": 6
}, },
"zoomingListenerNode": { "zoomingListenerNode": {
"__id__": 3 "__id__": 3
}, },
"stickhead": { "stickhead": {
"__id__": 46 "__id__": 45
}, },
"base": { "base": {
"__id__": 45 "__id__": 44
}, },
"joyStickEps": 0.1, "joyStickEps": 0.1,
"magicLeanLowerBound": 0.414, "magicLeanLowerBound": 0.414,
@@ -2479,10 +2448,10 @@
"linearMovingEps": 0.1, "linearMovingEps": 0.1,
"scaleByEps": 0.0375, "scaleByEps": 0.0375,
"btnA": { "btnA": {
"__id__": 36 "__id__": 35
}, },
"btnB": { "btnB": {
"__id__": 40 "__id__": 39
}, },
"_id": "e9oVYTr7ROlpp/IrNjBUmR" "_id": "e9oVYTr7ROlpp/IrNjBUmR"
} }

View File

@@ -64,9 +64,6 @@ cc.Class({
ctor() { ctor() {
this.speciesName = null; this.speciesName = null;
this.hp = 100;
this.maxHp = 100;
this.inAir = true;
}, },
setSpecies(speciesName) { setSpecies(speciesName) {
@@ -86,17 +83,17 @@ cc.Class({
updateCharacterAnim(rdfPlayer, prevRdfPlayer, forceAnimSwitch, chConfig) { updateCharacterAnim(rdfPlayer, prevRdfPlayer, forceAnimSwitch, chConfig) {
// As this function might be called after many frames of a rollback, it's possible that the playing animation was predicted, different from "prevRdfPlayer.CharacterState" but same as "newCharacterState". More granular checks are needed to determine whether we should interrupt the playing animation. // As this function might be called after many frames of a rollback, it's possible that the playing animation was predicted, different from "prevRdfPlayer.CharacterState" but same as "newCharacterState". More granular checks are needed to determine whether we should interrupt the playing animation.
let newCharacterState = rdfPlayer.CharacterState; let newCharacterState = rdfPlayer.GetCharacterState();
// Update directions // Update directions
if (this.animComp && this.animComp.node) { if (this.animComp && this.animComp.node) {
if (0 > rdfPlayer.DirX) { if (0 > rdfPlayer.GetDirX()) {
this.animNode.scaleX = (-1.0); this.animNode.scaleX = (-1.0);
} else if (0 < rdfPlayer.DirX) { } else if (0 < rdfPlayer.GetDirX()) {
this.animNode.scaleX = (+1.0); this.animNode.scaleX = (+1.0);
} }
if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState || ATK_CHARACTER_STATE.TurnAround1[0] == newCharacterState) { if (ATK_CHARACTER_STATE.OnWall[0] == newCharacterState || ATK_CHARACTER_STATE.TurnAround1[0] == newCharacterState) {
if (0 < rdfPlayer.OnWallNormX) { if (0 < rdfPlayer.GetOnWallNormX()) {
this.animNode.scaleX = (-1.0); this.animNode.scaleX = (-1.0);
} else { } else {
this.animNode.scaleX = (+1.0); this.animNode.scaleX = (+1.0);
@@ -116,7 +113,6 @@ cc.Class({
playingAnimName = (!underlyingAnimationCtrl ? null : underlyingAnimationCtrl.name); 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)) { if (newAnimName == playingAnimName && window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) {
// No need to interrupt // No need to interrupt
// console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, to: ${JSON.stringify(rdfPlayer)}`); // console.warn(`JoinIndex=${rdfPlayer.joinIndex}, not interrupting ${newAnimName} while the playing anim is also ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, to: ${JSON.stringify(rdfPlayer)}`);
@@ -151,7 +147,7 @@ cc.Class({
} }
// The "playTimes" counterpart is managed by each "cc.AnimationClip.wrapMode", already preset in the editor. // The "playTimes" counterpart is managed by each "cc.AnimationClip.wrapMode", already preset in the editor.
const targetClip = this.animComp.getClips()[newCharacterState]; // The clips follow the exact order in ATK_CHARACTER_STATE const targetClip = this.animComp.getClips()[newCharacterState]; // The clips follow the exact order in ATK_CHARACTER_STATE
let frameIdxInAnim = rdfPlayer.FramesInChState; let frameIdxInAnim = rdfPlayer.GetFramesInChState();
if (window.ATK_CHARACTER_STATE.InAirIdle1ByJump == newCharacterState && null != chConfig) { if (window.ATK_CHARACTER_STATE.InAirIdle1ByJump == newCharacterState && null != chConfig) {
frameIdxInAnim = chConfig.InAirIdleFrameIdxTurningPoint + (frameIdxInAnim - chConfig.InAirIdleFrameIdxTurningPoint) % chConfig.InAirIdleFrameIdxTurnedCycle; // TODO: Anyway to avoid using division here? frameIdxInAnim = chConfig.InAirIdleFrameIdxTurningPoint + (frameIdxInAnim - chConfig.InAirIdleFrameIdxTurningPoint) % chConfig.InAirIdleFrameIdxTurnedCycle; // TODO: Anyway to avoid using division here?
} }

View File

@@ -25,7 +25,7 @@ cc.Class({
if (!self.mapScriptIns) return; if (!self.mapScriptIns) return;
if (!self.mapScriptIns.selfPlayerInfo) return; if (!self.mapScriptIns.selfPlayerInfo) return;
if (!self.mapScriptIns.playerRichInfoDict) return; if (!self.mapScriptIns.playerRichInfoDict) return;
const selfPlayerRichInfo = self.mapScriptIns.playerRichInfoDict.get(self.mapScriptIns.selfPlayerInfo.Id); const selfPlayerRichInfo = self.mapScriptIns.playerRichInfoDict.get(self.mapScriptIns.selfPlayerInfo.id);
if (!selfPlayerRichInfo) return; if (!selfPlayerRichInfo) return;
const selfPlayerNode = selfPlayerRichInfo.node; const selfPlayerNode = selfPlayerRichInfo.node;
if (!selfPlayerNode) return; if (!selfPlayerNode) return;

View File

@@ -13,9 +13,9 @@ cc.Class({
if (speciesName == this.speciesName) return; if (speciesName == this.speciesName) return;
if (null != this.speciesName) { if (null != this.speciesName) {
for (let k in this.animNode.children) { for (let k in this.animNode.children) {
const child = this.children[k]; const child = this.animNode.children[k];
if (!child.active) continue; if (!child.active) continue;
if (child == effAnimNode || child.name == speciesName) continue; if (child == this.effAnimNode || child.name == speciesName) continue;
child.active = false; child.active = false;
} }
} }

View File

@@ -337,6 +337,7 @@ cc.Class({
const date = Number(res.expiresAt); const date = Number(res.expiresAt);
const selfPlayer = { const selfPlayer = {
expiresAt: date, expiresAt: date,
id: res.playerId,
playerId: res.playerId, playerId: res.playerId,
intAuthToken: res.intAuthToken, intAuthToken: res.intAuthToken,
avatar: res.avatar, avatar: res.avatar,

File diff suppressed because it is too large Load Diff

View File

@@ -1,66 +0,0 @@
cc.Class({
extends: cc.Component,
properties: {
BGMEffect: {
type: cc.AudioClip,
default: null
},
crashedByTrapBullet: {
type: cc.AudioClip,
default: null
},
highScoreTreasurePicked: {
type: cc.AudioClip,
default: null
},
treasurePicked: {
type: cc.AudioClip,
default: null
},
countDown10SecToEnd: {
type: cc.AudioClip,
default: null
},
mapNode: {
type: cc.Node,
default: null
},
},
// LIFE-CYCLE CALLBACKS:
onLoad() {
cc.audioEngine.setEffectsVolume(1);
cc.audioEngine.setMusicVolume(0.5);
},
stopAllMusic() {
cc.audioEngine.stopAll();
},
playBGM() {
if(this.BGMEffect) {
cc.audioEngine.playMusic(this.BGMEffect, true);
}
},
playCrashedByTrapBullet() {
if(this.crashedByTrapBullet) {
cc.audioEngine.playEffect(this.crashedByTrapBullet, false);
}
},
playHighScoreTreasurePicked() {
if(this.highScoreTreasurePicked) {
cc.audioEngine.playEffect(this.highScoreTreasurePicked, false);
}
},
playTreasurePicked() {
if(this.treasurePicked) {
cc.audioEngine.playEffect(this.treasurePicked, false);
}
},
playCountDown10SecToEnd() {
if(this.countDown10SecToEnd) {
cc.audioEngine.playEffect(this.countDown10SecToEnd, false);
}
},
});

View File

@@ -1,9 +0,0 @@
{
"ver": "1.0.5",
"uuid": "09e1bfed-132e-4ada-a68f-229a870db69e",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -89,23 +89,28 @@ NetworkDoctor.prototype.isTooFast = function(mapIns) {
const [inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats(); const [inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt] = this.stats();
if (sendingFps >= this.inputRateThreshold + 3) { if (sendingFps >= this.inputRateThreshold + 3) {
// Don't send too fast // Don't send too fast
console.log(`Sending too fast, sendingFps=${sendingFps}`); if (CC_DEBUG) {
// Printing of this message might induce a performance impact.
// console.log(`Sending too fast, sendingFps=${sendingFps}`);
}
return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt]; return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt];
} else { } else {
const sendingFpsNormal = (sendingFps >= this.inputRateThreshold); 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". // 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)); const recvFpsNormal = (srvDownsyncFps >= this.inputRateThreshold || peerUpsyncFps >= this.inputRateThreshold * (window.boundRoomCapacity - 1));
if (sendingFpsNormal && recvFpsNormal) { if (sendingFpsNormal && recvFpsNormal) {
let selfInputFrameIdFront = gopkgs.ConvertToNoDelayInputFrameId(mapIns.renderFrameId);
let minInputFrameIdFront = Number.MAX_VALUE; let minInputFrameIdFront = Number.MAX_VALUE;
for (let k = 0; k < window.boundRoomCapacity; ++k) { for (let k = 0; k < window.boundRoomCapacity; ++k) {
if (k + 1 == mapIns.selfPlayerInfo.JoinIndex) continue; if (k + 1 == mapIns.selfPlayerInfo.JoinIndex) continue;
if (mapIns.lastIndividuallyConfirmedInputFrameId[k] >= minInputFrameIdFront) continue; if (mapIns.lastIndividuallyConfirmedInputFrameId[k] >= minInputFrameIdFront) continue;
minInputFrameIdFront = mapIns.lastIndividuallyConfirmedInputFrameId[k]; minInputFrameIdFront = mapIns.lastIndividuallyConfirmedInputFrameId[k];
} }
if ((selfInputFrameIdFront > minInputFrameIdFront) && ((selfInputFrameIdFront - minInputFrameIdFront) > (mapIns.inputFrameUpsyncDelayTolerance + 1))) { if ((inputFrameIdFront > minInputFrameIdFront) && ((inputFrameIdFront - minInputFrameIdFront) > (mapIns.inputFrameUpsyncDelayTolerance + 1))) {
// first comparison condition is to avoid numeric overflow // first comparison condition is to avoid numeric overflow
console.log(`Game logic ticking too fast, selfInputFrameIdFront=${selfInputFrameIdFront}, minInputFrameIdFront=${minInputFrameIdFront}, inputFrameUpsyncDelayTolerance=${mapIns.inputFrameUpsyncDelayTolerance}`); if (CC_DEBUG) {
// Printing of this message might induce a performance impact.
// console.log(`Game logic ticking too fast, selfInputFrameIdFront=${inputFrameIdFront}, minInputFrameIdFront=${minInputFrameIdFront}, inputFrameUpsyncDelayTolerance=${mapIns.inputFrameUpsyncDelayTolerance}`);
}
return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt]; return [true, inputFrameIdFront, sendingFps, srvDownsyncFps, peerUpsyncFps, rollbackFrames, skippedRenderFrameCnt];
} }
} }

View File

@@ -11,13 +11,13 @@ cc.Class({
}, },
onLoad() { onLoad() {
cc.game.setFrameRate(60); cc.game.setFrameRate(59.9);
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE); cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
cc.view.enableAutoFullScreen(true); cc.view.enableAutoFullScreen(true);
const self = this; const self = this;
window.mapIns = self; window.mapIns = self;
self.showCriticalCoordinateLabels = false; self.showCriticalCoordinateLabels = false;
self.showNetworkDoctorInfo = true; self.showNetworkDoctorInfo = false;
const mapNode = self.node; const mapNode = self.node;
const canvasNode = mapNode.parent; const canvasNode = mapNode.parent;
@@ -33,9 +33,9 @@ cc.Class({
/** Init required prefab ended. */ /** Init required prefab ended. */
self.inputFrameUpsyncDelayTolerance = 2; self.inputFrameUpsyncDelayTolerance = 2;
self.collisionMinStep = 2; self.collisionMinStep = 16;
self.renderCacheSize = 1024; self.renderCacheSize = 128;
self.serverFps = 60; self.serverFps = 60;
self.rollbackEstimatedDt = 0.016667; self.rollbackEstimatedDt = 0.016667;
self.rollbackEstimatedDtMillis = 16.667; self.rollbackEstimatedDtMillis = 16.667;
@@ -73,6 +73,7 @@ cc.Class({
self.spaceOffsetX = ((newMapSize.width * newTileSize.width) >> 1); self.spaceOffsetX = ((newMapSize.width * newTileSize.width) >> 1);
self.spaceOffsetY = ((newMapSize.height * newTileSize.height) >> 1); self.spaceOffsetY = ((newMapSize.height * newTileSize.height) >> 1);
window.boundRoomCapacity = 2;
self._resetCurrentMatch(); self._resetCurrentMatch();
let barrierIdCounter = 0; let barrierIdCounter = 0;
const boundaryObjs = tileCollisionManager.extractBoundaryObjects(self.node); const boundaryObjs = tileCollisionManager.extractBoundaryObjects(self.node);
@@ -98,7 +99,7 @@ cc.Class({
const p2Vpos = gopkgs.WorldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y); const p2Vpos = gopkgs.WorldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y);
const colliderRadiusV = gopkgs.WorldToVirtualGridPos(12.0, 0); const colliderRadiusV = gopkgs.WorldToVirtualGridPos(12.0, 0);
const speciesIdList = [4096, 0]; const speciesIdList = [1, 0];
const chConfigsOrderedByJoinIndex = gopkgs.GetCharacterConfigsOrderedByJoinIndex(speciesIdList); const chConfigsOrderedByJoinIndex = gopkgs.GetCharacterConfigsOrderedByJoinIndex(speciesIdList);
const startRdf = window.pb.protos.RoomDownsyncFrame.create({ const startRdf = window.pb.protos.RoomDownsyncFrame.create({
@@ -111,7 +112,7 @@ cc.Class({
virtualGridY: p1Vpos[1], virtualGridY: p1Vpos[1],
revivalVirtualGridX: p1Vpos[0], revivalVirtualGridX: p1Vpos[0],
revivalVirtualGridY: p1Vpos[1], revivalVirtualGridY: p1Vpos[1],
speed: chConfigsOrderedByJoinIndex[0].Speed, speed: chConfigsOrderedByJoinIndex[0].GetSpeed(),
colliderRadius: colliderRadiusV[0], colliderRadius: colliderRadiusV[0],
characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0], characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0],
framesToRecover: 0, framesToRecover: 0,
@@ -131,7 +132,7 @@ cc.Class({
virtualGridY: p2Vpos[1], virtualGridY: p2Vpos[1],
revivalVirtualGridX: p2Vpos[0], revivalVirtualGridX: p2Vpos[0],
revivalVirtualGridY: p2Vpos[1], revivalVirtualGridY: p2Vpos[1],
speed: chConfigsOrderedByJoinIndex[1].Speed, speed: chConfigsOrderedByJoinIndex[1].GetSpeed(),
colliderRadius: colliderRadiusV[0], colliderRadius: colliderRadiusV[0],
characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0], characterState: window.ATK_CHARACTER_STATE.InAirIdle1NoJump[0],
framesToRecover: 0, framesToRecover: 0,
@@ -149,8 +150,8 @@ cc.Class({
}); });
self.selfPlayerInfo = { self.selfPlayerInfo = {
Id: 10, id: 10,
JoinIndex: 1, joinIndex: 1,
}; };
if (cc.sys.isNative) { if (cc.sys.isNative) {
window.onUdpMessage = (args) => { window.onUdpMessage = (args) => {
@@ -163,7 +164,7 @@ cc.Class({
const echoed = window.pb.protos.HolePunchUpsync.decode(ui8Arr); const echoed = window.pb.protos.HolePunchUpsync.decode(ui8Arr);
cc.log(`#2 Js called back by CPP: onUdpMessage: ${JSON.stringify(echoed)}`); cc.log(`#2 Js called back by CPP: onUdpMessage: ${JSON.stringify(echoed)}`);
}; };
const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + self.selfPlayerInfo.JoinIndex); const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + self.selfPlayerInfo.joinIndex);
const holePunchData = window.pb.protos.HolePunchUpsync.encode({ const holePunchData = window.pb.protos.HolePunchUpsync.encode({
boundRoomId: 22, boundRoomId: 22,
intAuthToken: "foobar", intAuthToken: "foobar",

View File

@@ -87,8 +87,8 @@ window.handleHbRequirements = function(resp) {
if (constants.RET_CODE.OK != resp.ret) return; if (constants.RET_CODE.OK != resp.ret) return;
// The assignment of "window.mapIns" is inside "Map.onLoad", which precedes "initPersistentSessionClient". // 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 = 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.id = window.mapIns.selfPlayerInfo.playerId;
window.mapIns.selfPlayerInfo.JoinIndex = resp.peerJoinIndex; window.mapIns.selfPlayerInfo.joinIndex = resp.peerJoinIndex;
console.log(`Handle hb requirements #2`); console.log(`Handle hb requirements #2`);
if (null == window.boundRoomId || null == window.boundRoomCapacity) { if (null == window.boundRoomId || null == window.boundRoomCapacity) {
window.boundRoomId = resp.bciFrame.boundRoomId; window.boundRoomId = resp.bciFrame.boundRoomId;
@@ -109,7 +109,7 @@ window.handleHbRequirements = function(resp) {
window.initSecondarySession(null, window.boundRoomId); window.initSecondarySession(null, window.boundRoomId);
} else { } else {
console.log(`Handle hb requirements #5, native, bciFrame.battleUdpTunnel=${resp.bciFrame.battleUdpTunnel}, selfPlayerInfo=${JSON.stringify(window.mapIns.selfPlayerInfo)}`); 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); const res1 = DelayNoMore.UdpSession.openUdpSession(8888 + window.mapIns.selfPlayerInfo.joinIndex);
window.mapIns.selfPlayerInfo.udpTunnelAuthKey = resp.bciFrame.battleUdpTunnel.authKey; window.mapIns.selfPlayerInfo.udpTunnelAuthKey = resp.bciFrame.battleUdpTunnel.authKey;
const intAuthToken = window.mapIns.selfPlayerInfo.intAuthToken; const intAuthToken = window.mapIns.selfPlayerInfo.intAuthToken;
const authKey = Math.floor(Math.random() * 65535); const authKey = Math.floor(Math.random() * 65535);
@@ -121,7 +121,7 @@ window.handleHbRequirements = function(resp) {
}).finish(); }).finish();
const udpTunnelHolePunchData = window.pb.protos.WsReq.encode({ const udpTunnelHolePunchData = window.pb.protos.WsReq.encode({
msgId: Date.now(), msgId: Date.now(),
playerId: window.mapIns.selfPlayerInfo.Id, playerId: window.mapIns.selfPlayerInfo.id,
act: window.UPSYNC_MSG_ACT_PLAYER_CMD, act: window.UPSYNC_MSG_ACT_PLAYER_CMD,
authKey: resp.bciFrame.battleUdpTunnel.authKey, authKey: resp.bciFrame.battleUdpTunnel.authKey,
}).finish(); }).finish();
@@ -250,7 +250,7 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
console.log(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR peerAddrList=${JSON.stringify(peerAddrList)}; boundRoomCapacity=${window.boundRoomCapacity}`); console.log(`Got DOWNSYNC_MSG_ACT_PEER_UDP_ADDR peerAddrList=${JSON.stringify(peerAddrList)}; boundRoomCapacity=${window.boundRoomCapacity}`);
for (let j = 0; j < 3; ++j) { for (let j = 0; j < 3; ++j) {
setTimeout(() => { 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" 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); }, j * 500);
} }
} }
@@ -288,13 +288,21 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
`); `);
} }
break; break;
case constants.RET_CODE.SAME_PLAYER_ALREADY_IN_SAME_ROOM:
mapIns.popupSimplePressToGo("You just logged into a conflicting account, please use a different account to retry", false, () => {
window.clearLocalStorageAndBackToLoginScene(true);
});
break;
case constants.RET_CODE.PLAYER_NOT_ADDABLE_TO_ROOM: case constants.RET_CODE.PLAYER_NOT_ADDABLE_TO_ROOM:
case constants.RET_CODE.PLAYER_NOT_READDABLE_TO_ROOM: case constants.RET_CODE.PLAYER_NOT_READDABLE_TO_ROOM:
window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); // To favor the player to join other rooms mapIns.popupSimplePressToGo("Couldn't join any room at the moment, please retry", false, () => {
mapIns.onManualRejoinRequired("Couldn't join any room at the moment, please retry"); window.clearLocalStorageAndBackToLoginScene(true);
});
break; break;
case constants.RET_CODE.ACTIVE_WATCHDOG: case constants.RET_CODE.ACTIVE_WATCHDOG:
mapIns.onManualRejoinRequired("Disconnected due to long-time inactivity, please rejoin"); mapIns.popupSimplePressToGo("Couldn't join any room at the moment, please retry", false, () => {
window.clearLocalStorageAndBackToLoginScene(true);
});
break; break;
case constants.RET_CODE.UNKNOWN_ERROR: case constants.RET_CODE.UNKNOWN_ERROR:
case constants.RET_CODE.MYSQL_ERROR: case constants.RET_CODE.MYSQL_ERROR:
@@ -305,9 +313,19 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()} console.warn(`${mapIns._stringifyRdfIdToActuallyUsedInput()}
`); `);
} }
window.clearLocalStorageAndBackToLoginScene(true); mapIns.popupSimplePressToGo("Disconnected unexpectedly, please retry", false, () => {
break; window.clearLocalStorageAndBackToLoginScene(true);
});
default: default:
if (cc.sys.isNative) {
// [WARNING] This could be a BUG in CocosCreator JSB implementation of WebSocket client, the "evt.code" is always "undefined" in the "onclose" callback!
if (window.ALL_BATTLE_STATES.IN_SETTLEMENT != mapIns.battleState && window.ALL_BATTLE_STATES.IN_DISMISSAL != mapIns.battleState) {
mapIns.popupSimplePressToGo("Disconnected unexpectedly, please retry", false, () => {
window.clearLocalStorageAndBackToLoginScene(true);
});
}
}
break; break;
} }
}; };

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{ {
"ver": "1.0.5", "ver": "1.0.5",
"uuid": "eeaa56f4-bd6c-4208-bec4-6ab1aa39ca93", "uuid": "02c5cdc1-9797-49ab-bc11-963215909926",
"isPlugin": true, "isPlugin": true,
"loadPluginInWeb": true, "loadPluginInWeb": true,
"loadPluginInNative": true, "loadPluginInNative": true,

View File

@@ -32,35 +32,60 @@ SendWork* SendRingBuff::pop() {
} }
// Recving // Recving
bool isFullWithLoadedVals(int n, int oldCnt, int oldSt, int oldEd) {
return (n <= oldCnt && oldEd == oldSt) || (n > oldCnt && 0 < oldCnt && oldEd == oldSt);
}
void RecvRingBuff::put(char* newBytes, size_t newBytesLen) { void RecvRingBuff::put(char* newBytes, size_t newBytesLen) {
RecvWork* slotEle = (&eles[ed.load()]); // Save for later update RecvWork* slotEle = (&eles[ed.load()]); // Save for later update
// "RecvRingBuff.ed" is only accessed in "UvRecvThread", thus the order of it relative to the other two is not important.
int oldEd = ed.load();
// We want to increase the success rate of "pop()" if it's being executed by "GameThread/pollUdpRecvRingBuff", thus the below order of loading is IMPORTANT, i.e. load "cnt" first because it's decremented earlier than "st" being incremented.
int oldCnt = cnt.load(); int oldCnt = cnt.load();
/*
[WARNING]
Note that "RecvRingBuff.st" might have decremented in "GameThread" by a successful "pop()" between "cnt.load()" and "st.load()" here in "UvRecvThread"! Therefore "n <= oldCnt" doesn't necessarily imply "oldEd == oldSt"!
*/
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 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; int tried = 0;
while (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 > tried) { /*
1. When "n <= oldCnt", it might still be true "oldEd != oldSt" (see the note above);
2. When "n > oldCnt", it might still be true that "oldEd == oldSt" if "pop()" hasn't successfully incremented "st" due to any reason;
3. When "oldEd == oldSt", it doesn't imply anything useful, because any of the following could be true
- a. "n <= oldCnt", i.e. the ringbuff is full
- b. "n > oldCnt && 0 < oldCnt" during the execution of "pop()", i.e. the ringbuff is still effectively full
- c. "n > oldCnt && 0 == oldCnt", i.e. the ringbuff is empty
*/
bool isFull = isFullWithLoadedVals(n, oldCnt, oldSt, oldEd);
while (isFull && 3 > tried) {
// Make room for the new element // Make room for the new element
this->pop(NULL); 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 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(); oldSt = st.load();
isFull = isFullWithLoadedVals(n, oldCnt, oldSt, oldEd);
++tried; ++tried;
} }
if (n <= oldCnt && !ed.compare_exchange_weak(oldSt, oldSt) && 3 == tried) { if (isFull && 3 == tried) {
// Failed silently, UDP packet can be dropped. // Failed silently, UDP packet can be dropped.
return; return;
} }
slotEle->bytesLen = newBytesLen; slotEle->bytesLen = newBytesLen;
memset(slotEle->ui8Arr, 0, sizeof slotEle->ui8Arr); memset(slotEle->ui8Arr, 0, sizeof slotEle->ui8Arr);
for (int i = 0; i < newBytesLen; i++) { for (size_t i = 0; i < newBytesLen; i++) {
*(slotEle->ui8Arr + i) = *(newBytes + i); *(slotEle->ui8Arr + i) = *(newBytes + i);
} }
// No need to compare-and-swap, only "UvRecvThread" will access "RecvRingBuff.ed". // No need to compare-and-swap, only "UvRecvThread" will access "RecvRingBuff.ed".
ed++; int newEd = oldEd+1;
if (ed >= n) { if (newEd >= n) {
ed -= n; // Deliberately not using "%" operator for performance concern newEd -= n; // Deliberately not using "%" operator for performance concern
} }
ed.compare_exchange_weak(oldEd, newEd); // Definitely succeeds because "RecvRingBuff.ed" is only accessed in "UvRecvThread"
// Only increment cnt when the putting of new element is fully done. // Only increment cnt when the putting of new element is fully done.
cnt++; cnt++;
} }
@@ -75,7 +100,7 @@ bool RecvRingBuff::pop(RecvWork* out) {
2. If "0 >= oldCnt", we need guard against another "pop" to avoid over-popping. 2. If "0 >= oldCnt", we need guard against another "pop" to avoid over-popping.
*/ */
if (0 >= oldCnt) { 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" // "pop" could be accessed by either "GameThread/pollUdpRecvRingBuff" or "UvRecvThread/put", thus we should be proactively guarding against concurrent popping while "1 == cnt"
++cnt; ++cnt;
return false; return false;
} }

View File

@@ -160,7 +160,6 @@ void startRecvLoop(void* arg) {
int uvCloseRet = uv_loop_close(l); int uvCloseRet = uv_loop_close(l);
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet); CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
uv_mutex_destroy(&recvRingBuffLock);
} }
void startSendLoop(void* arg) { void startSendLoop(void* arg) {
@@ -174,7 +173,6 @@ void startSendLoop(void* arg) {
int uvCloseRet = uv_loop_close(l); int uvCloseRet = uv_loop_close(l);
CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet); CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet);
uv_mutex_destroy(&sendRingBuffLock);
} }
int initSendLoop(struct sockaddr const* pUdpAddr) { int initSendLoop(struct sockaddr const* pUdpAddr) {
@@ -189,9 +187,6 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
uv_mutex_init(&sendRingBuffLock); uv_mutex_init(&sendRingBuffLock);
sendRingBuff = new SendRingBuff(maxBuffedMsgs); sendRingBuff = new SendRingBuff(maxBuffedMsgs);
uv_mutex_init(&recvRingBuffLock);
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig); uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend); uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
@@ -208,6 +203,9 @@ bool initRecvLoop(struct sockaddr const* pUdpAddr) {
CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes)); CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes));
exit(-1); exit(-1);
} }
uv_mutex_init(&recvRingBuffLock);
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead); uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead);
uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig); uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig);
@@ -248,20 +246,41 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
bool DelayNoMore::UdpSession::closeUdpSession() { bool DelayNoMore::UdpSession::closeUdpSession() {
CCLOG("About to close udp session and dealloc all resources..."); CCLOG("About to close udp session and dealloc all resources...");
/*
[WARNING] It's possible that "closeUdpSession" is called when "openUdpSession" was NEVER CALLED, thus we have to avoid program crash in this case.
uv_async_send(&uvSendLoopStopSig); In general one shouldn't just check the state of "sendTid" by whether or not "NULL == sendLoop", but in this particular game, both "openUdpSession" and "closeUdpSession" are only called from "GameThread", no thread-safety concern here, i.e. if "openUdpSession" was ever called earlier, then "sendLoop" wouldn't be NULL when "closeUdpSession" is later called.
CCLOG("Signaling UvSendThread to end in GameThread..."); */
uv_thread_join(&sendTid); if (NULL != sendLoop) {
free(udpSendSocket); uv_async_send(&uvSendLoopStopSig);
free(sendLoop); CCLOG("Signaling UvSendThread to end in GameThread...");
delete sendRingBuff; uv_thread_join(&sendTid);
free(udpSendSocket);
free(sendLoop);
delete sendRingBuff;
udpSendSocket = NULL;
sendLoop = NULL;
sendRingBuff = NULL;
uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send uv_mutex_destroy(&sendRingBuffLock);
CCLOG("Signaling UvRecvThread to end in GameThread..."); }
uv_thread_join(&recvTid);
free(udpRecvSocket); if (NULL != recvLoop) {
free(recvLoop); uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send
delete recvRingBuff; CCLOG("Signaling UvRecvThread to end in GameThread...");
uv_thread_join(&recvTid);
free(udpRecvSocket);
free(recvLoop);
delete recvRingBuff;
udpRecvSocket = NULL;
recvLoop = NULL;
recvRingBuff = NULL;
uv_mutex_destroy(&recvRingBuffLock);
}
CCLOG("Closed udp session and dealloc all resources in GameThread..."); CCLOG("Closed udp session and dealloc all resources in GameThread...");
@@ -353,11 +372,13 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
// This function is called by GameThread 60 fps. // This function is called by GameThread 60 fps.
//uv_mutex_lock(&recvRingBuffLock); //uv_mutex_lock(&recvRingBuffLock);
while (true) { while (true && NULL != recvLoop) {
RecvWork f; RecvWork f;
bool res = recvRingBuff->pop(&f); bool res = recvRingBuff->pop(&f);
if (!res) return false; if (!res) {
// Deliberately returning "true" here to prevent "jswrapper" from printing "Failed to invoke Xxx..." too frequently
return true;
}
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine! // [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
se::AutoHandleScope hs; se::AutoHandleScope hs;
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"! // [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
@@ -381,4 +402,4 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
} }
//uv_mutex_unlock(&recvRingBuffLock); //uv_mutex_unlock(&recvRingBuffLock);
return true; return true;
} }

View File

@@ -27,8 +27,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask" android:launchMode="singleTask">
android:taskAffinity="" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -76,7 +76,7 @@
"shelter_z_reducer", "shelter_z_reducer",
"shelter" "shelter"
], ],
"last-module-event-record-time": 1675852779064, "last-module-event-record-time": 1680159688511,
"simulator-orientation": false, "simulator-orientation": false,
"simulator-resolution": { "simulator-resolution": {
"height": 640, "height": 640,

View File

@@ -7,7 +7,7 @@ import (
) )
const ( const (
MAX_FLOAT64 = 1.7e+308 MAX_FLOAT64 = resolv.MaxFloat64
MAX_INT32 = int32(999999999) MAX_INT32 = int32(999999999)
COLLISION_PLAYER_INDEX_PREFIX = (1 << 17) COLLISION_PLAYER_INDEX_PREFIX = (1 << 17)
COLLISION_BARRIER_INDEX_PREFIX = (1 << 16) COLLISION_BARRIER_INDEX_PREFIX = (1 << 16)
@@ -16,13 +16,13 @@ const (
PATTERN_ID_UNABLE_TO_OP = -2 PATTERN_ID_UNABLE_TO_OP = -2
PATTERN_ID_NO_OP = -1 PATTERN_ID_NO_OP = -1
WORLD_TO_VIRTUAL_GRID_RATIO = float64(100.0) WORLD_TO_VIRTUAL_GRID_RATIO = float64(10.0)
VIRTUAL_GRID_TO_WORLD_RATIO = float64(1.0) / WORLD_TO_VIRTUAL_GRID_RATIO VIRTUAL_GRID_TO_WORLD_RATIO = float64(1.0) / WORLD_TO_VIRTUAL_GRID_RATIO
GRAVITY_X = int32(0) GRAVITY_X = int32(0)
GRAVITY_Y = -int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO) // makes all "playerCollider.Y" a multiple of 0.5 in all cases GRAVITY_Y = -int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO) // makes all "playerCollider.Y" a multiple of 0.5 in all cases
INPUT_DELAY_FRAMES = int32(6) // in the count of render frames INPUT_DELAY_FRAMES = int32(4) // in the count of render frames
/* /*
[WARNING] [WARNING]
@@ -43,6 +43,11 @@ const (
NO_SKILL_HIT = -1 NO_SKILL_HIT = -1
NO_LOCK_VEL = int32(-1) NO_LOCK_VEL = int32(-1)
// Used in preallocated RoomDownsyncFrame to check termination
TERMINATING_BULLET_LOCAL_ID = int32(-1)
TERMINATING_PLAYER_ID = int32(-1)
TERMINATING_RENDER_FRAME_ID = int32(-1)
) )
// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged. // These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged.
@@ -96,6 +101,7 @@ var inAirSet = map[int32]bool{
ATK_CHARACTER_STATE_INAIR_ATKED1: true, ATK_CHARACTER_STATE_INAIR_ATKED1: true,
ATK_CHARACTER_STATE_BLOWN_UP1: true, ATK_CHARACTER_STATE_BLOWN_UP1: true,
ATK_CHARACTER_STATE_ONWALL: true, ATK_CHARACTER_STATE_ONWALL: true,
ATK_CHARACTER_STATE_DASHING: true, // Yes dashing is an inair state even if you dashed on the ground :)
} }
var noOpSet = map[int32]bool{ var noOpSet = map[int32]bool{
@@ -211,26 +217,43 @@ func calcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.Conve
} }
func isPolygonPairOverlapped(a, b *resolv.ConvexPolygon, result *SatResult) bool { func isPolygonPairOverlapped(a, b *resolv.ConvexPolygon, result *SatResult) bool {
aCnt, bCnt := len(a.Points), len(b.Points) aCnt, bCnt := a.Points.Cnt, b.Points.Cnt
// Single point case // Single point case
if 1 == aCnt && 1 == bCnt { if 1 == aCnt && 1 == bCnt {
if nil != result { if nil != result {
result.Overlap = 0 result.Overlap = 0
} }
return a.Points[0][0] == b.Points[0][0] && a.Points[0][1] == b.Points[0][1] aPoint := a.GetPointByOffset(0)
bPoint := b.GetPointByOffset(0)
return aPoint[0] == bPoint[0] && aPoint[1] == bPoint[1]
} }
if 1 < aCnt { if 1 < aCnt {
for _, axis := range a.SATAxes() { // Deliberately using "Points" instead of "SATAxes" to avoid unnecessary heap memory alloc
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) { for i := int32(0); i < a.Points.Cnt; i++ {
u, v := a.GetPointByOffset(i), a.GetPointByOffset(0)
if i != a.Points.Cnt-1 {
v = a.GetPointByOffset(i + 1)
}
dy := v[1] - u[1]
dx := v[0] - u[0]
axis := resolv.Vector{dy, -dx}.Unit()
if isPolygonPairSeparatedByDir(a, b, axis, result) {
return false return false
} }
} }
} }
if 1 < bCnt { if 1 < bCnt {
for _, axis := range b.SATAxes() { for i := int32(0); i < b.Points.Cnt; i++ {
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) { u, v := b.GetPointByOffset(i), b.GetPointByOffset(0)
if i != b.Points.Cnt-1 {
v = b.GetPointByOffset(i + 1)
}
dy := v[1] - u[1]
dx := v[0] - u[0]
axis := resolv.Vector{dy, -dx}.Unit()
if isPolygonPairSeparatedByDir(a, b, axis, result) {
return false return false
} }
} }
@@ -239,11 +262,15 @@ func isPolygonPairOverlapped(a, b *resolv.ConvexPolygon, result *SatResult) bool
return true return true
} }
func IsMeleeBulletActive(meleeBullet *MeleeBullet, currRenderFrame *RoomDownsyncFrame) bool { func IsGeneralBulletActive(blState, originatedRenderFrameId, startupFrames, activeFrames, renderFrameId int32) bool {
if BULLET_EXPLODING == meleeBullet.BlState { if BULLET_EXPLODING == blState {
return false return false
} }
return (meleeBullet.BattleAttr.OriginatedRenderFrameId+meleeBullet.Bullet.StartupFrames <= currRenderFrame.Id) && (meleeBullet.BattleAttr.OriginatedRenderFrameId+meleeBullet.Bullet.StartupFrames+meleeBullet.Bullet.ActiveFrames > currRenderFrame.Id) return (originatedRenderFrameId+startupFrames < renderFrameId) && (originatedRenderFrameId+startupFrames+activeFrames > renderFrameId)
}
func IsMeleeBulletActive(meleeBullet *MeleeBullet, currRenderFrame *RoomDownsyncFrame) bool {
return IsGeneralBulletActive(meleeBullet.BlState, meleeBullet.BattleAttr.OriginatedRenderFrameId, meleeBullet.Bullet.StartupFrames, meleeBullet.Bullet.ActiveFrames, currRenderFrame.Id)
} }
func IsMeleeBulletAlive(meleeBullet *MeleeBullet, currRenderFrame *RoomDownsyncFrame) bool { func IsMeleeBulletAlive(meleeBullet *MeleeBullet, currRenderFrame *RoomDownsyncFrame) bool {
@@ -254,10 +281,7 @@ func IsMeleeBulletAlive(meleeBullet *MeleeBullet, currRenderFrame *RoomDownsyncF
} }
func IsFireballBulletActive(fireballBullet *FireballBullet, currRenderFrame *RoomDownsyncFrame) bool { func IsFireballBulletActive(fireballBullet *FireballBullet, currRenderFrame *RoomDownsyncFrame) bool {
if BULLET_EXPLODING == fireballBullet.BlState { return IsGeneralBulletActive(fireballBullet.BlState, fireballBullet.BattleAttr.OriginatedRenderFrameId, fireballBullet.Bullet.StartupFrames, fireballBullet.Bullet.ActiveFrames, currRenderFrame.Id)
return false
}
return (fireballBullet.BattleAttr.OriginatedRenderFrameId+fireballBullet.Bullet.StartupFrames < currRenderFrame.Id) && (fireballBullet.BattleAttr.OriginatedRenderFrameId+fireballBullet.Bullet.StartupFrames+fireballBullet.Bullet.ActiveFrames > currRenderFrame.Id)
} }
func IsFireballBulletAlive(fireballBullet *FireballBullet, currRenderFrame *RoomDownsyncFrame) bool { func IsFireballBulletAlive(fireballBullet *FireballBullet, currRenderFrame *RoomDownsyncFrame) bool {
@@ -285,7 +309,8 @@ func isPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e resolv.Vector, re
*/ */
var aStart, aEnd, bStart, bEnd float64 = MAX_FLOAT64, -MAX_FLOAT64, MAX_FLOAT64, -MAX_FLOAT64 var aStart, aEnd, bStart, bEnd float64 = MAX_FLOAT64, -MAX_FLOAT64, MAX_FLOAT64, -MAX_FLOAT64
for _, p := range a.Points { for i := int32(0); i < a.Points.Cnt; i++ {
p := a.GetPointByOffset(i)
dot := (p[0]+a.X)*e[0] + (p[1]+a.Y)*e[1] dot := (p[0]+a.X)*e[0] + (p[1]+a.Y)*e[1]
if aStart > dot { if aStart > dot {
@@ -297,7 +322,8 @@ func isPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e resolv.Vector, re
} }
} }
for _, p := range b.Points { for i := int32(0); i < b.Points.Cnt; i++ {
p := b.GetPointByOffset(i)
dot := (p[0]+b.X)*e[0] + (p[1]+b.Y)*e[1] dot := (p[0]+b.X)*e[0] + (p[1]+b.Y)*e[1]
if bStart > dot { if bStart > dot {
@@ -406,8 +432,7 @@ func VirtualGridToPolygonColliderBLPos(vx, vy int32, halfBoundingW, halfBounding
return WorldToPolygonColliderBLPos(wx, wy, halfBoundingW, halfBoundingH, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY) return WorldToPolygonColliderBLPos(wx, wy, halfBoundingW, halfBoundingH, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY)
} }
func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, playerCollider *resolv.Object, playerShape *resolv.ConvexPolygon, snapIntoPlatformOverlap float64, pEffPushback *Vec2D) *[]Vec2D { func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, playerCollider *resolv.Object, playerShape *resolv.ConvexPolygon, snapIntoPlatformOverlap float64, effPushback *Vec2D, hardPushbackNorms []*Vec2D, collision *resolv.Collision) int {
ret := make([]Vec2D, 0, 10) // no one would simultaneously have more than 5 hardPushbacks
virtualGripToWall := float64(0) virtualGripToWall := float64(0)
if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && 0 == thatPlayerInNextFrame.VelX && currPlayerDownsync.DirX == thatPlayerInNextFrame.DirX { if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && 0 == thatPlayerInNextFrame.VelX && currPlayerDownsync.DirX == thatPlayerInNextFrame.DirX {
/* /*
@@ -424,14 +449,19 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
} }
virtualGripToWall = xfac * float64(currPlayerDownsync.Speed) * VIRTUAL_GRID_TO_WORLD_RATIO virtualGripToWall = xfac * float64(currPlayerDownsync.Speed) * VIRTUAL_GRID_TO_WORLD_RATIO
} }
collision := playerCollider.Check(virtualGripToWall, 0) retCnt := 0
if nil == collision { collided := playerCollider.CheckAllWithHolder(virtualGripToWall, 0, collision)
return &ret if !collided {
return retCnt
} }
//playerColliderCenterX, playerColliderCenterY := playerCollider.Center() //playerColliderCenterX, playerColliderCenterY := playerCollider.Center()
//fmt.Printf("joinIndex=%d calcHardPushbacksNorms has non-empty collision;playerColliderPos=(%.2f,%.2f)\n", joinIndex, playerColliderCenterX, playerColliderCenterY) //fmt.Printf("joinIndex=%d calcHardPushbacksNorms has non-empty collision;playerColliderPos=(%.2f,%.2f)\n", joinIndex, playerColliderCenterX, playerColliderCenterY)
for _, obj := range collision.Objects { for true {
obj := collision.PopFirstCollidedObject()
if nil == obj {
break
}
isBarrier := false isBarrier := false
switch obj.Data.(type) { switch obj.Data.(type) {
case *PlayerDownsync, *MeleeBullet, *FireballBullet: case *PlayerDownsync, *MeleeBullet, *FireballBullet:
@@ -451,15 +481,39 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
// ALWAY snap into hardPushbacks! // ALWAY snap into hardPushbacks!
// [OverlapX, OverlapY] is the unit vector that points into the platform // [OverlapX, OverlapY] is the unit vector that points into the platform
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY
ret = append(ret, Vec2D{X: overlapResult.OverlapX, Y: overlapResult.OverlapY}) hardPushbackNorms[retCnt].X, hardPushbackNorms[retCnt].Y = overlapResult.OverlapX, overlapResult.OverlapY
pEffPushback.X += pushbackX effPushback.X += pushbackX
pEffPushback.Y += pushbackY effPushback.Y += pushbackY
retCnt++
//fmt.Printf("joinIndex=%d calcHardPushbacksNorms found one hardpushback; immediatePushback=(%.2f,%.2f)\n", joinIndex, pushbackX, pushbackY) //fmt.Printf("joinIndex=%d calcHardPushbacksNorms found one hardpushback; immediatePushback=(%.2f,%.2f)\n", joinIndex, pushbackX, pushbackY)
} }
return &ret return retCnt
} }
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *RingBuffer) (int, bool, int32, int32) { func UpdateInputFrameInPlaceUponDynamics(inputFrameId int32, roomCapacity int, confirmedList uint64, inputList []uint64, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
hasInputFrameUpdatedOnDynamics := false
for i := 0; i < roomCapacity; i++ {
if int32(i+1) == toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics {
// On frontend, a "self input" is only confirmed by websocket downsync, which is quite late and might get the "self input" incorrectly overwritten if not excluded here
continue
}
if 0 < (confirmedList & (1 << uint32(i))) {
// This in-place update on the "inputsBuffer" is only correct when "delayed input for this player is not yet confirmed"
continue
}
if lastIndividuallyConfirmedInputFrameId[i] >= inputFrameId {
continue
}
newVal := (lastIndividuallyConfirmedInputList[i] & uint64(15))
if newVal != inputList[i] {
inputList[i] = newVal
hasInputFrameUpdatedOnDynamics = true
}
}
return hasInputFrameUpdatedOnDynamics
}
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *resolv.RingBuffer) (int, bool, int32, int32) {
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy) // returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id) delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
delayedInputFrameIdForPrevRdf := ConvertToDelayedInputFrameId(currRenderFrame.Id - 1) delayedInputFrameIdForPrevRdf := ConvertToDelayedInputFrameId(currRenderFrame.Id - 1)
@@ -472,10 +526,13 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
return PATTERN_ID_UNABLE_TO_OP, false, 0, 0 return PATTERN_ID_UNABLE_TO_OP, false, 0, 0
} }
delayedInputList := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync).InputList delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
delayedInputList := delayedInputFrameDownsync.InputList
var delayedInputListForPrevRdf []uint64 = nil var delayedInputListForPrevRdf []uint64 = nil
if 0 < delayedInputFrameIdForPrevRdf { if 0 < delayedInputFrameIdForPrevRdf {
delayedInputListForPrevRdf = inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync).InputList delayedInputFrameDownsyncForPrevRdf := inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync)
delayedInputListForPrevRdf = delayedInputFrameDownsyncForPrevRdf.InputList
} }
jumpedOrNot := false jumpedOrNot := false
@@ -531,82 +588,66 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
/* /*
[LONG TERM PERFORMANCE ENHANCEMENT PLAN] [LONG TERM PERFORMANCE ENHANCEMENT PLAN]
The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & playerColliders & bulletColliders, which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously. The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & dynamicRectangleColliders("player" & "bullet"), which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously.
It's not easy to remove all of the dynamic heap-memory blocks allocation/deallocation, but we can reduce them to some extent. For example, the creation of new "RoomDownsyncFrame" in heap-memory can be avoided by adding
```
func overwriteRoomDownsyncFrame(src *RoomDownsyncFrame, dst *RoomDownsyncFrame) {
// Copy "src" into "dst" down to every primitive field; as for a same room, the "RenderFrameBuffer" is always accessed (R & W) by a same kernel thread (both frontend & backend), no thread-safety concern here
}
type Room struct {
newRoomDownsyncFrameHolder *RoomDownsyncFrame
}
func (pR *Room) provisionNewRoomDownsyncFrameHolder(src *RoomDownsyncFrame) {
overwriteRoomDownsyncFrame(src, pR.newRoomDownsyncFrameHolder)
}
```
then pass in the whole "renderFrameBuffer *SpecificRingBuffer" to this function and overwrite the target slot IN-PLACE, i.e. need write new "SpecificRingBuffer.Put/SetByFrameId" to use the new function "overwriteRoomDownsyncFrame(src, dst)" to keep "%p of every SpecificRingBuffer.Eles[i]" constant.
However, the enhancement for "playerColliders & bulletColliders" of each room is even more difficult, because the feasibility of doing in-place overwrites depends on the collision library in use.
*/ */
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig) *RoomDownsyncFrame { func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
// [WARNING] On backend this function MUST BE called while "InputsBufferLock" is locked! hasInputFrameUpdatedOnDynamics := false
currRenderFrame := renderFrameBuffer.GetByFrameId(currRenderFrameId).(*RoomDownsyncFrame)
nextRenderFrameId := currRenderFrameId + 1
roomCapacity := len(currRenderFrame.PlayersArr) roomCapacity := len(currRenderFrame.PlayersArr)
nextRenderFramePlayers := make([]*PlayerDownsync, roomCapacity) var ret *RoomDownsyncFrame = nil
candidate := renderFrameBuffer.GetByFrameId(nextRenderFrameId)
if nil == candidate {
if nextRenderFrameId == renderFrameBuffer.EdFrameId {
renderFrameBuffer.DryPut()
candidate = renderFrameBuffer.GetByFrameId(nextRenderFrameId)
if nil == candidate {
// Lazy alloc heap-mem for holder
ret = NewPreallocatedRoomDownsyncFrame(roomCapacity, 64, 64)
renderFrameBuffer.SetByFrameId(ret, nextRenderFrameId)
} else {
ret = candidate.(*RoomDownsyncFrame)
}
} else {
panic("Invalid nextRenderFrameId=" + string(nextRenderFrameId) + "!")
}
} else {
ret = candidate.(*RoomDownsyncFrame)
}
// [WARNING] On backend this function MUST BE called while "InputsBufferLock" is locked!
nextRenderFramePlayers := ret.PlayersArr
// Make a copy first // Make a copy first
for i, currPlayerDownsync := range currRenderFrame.PlayersArr { for i, src := range currRenderFrame.PlayersArr {
nextRenderFramePlayers[i] = &PlayerDownsync{ framesToRecover := src.FramesToRecover - 1
Id: currPlayerDownsync.Id, framesInChState := src.FramesInChState + 1
VirtualGridX: currPlayerDownsync.VirtualGridX, framesInvinsible := src.FramesInvinsible - 1
VirtualGridY: currPlayerDownsync.VirtualGridY, if framesToRecover < 0 {
DirX: currPlayerDownsync.DirX, framesToRecover = 0
DirY: currPlayerDownsync.DirY,
VelX: currPlayerDownsync.VelX,
VelY: currPlayerDownsync.VelY,
CharacterState: currPlayerDownsync.CharacterState,
InAir: true,
OnWall: false,
Speed: currPlayerDownsync.Speed,
BattleState: currPlayerDownsync.BattleState,
Score: currPlayerDownsync.Score,
Removed: currPlayerDownsync.Removed,
JoinIndex: currPlayerDownsync.JoinIndex,
Hp: currPlayerDownsync.Hp,
MaxHp: currPlayerDownsync.MaxHp,
FramesToRecover: currPlayerDownsync.FramesToRecover - 1,
FramesInChState: currPlayerDownsync.FramesInChState + 1,
ActiveSkillId: currPlayerDownsync.ActiveSkillId,
ActiveSkillHit: currPlayerDownsync.ActiveSkillHit,
FramesInvinsible: currPlayerDownsync.FramesInvinsible - 1,
BulletTeamId: currPlayerDownsync.BulletTeamId,
ChCollisionTeamId: currPlayerDownsync.ChCollisionTeamId,
RevivalVirtualGridX: currPlayerDownsync.RevivalVirtualGridX,
RevivalVirtualGridY: currPlayerDownsync.RevivalVirtualGridY,
ColliderRadius: currPlayerDownsync.ColliderRadius,
OnWallNormX: currPlayerDownsync.OnWallNormX,
OnWallNormY: currPlayerDownsync.OnWallNormY,
CapturedByInertia: currPlayerDownsync.CapturedByInertia,
} }
if nextRenderFramePlayers[i].FramesToRecover < 0 { if framesInvinsible < 0 {
nextRenderFramePlayers[i].FramesToRecover = 0 framesInvinsible = 0
}
if nextRenderFramePlayers[i].FramesInvinsible < 0 {
nextRenderFramePlayers[i].FramesInvinsible = 0
} }
ClonePlayerDownsync(src.Id, src.VirtualGridX, src.VirtualGridY, src.DirX, src.DirY, src.VelX, src.VelY, framesToRecover, framesInChState, src.ActiveSkillId, src.ActiveSkillHit, framesInvinsible, src.Speed, src.BattleState, src.CharacterState, src.JoinIndex, src.Hp, src.MaxHp, src.ColliderRadius, true, false, src.OnWallNormX, src.OnWallNormY, src.CapturedByInertia, src.BulletTeamId, src.ChCollisionTeamId, src.RevivalVirtualGridX, src.RevivalVirtualGridY, nextRenderFramePlayers[i])
} }
nextRenderFrameMeleeBullets := make([]*MeleeBullet, 0, len(currRenderFrame.MeleeBullets)) // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation? meleeBulletCnt := 0
nextRenderFrameFireballBullets := make([]*FireballBullet, 0, len(currRenderFrame.FireballBullets)) nextRenderFrameMeleeBullets := ret.MeleeBullets
effPushbacks := make([]Vec2D, roomCapacity) fireballBulletCnt := 0
hardPushbackNorms := make([]*[]Vec2D, roomCapacity) nextRenderFrameFireballBullets := ret.FireballBullets
jumpedOrNotList := make([]bool, roomCapacity)
bulletLocalId := currRenderFrame.BulletLocalIdCounter bulletLocalId := currRenderFrame.BulletLocalIdCounter
// 1. Process player inputs // 1. Process player inputs
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
if 0 < delayedInputFrameId {
delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
delayedInputList := delayedInputFrameDownsync.InputList
roomCapacity := len(delayedInputList)
if allowUpdateInputFrameInPlaceUponDynamics {
hasInputFrameUpdatedOnDynamics = UpdateInputFrameInPlaceUponDynamics(delayedInputFrameId, roomCapacity, delayedInputFrameDownsync.ConfirmedList, delayedInputList, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
}
}
for i, currPlayerDownsync := range currRenderFrame.PlayersArr { for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
chConfig := chConfigsOrderedByJoinIndex[i] chConfig := chConfigsOrderedByJoinIndex[i]
thatPlayerInNextFrame := nextRenderFramePlayers[i] thatPlayerInNextFrame := nextRenderFramePlayers[i]
@@ -614,7 +655,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
jumpedOrNotList[i] = jumpedOrNot jumpedOrNotList[i] = jumpedOrNot
joinIndex := currPlayerDownsync.JoinIndex joinIndex := currPlayerDownsync.JoinIndex
skillId := chConfig.SkillMapper(patternId, currPlayerDownsync) skillId := chConfig.SkillMapper(patternId, currPlayerDownsync, chConfig.SpeciesId)
if skillConfig, existent := skills[skillId]; existent { if skillConfig, existent := skills[skillId]; existent {
thatPlayerInNextFrame.ActiveSkillId = int32(skillId) thatPlayerInNextFrame.ActiveSkillId = int32(skillId)
thatPlayerInNextFrame.ActiveSkillHit = 0 thatPlayerInNextFrame.ActiveSkillHit = 0
@@ -628,16 +669,9 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// Hardcoded to use only the first hit for now // Hardcoded to use only the first hit for now
switch v := skillConfig.Hits[thatPlayerInNextFrame.ActiveSkillHit].(type) { switch v := skillConfig.Hits[thatPlayerInNextFrame.ActiveSkillHit].(type) {
case *MeleeBullet: case *MeleeBullet:
var newBullet MeleeBullet = *v // Copied primitive fields into an onstack variable CloneMeleeBullet(BULLET_STARTUP, 0, bulletLocalId, currRenderFrameId, joinIndex, currPlayerDownsync.BulletTeamId, v.Bullet, nextRenderFrameMeleeBullets[meleeBulletCnt])
newBullet.BattleAttr = &BulletBattleAttr{
BulletLocalId: bulletLocalId,
OriginatedRenderFrameId: currRenderFrame.Id,
OffenderJoinIndex: joinIndex,
TeamId: currPlayerDownsync.BulletTeamId,
}
bulletLocalId++ bulletLocalId++
newBullet.BlState = BULLET_STARTUP meleeBulletCnt++
nextRenderFrameMeleeBullets = append(nextRenderFrameMeleeBullets, &newBullet)
if NO_LOCK_VEL != v.Bullet.SelfLockVelX { if NO_LOCK_VEL != v.Bullet.SelfLockVelX {
hasLockVel = true hasLockVel = true
thatPlayerInNextFrame.VelX = xfac * v.Bullet.SelfLockVelX thatPlayerInNextFrame.VelX = xfac * v.Bullet.SelfLockVelX
@@ -647,23 +681,9 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
thatPlayerInNextFrame.VelY = v.Bullet.SelfLockVelY thatPlayerInNextFrame.VelY = v.Bullet.SelfLockVelY
} }
case *FireballBullet: case *FireballBullet:
var newBullet FireballBullet = *v // Copied primitive fields into an onstack variable CloneFireballBullet(BULLET_STARTUP, 0, currPlayerDownsync.VirtualGridX+xfac*v.Bullet.HitboxOffsetX, currPlayerDownsync.VirtualGridY+v.Bullet.HitboxOffsetY, xfac, 0, v.Speed*xfac, 0, v.Speed, bulletLocalId, currRenderFrameId, joinIndex, currPlayerDownsync.BulletTeamId, v.Bullet, nextRenderFrameFireballBullets[fireballBulletCnt])
newBullet.BattleAttr = &BulletBattleAttr{
BulletLocalId: bulletLocalId,
OriginatedRenderFrameId: currRenderFrame.Id,
OffenderJoinIndex: joinIndex,
TeamId: currPlayerDownsync.BulletTeamId,
}
bulletLocalId++ bulletLocalId++
newBullet.VirtualGridX, newBullet.VirtualGridY = currPlayerDownsync.VirtualGridX+xfac*newBullet.Bullet.HitboxOffsetX, currPlayerDownsync.VirtualGridY+newBullet.Bullet.HitboxOffsetY fireballBulletCnt++
newBullet.DirX = xfac
newBullet.DirY = 0
newBullet.VelX = newBullet.Speed * xfac
newBullet.VelY = 0
newBullet.BlState = BULLET_STARTUP
nextRenderFrameFireballBullets = append(nextRenderFrameFireballBullets, &newBullet)
//fmt.Printf("Created new fireball @currRenderFrame.Id=%d, %p, bulletLocalId=%d, virtualGridX=%d, virtualGridY=%d, offenderVpos=(%d,%d)\n", currRenderFrame.Id, &newBullet, bulletLocalId, newBullet.VirtualGridX, newBullet.VirtualGridY, currPlayerDownsync.VirtualGridX, currPlayerDownsync.VirtualGridY)
if NO_LOCK_VEL != v.Bullet.SelfLockVelX { if NO_LOCK_VEL != v.Bullet.SelfLockVelX {
hasLockVel = true hasLockVel = true
thatPlayerInNextFrame.VelX = xfac * v.Bullet.SelfLockVelX thatPlayerInNextFrame.VelX = xfac * v.Bullet.SelfLockVelX
@@ -716,7 +736,9 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} else if stoppingFromWalking { } else if stoppingFromWalking {
thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover thatPlayerInNextFrame.FramesToRecover = chConfig.InertiaFramesToRecover
} else { } else {
thatPlayerInNextFrame.FramesToRecover = ((chConfig.InertiaFramesToRecover >> 1) + (chConfig.InertiaFramesToRecover >> 2)) // Updates CharacterState and thus the animation to make user see graphical feedback asap.
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING
thatPlayerInNextFrame.FramesToRecover = (chConfig.InertiaFramesToRecover >> 1)
} }
} else { } else {
thatPlayerInNextFrame.CapturedByInertia = false thatPlayerInNextFrame.CapturedByInertia = false
@@ -744,8 +766,14 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
} }
/*
[WARNING]
1. The dynamic colliders will all be removed from "Space" at the end of this function due to the need for being rollback-compatible.
2. To achieve "zero gc" in "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame", I deliberately chose a collision system that doesn't use dynamic tree node alloc.
*/
colliderCnt := 0
// 2. Process player movement // 2. Process player movement
playerColliders := make([]*resolv.Object, len(currRenderFrame.PlayersArr), len(currRenderFrame.PlayersArr)) // Will all be removed at the end of this function due to the need for being rollback-compatible
for i, currPlayerDownsync := range currRenderFrame.PlayersArr { for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex joinIndex := currPlayerDownsync.JoinIndex
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0) effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
@@ -758,7 +786,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// Revive from Dying // Revive from Dying
newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY newVx, newVy = currPlayerDownsync.RevivalVirtualGridX, currPlayerDownsync.RevivalVirtualGridY
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1 thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
thatPlayerInNextFrame.FramesInChState = ATK_CHARACTER_STATE_GET_UP1 thatPlayerInNextFrame.FramesInChState = 0
thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover thatPlayerInNextFrame.FramesToRecover = chConfig.GetUpFramesToRecover
thatPlayerInNextFrame.FramesInvinsible = chConfig.GetUpInvinsibleFrames thatPlayerInNextFrame.FramesInvinsible = chConfig.GetUpInvinsibleFrames
thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp thatPlayerInNextFrame.Hp = currPlayerDownsync.MaxHp
@@ -804,11 +832,12 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
colliderWorldWidth, colliderWorldHeight := VirtualGridToWorldPos(colliderWidth, colliderHeight) colliderWorldWidth, colliderWorldHeight := VirtualGridToWorldPos(colliderWidth, colliderHeight)
playerCollider := GenerateRectCollider(wx, wy, colliderWorldWidth, colliderWorldHeight, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, currPlayerDownsync, "Player") // the coords of all barrier boundaries are multiples of tileWidth(i.e. 16), by adding snapping y-padding when "landedOnGravityPushback" all "playerCollider.Y" would be a multiple of 1.0 playerCollider := dynamicRectangleColliders[colliderCnt]
playerColliders[i] = playerCollider UpdateRectCollider(playerCollider, wx, wy, colliderWorldWidth, colliderWorldHeight, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, currPlayerDownsync, "Player") // the coords of all barrier boundaries are multiples of tileWidth(i.e. 16), by adding snapping y-padding when "landedOnGravityPushback" all "playerCollider.Y" would be a multiple of 1.0
colliderCnt++
// Add to collision system // Add to collision system
collisionSys.Add(playerCollider) collisionSys.AddSingle(playerCollider)
if currPlayerDownsync.InAir { if currPlayerDownsync.InAir {
if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && !jumpedOrNotList[i] { if ATK_CHARACTER_STATE_ONWALL == currPlayerDownsync.CharacterState && !jumpedOrNotList[i] {
@@ -823,30 +852,25 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
} }
// 3. Add bullet colliders into collision system // 3. Add bullet colliders into collision system; [DIRTY TRICK] Players always precede bullets in "dynamicRectangleColliders".
// [WARNING] For rollback compatibility, static data of "BulletConfig" & "BattleAttr(static since instantiated)" can just be copies of the pointers in "RenderFrameBuffer", however, FireballBullets movement data as well as bullet animation data must be copies of instances for each RenderFrame! // [WARNING] For rollback compatibility, static data of "BulletConfig" & "BattleAttr(static since instantiated)" can just be copies of the pointers in "RenderFrameBuffer", however, FireballBullets movement data as well as bullet animation data must be copies of instances for each RenderFrame!
bulletColliders := make([]*resolv.Object, 0, ((len(currRenderFrame.MeleeBullets) + len(currRenderFrame.FireballBullets)) << 1)) // Will all be removed at the end of this function due to the need for being rollback-compatible
for _, prevFireball := range currRenderFrame.FireballBullets { for _, prevFireball := range currRenderFrame.FireballBullets {
fireballBullet := &FireballBullet{ if TERMINATING_BULLET_LOCAL_ID == prevFireball.BattleAttr.BulletLocalId {
VirtualGridX: prevFireball.VirtualGridX, break
VirtualGridY: prevFireball.VirtualGridY,
DirX: prevFireball.DirX,
DirY: prevFireball.DirY,
VelX: prevFireball.VelX,
VelY: prevFireball.VelY,
Speed: prevFireball.Speed,
Bullet: prevFireball.Bullet,
BattleAttr: prevFireball.BattleAttr,
FramesInBlState: prevFireball.FramesInBlState + 1,
BlState: prevFireball.BlState,
} }
fireballBullet := nextRenderFrameFireballBullets[fireballBulletCnt]
CloneFireballBullet(prevFireball.BlState, prevFireball.FramesInBlState+1, prevFireball.VirtualGridX, prevFireball.VirtualGridY, prevFireball.DirX, prevFireball.DirY, prevFireball.VelX, prevFireball.VelY, prevFireball.Speed, prevFireball.BattleAttr.BulletLocalId, prevFireball.BattleAttr.OriginatedRenderFrameId, prevFireball.BattleAttr.OffenderJoinIndex, prevFireball.BattleAttr.TeamId, prevFireball.Bullet, fireballBullet)
if IsFireballBulletAlive(fireballBullet, currRenderFrame) { if IsFireballBulletAlive(fireballBullet, currRenderFrame) {
if IsFireballBulletActive(fireballBullet, currRenderFrame) { if IsFireballBulletActive(fireballBullet, currRenderFrame) {
bulletWx, bulletWy := VirtualGridToWorldPos(fireballBullet.VirtualGridX, fireballBullet.VirtualGridY) bulletWx, bulletWy := VirtualGridToWorldPos(fireballBullet.VirtualGridX, fireballBullet.VirtualGridY)
hitboxSizeWx, hitboxSizeWy := VirtualGridToWorldPos(fireballBullet.Bullet.HitboxSizeX, fireballBullet.Bullet.HitboxSizeY) hitboxSizeWx, hitboxSizeWy := VirtualGridToWorldPos(fireballBullet.Bullet.HitboxSizeX, fireballBullet.Bullet.HitboxSizeY)
newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, hitboxSizeWx, hitboxSizeWy, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, fireballBullet, "FireballBullet")
collisionSys.Add(newBulletCollider) newBulletCollider := dynamicRectangleColliders[colliderCnt]
bulletColliders = append(bulletColliders, newBulletCollider) UpdateRectCollider(newBulletCollider, bulletWx, bulletWy, hitboxSizeWx, hitboxSizeWy, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, fireballBullet, "FireballBullet")
colliderCnt++
collisionSys.AddSingle(newBulletCollider)
fireballBullet.BlState = BULLET_ACTIVE fireballBullet.BlState = BULLET_ACTIVE
if fireballBullet.BlState != prevFireball.BlState { if fireballBullet.BlState != prevFireball.BlState {
fireballBullet.FramesInBlState = 0 fireballBullet.FramesInBlState = 0
@@ -861,17 +885,19 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
//fmt.Printf("Pushing non-active fireball to next frame @currRenderFrame.Id=%d, bulletLocalId=%d, virtualGridX=%d, virtualGridY=%d, blState=%d\n", currRenderFrame.Id, fireballBullet.BattleAttr.BulletLocalId, fireballBullet.VirtualGridX, fireballBullet.VirtualGridY, fireballBullet.BlState) //fmt.Printf("Pushing non-active fireball to next frame @currRenderFrame.Id=%d, bulletLocalId=%d, virtualGridX=%d, virtualGridY=%d, blState=%d\n", currRenderFrame.Id, fireballBullet.BattleAttr.BulletLocalId, fireballBullet.VirtualGridX, fireballBullet.VirtualGridY, fireballBullet.BlState)
} }
nextRenderFrameFireballBullets = append(nextRenderFrameFireballBullets, fireballBullet) fireballBulletCnt++
} }
} }
// Explicitly specify termination of fireball bullets
nextRenderFrameFireballBullets[fireballBulletCnt].BattleAttr.BulletLocalId = TERMINATING_BULLET_LOCAL_ID
for _, prevMelee := range currRenderFrame.MeleeBullets { for _, prevMelee := range currRenderFrame.MeleeBullets {
meleeBullet := &MeleeBullet{ if TERMINATING_BULLET_LOCAL_ID == prevMelee.BattleAttr.BulletLocalId {
Bullet: prevMelee.Bullet, break
BattleAttr: prevMelee.BattleAttr,
FramesInBlState: prevMelee.FramesInBlState + 1,
BlState: prevMelee.BlState,
} }
meleeBullet := nextRenderFrameMeleeBullets[meleeBulletCnt]
CloneMeleeBullet(prevMelee.BlState, prevMelee.FramesInBlState+1, prevMelee.BattleAttr.BulletLocalId, prevMelee.BattleAttr.OriginatedRenderFrameId, prevMelee.BattleAttr.OffenderJoinIndex, prevMelee.BattleAttr.TeamId, prevMelee.Bullet, meleeBullet)
if IsMeleeBulletAlive(meleeBullet, currRenderFrame) { if IsMeleeBulletAlive(meleeBullet, currRenderFrame) {
offender := currRenderFrame.PlayersArr[meleeBullet.BattleAttr.OffenderJoinIndex-1] offender := currRenderFrame.PlayersArr[meleeBullet.BattleAttr.OffenderJoinIndex-1]
if _, existent := noOpSet[offender.CharacterState]; existent { if _, existent := noOpSet[offender.CharacterState]; existent {
@@ -885,30 +911,39 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
bulletWx, bulletWy := VirtualGridToWorldPos(offender.VirtualGridX+xfac*meleeBullet.Bullet.HitboxOffsetX, offender.VirtualGridY) bulletWx, bulletWy := VirtualGridToWorldPos(offender.VirtualGridX+xfac*meleeBullet.Bullet.HitboxOffsetX, offender.VirtualGridY)
hitboxSizeWx, hitboxSizeWy := VirtualGridToWorldPos(meleeBullet.Bullet.HitboxSizeX, meleeBullet.Bullet.HitboxSizeY) hitboxSizeWx, hitboxSizeWy := VirtualGridToWorldPos(meleeBullet.Bullet.HitboxSizeX, meleeBullet.Bullet.HitboxSizeY)
newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, hitboxSizeWx, hitboxSizeWy, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, meleeBullet, "MeleeBullet")
collisionSys.Add(newBulletCollider) newBulletCollider := dynamicRectangleColliders[colliderCnt]
bulletColliders = append(bulletColliders, newBulletCollider) UpdateRectCollider(newBulletCollider, bulletWx, bulletWy, hitboxSizeWx, hitboxSizeWy, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, collisionSpaceOffsetX, collisionSpaceOffsetY, meleeBullet, "MeleeBullet")
colliderCnt++
collisionSys.AddSingle(newBulletCollider)
meleeBullet.BlState = BULLET_ACTIVE meleeBullet.BlState = BULLET_ACTIVE
if meleeBullet.BlState != prevMelee.BlState { if meleeBullet.BlState != prevMelee.BlState {
meleeBullet.FramesInBlState = 0 meleeBullet.FramesInBlState = 0
} }
} }
nextRenderFrameMeleeBullets = append(nextRenderFrameMeleeBullets, meleeBullet) meleeBulletCnt++
} }
} }
// Explicitly specify termination of melee bullets
nextRenderFrameMeleeBullets[meleeBulletCnt].BattleAttr.BulletLocalId = TERMINATING_BULLET_LOCAL_ID
// 4. Calc pushbacks for each player (after its movement) w/o bullets // 4. Calc pushbacks for each player (after its movement) w/o bullets
for i, currPlayerDownsync := range currRenderFrame.PlayersArr { for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex joinIndex := currPlayerDownsync.JoinIndex
playerCollider := playerColliders[i] playerCollider := dynamicRectangleColliders[i]
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon) playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
thatPlayerInNextFrame := nextRenderFramePlayers[i] thatPlayerInNextFrame := nextRenderFramePlayers[i]
hardPushbackNorms[joinIndex-1] = calcHardPushbacksNorms(joinIndex, currPlayerDownsync, thatPlayerInNextFrame, playerCollider, playerShape, SNAP_INTO_PLATFORM_OVERLAP, &(effPushbacks[joinIndex-1])) hardPushbackCnt := calcHardPushbacksNorms(joinIndex, currPlayerDownsync, thatPlayerInNextFrame, playerCollider, playerShape, SNAP_INTO_PLATFORM_OVERLAP, effPushbacks[joinIndex-1], hardPushbackNormsArr[joinIndex-1], collision)
chConfig := chConfigsOrderedByJoinIndex[i] chConfig := chConfigsOrderedByJoinIndex[i]
landedOnGravityPushback := false landedOnGravityPushback := false
if collision := playerCollider.Check(0, 0); nil != collision { if collided := playerCollider.CheckAllWithHolder(0, 0, collision); collided {
for _, obj := range collision.Objects { for true {
obj := collision.PopFirstCollidedObject()
if nil == obj {
break
}
isBarrier, isAnotherPlayer, isBullet := false, false, false isBarrier, isAnotherPlayer, isBullet := false, false, false
switch v := obj.Data.(type) { switch v := obj.Data.(type) {
case *PlayerDownsync: case *PlayerDownsync:
@@ -938,7 +973,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame. // [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame.
pushbackX, pushbackY = (overlapResult.Overlap-SNAP_INTO_PLATFORM_OVERLAP*2)*overlapResult.OverlapX, (overlapResult.Overlap-SNAP_INTO_PLATFORM_OVERLAP*2)*overlapResult.OverlapY pushbackX, pushbackY = (overlapResult.Overlap-SNAP_INTO_PLATFORM_OVERLAP*2)*overlapResult.OverlapX, (overlapResult.Overlap-SNAP_INTO_PLATFORM_OVERLAP*2)*overlapResult.OverlapY
} }
for _, hardPushbackNorm := range *hardPushbackNorms[joinIndex-1] { for i := 0; i < hardPushbackCnt; i++ {
hardPushbackNorm := hardPushbackNormsArr[joinIndex-1][i]
projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y
if isBarrier || (isAnotherPlayer && 0 > projectedMagnitude) { if isBarrier || (isAnotherPlayer && 0 > projectedMagnitude) {
pushbackX -= projectedMagnitude * hardPushbackNorm.X pushbackX -= projectedMagnitude * hardPushbackNorm.X
@@ -951,10 +987,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
if SNAP_INTO_PLATFORM_THRESHOLD < normAlignmentWithGravity { if SNAP_INTO_PLATFORM_THRESHOLD < normAlignmentWithGravity {
landedOnGravityPushback = true landedOnGravityPushback = true
//playerColliderCenterX, playerColliderCenterY := playerCollider.Center() //playerColliderCenterX, playerColliderCenterY := playerCollider.Center()
//fmt.Printf("joinIndex=%d landedOnGravityPushback\n{renderFrame.id: %d, isBarrier: %v, isAnotherPlayer: %v}\nhardPushbackNormsOfThisPlayer=%v, playerColliderPos=(%.2f,%.2f), immediatePushback={%.3f, %.3f}, effPushback={%.3f, %.3f}, overlapMag=%.4f\n", joinIndex, currRenderFrame.Id, isBarrier, isAnotherPlayer, *hardPushbackNorms[joinIndex-1], playerColliderCenterX, playerColliderCenterY, pushbackX, pushbackY, effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y, overlapResult.Overlap) //fmt.Printf("joinIndex=%d landedOnGravityPushback\n{renderFrame.id: %d, isBarrier: %v, isAnotherPlayer: %v}\nhardPushbackNormsOfThisPlayer=%v, playerColliderPos=(%.2f,%.2f), immediatePushback={%.3f, %.3f}, effPushback={%.3f, %.3f}, overlapMag=%.4f\n", joinIndex, currRenderFrame.Id, isBarrier, isAnotherPlayer, hardPushbackNorms[joinIndex-1], playerColliderCenterX, playerColliderCenterY, pushbackX, pushbackY, effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y, overlapResult.Overlap)
} }
} }
} }
if landedOnGravityPushback { if landedOnGravityPushback {
thatPlayerInNextFrame.InAir = false thatPlayerInNextFrame.InAir = false
fallStopping := (currPlayerDownsync.InAir && 0 >= currPlayerDownsync.VelY) fallStopping := (currPlayerDownsync.InAir && 0 >= currPlayerDownsync.VelY)
@@ -962,6 +999,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
thatPlayerInNextFrame.VelY = 0 thatPlayerInNextFrame.VelY = 0
thatPlayerInNextFrame.VelX = 0 thatPlayerInNextFrame.VelX = 0
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState { if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
// No update needed for Dying
} else if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState { } else if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame.CharacterState {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1 thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover thatPlayerInNextFrame.FramesToRecover = chConfig.LayDownFramesToRecover
@@ -980,7 +1019,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// landedOnGravityPushback not fallStopping, could be in LayDown or GetUp or Dying // landedOnGravityPushback not fallStopping, could be in LayDown or GetUp or Dying
if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent { if _, existent := nonAttackingSet[thatPlayerInNextFrame.CharacterState]; existent {
if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState { if ATK_CHARACTER_STATE_DYING == thatPlayerInNextFrame.CharacterState {
// No update needed for Dying thatPlayerInNextFrame.VelY = 0
thatPlayerInNextFrame.VelX = 0
} else if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState { } else if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame.CharacterState {
if 0 == thatPlayerInNextFrame.FramesToRecover { if 0 == thatPlayerInNextFrame.FramesToRecover {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1 thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_GET_UP1
@@ -1001,7 +1041,8 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// [WARNING] Sticking to wall MUST BE based on "InAir", otherwise we would get gravity reduction from ground up incorrectly! // [WARNING] Sticking to wall MUST BE based on "InAir", otherwise we would get gravity reduction from ground up incorrectly!
if _, existent := noOpSet[currPlayerDownsync.CharacterState]; !existent { if _, existent := noOpSet[currPlayerDownsync.CharacterState]; !existent {
// [WARNING] Sticking to wall could only be triggered by proactive player input // [WARNING] Sticking to wall could only be triggered by proactive player input
for _, hardPushbackNorm := range *hardPushbackNorms[joinIndex-1] { for i := 0; i < hardPushbackCnt; i++ {
hardPushbackNorm := hardPushbackNormsArr[joinIndex-1][i]
normAlignmentWithHorizon1 := (hardPushbackNorm.X*float64(1.0) + hardPushbackNorm.Y*float64(0.0)) normAlignmentWithHorizon1 := (hardPushbackNorm.X*float64(1.0) + hardPushbackNorm.Y*float64(0.0))
normAlignmentWithHorizon2 := (hardPushbackNorm.X*float64(-1.0) + hardPushbackNorm.Y*float64(0.0)) normAlignmentWithHorizon2 := (hardPushbackNorm.X*float64(-1.0) + hardPushbackNorm.Y*float64(0.0))
if VERTICAL_PLATFORM_THRESHOLD < normAlignmentWithHorizon1 { if VERTICAL_PLATFORM_THRESHOLD < normAlignmentWithHorizon1 {
@@ -1021,19 +1062,19 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
thatPlayerInNextFrame.OnWallNormX, thatPlayerInNextFrame.OnWallNormY = 0, 0 thatPlayerInNextFrame.OnWallNormX, thatPlayerInNextFrame.OnWallNormY = 0, 0
} }
} }
} }
// 5. Check bullet-anything collisions // 5. Check bullet-anything collisions
for _, bulletCollider := range bulletColliders { for i := len(nextRenderFramePlayers); i < colliderCnt; i++ {
collision := bulletCollider.Check(0, 0) bulletCollider := dynamicRectangleColliders[i]
bulletCollider.Space.Remove(bulletCollider) // Make sure that the bulletCollider is always removed for each renderFrame collided := bulletCollider.CheckAllWithHolder(0, 0, collision)
exploded := false if !collided {
explodedOnAnotherPlayer := false
if nil == collision {
continue continue
} }
exploded := false
explodedOnAnotherPlayer := false
var bulletStaticAttr *BulletConfig = nil var bulletStaticAttr *BulletConfig = nil
var bulletBattleAttr *BulletBattleAttr = nil var bulletBattleAttr *BulletBattleAttr = nil
switch v := bulletCollider.Data.(type) { switch v := bulletCollider.Data.(type) {
@@ -1047,7 +1088,11 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon) bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon)
offender := currRenderFrame.PlayersArr[bulletBattleAttr.OffenderJoinIndex-1] offender := currRenderFrame.PlayersArr[bulletBattleAttr.OffenderJoinIndex-1]
for _, obj := range collision.Objects { for true {
obj := collision.PopFirstCollidedObject()
if nil == obj {
break
}
defenderShape := obj.Shape.(*resolv.ConvexPolygon) defenderShape := obj.Shape.(*resolv.ConvexPolygon)
switch t := obj.Data.(type) { switch t := obj.Data.(type) {
case *PlayerDownsync: case *PlayerDownsync:
@@ -1072,15 +1117,15 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
atkedPlayerInNextFrame := nextRenderFramePlayers[t.JoinIndex-1] atkedPlayerInNextFrame := nextRenderFramePlayers[t.JoinIndex-1]
atkedPlayerInNextFrame.Hp -= bulletStaticAttr.Damage atkedPlayerInNextFrame.Hp -= bulletStaticAttr.Damage
pushbackVelX, pushbackVelY := xfac*bulletStaticAttr.PushbackVelX, bulletStaticAttr.PushbackVelY
atkedPlayerInNextFrame.VelX = pushbackVelX
atkedPlayerInNextFrame.VelY = pushbackVelY
if 0 >= atkedPlayerInNextFrame.Hp { if 0 >= atkedPlayerInNextFrame.Hp {
// [WARNING] We don't have "dying in air" animation for now, and for better graphical recognition, play the same dying animation even in air // [WARNING] We don't have "dying in air" animation for now, and for better graphical recognition, play the same dying animation even in air
atkedPlayerInNextFrame.Hp = 0 atkedPlayerInNextFrame.Hp = 0
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_DYING atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_DYING
atkedPlayerInNextFrame.FramesToRecover = DYING_FRAMES_TO_RECOVER atkedPlayerInNextFrame.FramesToRecover = DYING_FRAMES_TO_RECOVER
} else { } else {
pushbackVelX, pushbackVelY := xfac*bulletStaticAttr.PushbackVelX, bulletStaticAttr.PushbackVelY
atkedPlayerInNextFrame.VelX = pushbackVelX
atkedPlayerInNextFrame.VelY = pushbackVelY
if bulletStaticAttr.BlowUp { if bulletStaticAttr.BlowUp {
atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1 atkedPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1
} else { } else {
@@ -1118,7 +1163,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
// 6. Get players out of stuck barriers if there's any // 6. Get players out of stuck barriers if there's any
for i, currPlayerDownsync := range currRenderFrame.PlayersArr { for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex joinIndex := currPlayerDownsync.JoinIndex
playerCollider := playerColliders[i] playerCollider := dynamicRectangleColliders[i]
// Update "virtual grid position" // Update "virtual grid position"
thatPlayerInNextFrame := nextRenderFramePlayers[i] thatPlayerInNextFrame := nextRenderFramePlayers[i]
thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = PolygonColliderBLToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, playerCollider.W*0.5, playerCollider.H*0.5, 0, 0, 0, 0, collisionSpaceOffsetX, collisionSpaceOffsetY) thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = PolygonColliderBLToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, playerCollider.W*0.5, playerCollider.H*0.5, 0, 0, 0, 0, collisionSpaceOffsetX, collisionSpaceOffsetY)
@@ -1164,17 +1209,15 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *RingBuffer
} }
} }
for _, playerCollider := range playerColliders { for i := 0; i < colliderCnt; i++ {
playerCollider.Space.Remove(playerCollider) dynamicCollider := dynamicRectangleColliders[i]
dynamicCollider.Space.RemoveSingle(dynamicCollider)
} }
return &RoomDownsyncFrame{ ret.Id = nextRenderFrameId
Id: currRenderFrame.Id + 1, ret.BulletLocalIdCounter = bulletLocalId
PlayersArr: nextRenderFramePlayers,
BulletLocalIdCounter: bulletLocalId, return hasInputFrameUpdatedOnDynamics
MeleeBullets: nextRenderFrameMeleeBullets,
FireballBullets: nextRenderFrameFireballBullets,
}
} }
func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object { func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object {
@@ -1183,13 +1226,23 @@ func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding,
} }
func generateRectColliderInCollisionSpace(blX, blY, w, h float64, data interface{}, tag string) *resolv.Object { func generateRectColliderInCollisionSpace(blX, blY, w, h float64, data interface{}, tag string) *resolv.Object {
collider := resolv.NewObject(blX, blY, w, h, tag) // Unlike its frontend counter part, the position of a "resolv.Object" must be specified by "bottom-left point" because "w" and "h" must be positive, see "resolv.Object.BoundsToSpace" for details collider := resolv.NewObjectSingleTag(blX, blY, w, h, tag) // Unlike its frontend counter part, the position of a "resolv.Object" must be specified by "bottom-left point" because "w" and "h" must be positive, see "resolv.Object.BoundsToSpace" for details
shape := resolv.NewRectangle(0, 0, w, h) shape := resolv.NewRectangle(0, 0, w, h)
collider.SetShape(shape) collider.SetShape(shape)
collider.Data = data collider.Data = data
return collider return collider
} }
func UpdateRectCollider(collider *resolv.Object, wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) {
blX, blY := WorldToPolygonColliderBLPos(wx, wy, w*0.5, h*0.5, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY)
effW, effH := leftPadding+w+rightPadding, bottomPadding+h+topPadding
collider.X, collider.Y, collider.W, collider.H = blX, blY, effW, effH
rectShape := collider.Shape.(*resolv.ConvexPolygon)
rectShape.UpdateAsRectangle(0, 0, effW, effH)
collider.Data = data
// Ignore "tag" for now
}
func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object { func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object {
aligned := AlignPolygon2DToBoundingBox(unalignedSrc) aligned := AlignPolygon2DToBoundingBox(unalignedSrc)
var w, h float64 = 0, 0 var w, h float64 = 0, 0
@@ -1294,13 +1347,15 @@ func NewMeleeBullet(bulletLocalId, originatedRenderFrameId, offenderJoinIndex, s
func NewFireballBullet(bulletLocalId, originatedRenderFrameId, offenderJoinIndex, startupFrames, cancellableStFrame, cancellableEdFrame, activeFrames, hitStunFrames, blockStunFrames, pushbackVelX, pushbackVelY, damage, selfLockVelX, selfLockVelY, hitboxOffsetX, hitboxOffsetY, hitboxSizeX, hitboxSizeY int32, blowUp bool, teamId int32, virtualGridX, virtualGridY, dirX, dirY, velX, velY, speed, blState, framesInBlState, explosionFrames, speciesId int32) *FireballBullet { func NewFireballBullet(bulletLocalId, originatedRenderFrameId, offenderJoinIndex, startupFrames, cancellableStFrame, cancellableEdFrame, activeFrames, hitStunFrames, blockStunFrames, pushbackVelX, pushbackVelY, damage, selfLockVelX, selfLockVelY, hitboxOffsetX, hitboxOffsetY, hitboxSizeX, hitboxSizeY int32, blowUp bool, teamId int32, virtualGridX, virtualGridY, dirX, dirY, velX, velY, speed, blState, framesInBlState, explosionFrames, speciesId int32) *FireballBullet {
return &FireballBullet{ return &FireballBullet{
VirtualGridX: virtualGridX, BlState: blState,
VirtualGridY: virtualGridY, FramesInBlState: framesInBlState,
DirX: dirX, VirtualGridX: virtualGridX,
DirY: dirY, VirtualGridY: virtualGridY,
VelX: velX, DirX: dirX,
VelY: velY, DirY: dirY,
Speed: speed, VelX: velX,
VelY: velY,
Speed: speed,
BattleAttr: &BulletBattleAttr{ BattleAttr: &BulletBattleAttr{
BulletLocalId: bulletLocalId, BulletLocalId: bulletLocalId,
OriginatedRenderFrameId: originatedRenderFrameId, OriginatedRenderFrameId: originatedRenderFrameId,
@@ -1333,3 +1388,150 @@ func NewFireballBullet(bulletLocalId, originatedRenderFrameId, offenderJoinIndex
}, },
} }
} }
func NewPlayerDownsync(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, framesToRecover, framesInChState, activeSkillId, activeSkillHit, framesInvinsible, speed, battleState, characterState, joinIndex, hp, maxHp, colliderRadius int32, inAir, onWall bool, onWallNormX, onWallNormY int32, capturedByInertia bool, bulletTeamId, chCollisionTeamId int32, revivalVirtualGridX, revivalVirtualGridY int32) *PlayerDownsync {
return &PlayerDownsync{
Id: id,
VirtualGridX: virtualGridX,
VirtualGridY: virtualGridY,
DirX: dirX,
DirY: dirY,
VelX: velX,
VelY: velY,
FramesToRecover: framesToRecover,
FramesInChState: framesInChState,
ActiveSkillId: activeSkillId,
ActiveSkillHit: activeSkillHit,
FramesInvinsible: framesInvinsible,
Speed: speed,
BattleState: battleState,
CharacterState: characterState,
JoinIndex: joinIndex,
Hp: hp,
MaxHp: maxHp,
ColliderRadius: colliderRadius,
InAir: inAir,
OnWall: onWall,
OnWallNormX: onWallNormX,
OnWallNormY: onWallNormY,
CapturedByInertia: capturedByInertia,
BulletTeamId: bulletTeamId,
ChCollisionTeamId: chCollisionTeamId,
RevivalVirtualGridX: revivalVirtualGridX,
RevivalVirtualGridY: revivalVirtualGridY,
}
}
func CloneMeleeBullet(blState, framesInBlState, bulletLocalId, originatedRenderFrameId, offenderJoinIndex, teamId int32, staticBulletConfig *BulletConfig, dst *MeleeBullet /* preallocated */) {
dst.BlState = blState
dst.FramesInBlState = framesInBlState
dst.BattleAttr.BulletLocalId = bulletLocalId
dst.BattleAttr.OriginatedRenderFrameId = originatedRenderFrameId
dst.BattleAttr.OffenderJoinIndex = offenderJoinIndex
dst.BattleAttr.TeamId = teamId
dst.Bullet = staticBulletConfig // It's OK to just assign the pointer here, static bullet config is meant to be passed this way
}
func CloneFireballBullet(blState, framesInBlState, virtualGridX, virtualGridY, dirX, dirY, velX, velY, speed, bulletLocalId, originatedRenderFrameId, offenderJoinIndex, teamId int32, staticBulletConfig *BulletConfig, dst *FireballBullet /* preallocated */) {
dst.BlState = blState
dst.FramesInBlState = framesInBlState
dst.VirtualGridX = virtualGridX
dst.VirtualGridY = virtualGridY
dst.DirX = dirX
dst.DirY = dirY
dst.VelX = velX
dst.VelY = velY
dst.Speed = speed
dst.BattleAttr.BulletLocalId = bulletLocalId
dst.BattleAttr.OriginatedRenderFrameId = originatedRenderFrameId
dst.BattleAttr.OffenderJoinIndex = offenderJoinIndex
dst.BattleAttr.TeamId = teamId
dst.Bullet = staticBulletConfig // It's OK to just assign the pointer here, static bullet config is meant to be passed this way
}
func ClonePlayerDownsync(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, framesToRecover, framesInChState, activeSkillId, activeSkillHit, framesInvinsible, speed, battleState, characterState, joinIndex, hp, maxHp, colliderRadius int32, inAir, onWall bool, onWallNormX, onWallNormY int32, capturedByInertia bool, bulletTeamId, chCollisionTeamId, revivalVirtualGridX, revivalVirtualGridY int32, dst *PlayerDownsync) {
dst.Id = id
dst.VirtualGridX = virtualGridX
dst.VirtualGridY = virtualGridY
dst.DirX = dirX
dst.DirY = dirY
dst.VelX = velX
dst.VelY = velY
dst.FramesToRecover = framesToRecover
dst.FramesInChState = framesInChState
dst.ActiveSkillId = activeSkillId
dst.ActiveSkillHit = activeSkillHit
dst.FramesInvinsible = framesInvinsible
dst.Speed = speed
dst.BattleState = battleState
dst.CharacterState = characterState
dst.JoinIndex = joinIndex
dst.Hp = hp
dst.MaxHp = maxHp
dst.ColliderRadius = colliderRadius
dst.InAir = inAir
dst.OnWall = onWall
dst.OnWallNormX = onWallNormX
dst.OnWallNormY = onWallNormY
dst.CapturedByInertia = capturedByInertia
dst.BulletTeamId = bulletTeamId
dst.ChCollisionTeamId = chCollisionTeamId
dst.RevivalVirtualGridX = revivalVirtualGridX
dst.RevivalVirtualGridY = revivalVirtualGridY
}
func CloneRoomDownsyncFrame(id int32, playersArr []*PlayerDownsync, bulletLocalIdCounter int32, meleeBullets []*MeleeBullet, fireballBullets []*FireballBullet, dst *RoomDownsyncFrame) {
dst.Id = id
dst.BulletLocalIdCounter = bulletLocalIdCounter
for i := 0; i < len(playersArr); i++ {
src := playersArr[i]
if nil == src || TERMINATING_PLAYER_ID == src.Id {
break
}
ClonePlayerDownsync(src.Id, src.VirtualGridX, src.VirtualGridY, src.DirX, src.DirY, src.VelX, src.VelY, src.FramesToRecover, src.FramesInChState, src.ActiveSkillId, src.ActiveSkillHit, src.FramesInvinsible, src.Speed, src.BattleState, src.CharacterState, src.JoinIndex, src.Hp, src.MaxHp, src.ColliderRadius, src.InAir, src.OnWall, src.OnWallNormX, src.OnWallNormY, src.CapturedByInertia, src.BulletTeamId, src.ChCollisionTeamId, src.RevivalVirtualGridX, src.RevivalVirtualGridY, dst.PlayersArr[i])
}
for i := 0; i < len(meleeBullets); i++ {
src := meleeBullets[i]
if nil == src || TERMINATING_BULLET_LOCAL_ID == src.BattleAttr.BulletLocalId {
break
}
CloneMeleeBullet(src.BlState, src.FramesInBlState, src.BattleAttr.BulletLocalId, src.BattleAttr.OriginatedRenderFrameId, src.BattleAttr.OffenderJoinIndex, src.BattleAttr.TeamId, src.Bullet, dst.MeleeBullets[i])
}
for i := 0; i < len(fireballBullets); i++ {
src := fireballBullets[i]
if nil == src || TERMINATING_BULLET_LOCAL_ID == src.BattleAttr.BulletLocalId {
break
}
CloneFireballBullet(src.BlState, src.FramesInBlState, src.VirtualGridX, src.VirtualGridY, src.DirX, src.DirY, src.VelX, src.VelY, src.Speed, src.BattleAttr.BulletLocalId, src.BattleAttr.OriginatedRenderFrameId, src.BattleAttr.OffenderJoinIndex, src.BattleAttr.TeamId, src.Bullet, dst.FireballBullets[i])
}
}
func NewPreallocatedRoomDownsyncFrame(roomCapacity, preallocMeleeBulletCount int, preallocFireballBulletCount int) *RoomDownsyncFrame {
preallocatedPlayers := make([]*PlayerDownsync, roomCapacity)
for i := 0; i < roomCapacity; i++ {
preallocatedPlayer := NewPlayerDownsync(TERMINATING_PLAYER_ID, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, false, 0, 0, false, 0, 0, 0, 0)
preallocatedPlayers[i] = preallocatedPlayer
}
preallocatedMeleeBullets := make([]*MeleeBullet, preallocMeleeBulletCount)
for i := 0; i < preallocMeleeBulletCount; i++ {
preallocatedMelee := NewMeleeBullet(TERMINATING_BULLET_LOCAL_ID, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0)
preallocatedMeleeBullets[i] = preallocatedMelee
}
preallocatedFireballBullets := make([]*FireballBullet, preallocFireballBulletCount)
for i := 0; i < preallocFireballBulletCount; i++ {
preallocatedFireball := NewFireballBullet(TERMINATING_BULLET_LOCAL_ID, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
preallocatedFireballBullets[i] = preallocatedFireball
}
return &RoomDownsyncFrame{
Id: TERMINATING_RENDER_FRAME_ID,
BulletLocalIdCounter: TERMINATING_BULLET_LOCAL_ID,
PlayersArr: preallocatedPlayers,
MeleeBullets: preallocatedMeleeBullets,
FireballBullets: preallocatedFireballBullets,
}
}

View File

@@ -1,6 +1,6 @@
package battle package battle
type SkillMapperType func(patternId int, currPlayerDownsync *PlayerDownsync) int type SkillMapperType = func(patternId int, currPlayerDownsync *PlayerDownsync, speciesId int) int
type CharacterConfig struct { type CharacterConfig struct {
SpeciesId int SpeciesId int
@@ -31,6 +31,118 @@ type CharacterConfig struct {
SkillMapper SkillMapperType SkillMapper SkillMapperType
} }
func defaultSkillMapper(patternId int, currPlayerDownsync *PlayerDownsync, speciesId int) int {
switch speciesId {
case 0:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 255
} else {
return 1
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 15
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 12
}
}
// By default no skill can be fired
return NO_SKILL
case 1:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 256
} else {
return 4
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 16
}
} else if 5 == patternId {
// Air dash allowed for this character
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
return 13
}
// By default no skill can be fired
return NO_SKILL
case 4096:
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 257
} else {
return 7
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 2 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 11
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 10
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 14
}
}
// By default no skill can be fired
return NO_SKILL
}
return NO_SKILL
}
var Characters = map[int]*CharacterConfig{ var Characters = map[int]*CharacterConfig{
0: &CharacterConfig{ 0: &CharacterConfig{
SpeciesId: 0, SpeciesId: 0,
@@ -58,41 +170,7 @@ var Characters = map[int]*CharacterConfig{
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO), WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO), WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 255
} else {
return 1
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 15
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 12
}
}
// By default no skill can be fired
return NO_SKILL
},
}, },
1: &CharacterConfig{ 1: &CharacterConfig{
SpeciesId: 1, SpeciesId: 1,
@@ -107,7 +185,7 @@ var Characters = map[int]*CharacterConfig{
GetUpInvinsibleFrames: int32(10), GetUpInvinsibleFrames: int32(10),
GetUpFramesToRecover: int32(27), GetUpFramesToRecover: int32(27),
Speed: int32(float64(2.19) * WORLD_TO_VIRTUAL_GRID_RATIO), // I don't know why "2.2" is so special that it throws a compile error Speed: int32(float64(2.2) * WORLD_TO_VIRTUAL_GRID_RATIO), // I don't know why "2.2" is so special that it throws a compile error
JumpingInitVelY: int32(float64(7.5) * WORLD_TO_VIRTUAL_GRID_RATIO), JumpingInitVelY: int32(float64(7.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
JumpingFramesToRecover: int32(2), JumpingFramesToRecover: int32(2),
@@ -120,40 +198,7 @@ var Characters = map[int]*CharacterConfig{
WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO), WallJumpingInitVelY: int32(float64(7) * WORLD_TO_VIRTUAL_GRID_RATIO),
WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO), WallSlidingVelY: int32(float64(-1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 256
} else {
return 4
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 16
}
} else if 5 == patternId {
// Air dash allowed for this character
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
return 13
}
// By default no skill can be fired
return NO_SKILL
},
}, },
4096: &CharacterConfig{ 4096: &CharacterConfig{
SpeciesId: 4096, SpeciesId: 4096,
@@ -177,45 +222,7 @@ var Characters = map[int]*CharacterConfig{
DashingEnabled: true, DashingEnabled: true,
OnWallEnabled: false, OnWallEnabled: false,
SkillMapper: func(patternId int, currPlayerDownsync *PlayerDownsync) int { SkillMapper: defaultSkillMapper,
if 1 == patternId {
if 0 == currPlayerDownsync.FramesToRecover {
if currPlayerDownsync.InAir {
return 257
} else {
return 7
}
} else {
// Now that "0 < FramesToRecover", we're only able to fire any skill if it's a cancellation
if skillConfig, existent1 := skills[int(currPlayerDownsync.ActiveSkillId)]; existent1 {
switch v := skillConfig.Hits[currPlayerDownsync.ActiveSkillHit].(type) {
case *MeleeBullet:
if v.Bullet.CancellableStFrame <= currPlayerDownsync.FramesInChState && currPlayerDownsync.FramesInChState < v.Bullet.CancellableEdFrame {
if nextSkillId, existent2 := v.Bullet.CancelTransit[patternId]; existent2 {
return nextSkillId
}
}
}
}
}
} else if 2 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 11
}
} else if 3 == patternId {
if 0 == currPlayerDownsync.FramesToRecover && !currPlayerDownsync.InAir {
return 10
}
} else if 5 == patternId {
// Dashing is already constrained by "FramesToRecover & CapturedByInertia" in "deriveOpPattern"
if !currPlayerDownsync.InAir {
return 14
}
}
// By default no skill can be fired
return NO_SKILL
},
}, },
} }
@@ -226,7 +233,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -234,7 +241,7 @@ var skills = map[int]*Skill{
HitStunFrames: int32(13), HitStunFrames: int32(13),
BlockStunFrames: int32(9), BlockStunFrames: int32(9),
Damage: int32(5), Damage: int32(5),
SelfLockVelX: int32(float64(0.05) * WORLD_TO_VIRTUAL_GRID_RATIO), SelfLockVelX: int32(float64(0.1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SelfLockVelY: NO_LOCK_VEL, SelfLockVelY: NO_LOCK_VEL,
PushbackVelX: int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO), PushbackVelX: int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
PushbackVelY: int32(0), PushbackVelY: int32(0),
@@ -261,7 +268,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -295,7 +302,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(50), RecoveryFramesOnHit: int32(50),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(8), StartupFrames: int32(8),
@@ -324,7 +331,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -332,7 +339,7 @@ var skills = map[int]*Skill{
HitStunFrames: int32(13), HitStunFrames: int32(13),
BlockStunFrames: int32(9), BlockStunFrames: int32(9),
Damage: int32(5), Damage: int32(5),
SelfLockVelX: int32(float64(0.05) * WORLD_TO_VIRTUAL_GRID_RATIO), SelfLockVelX: int32(float64(0.1) * WORLD_TO_VIRTUAL_GRID_RATIO),
SelfLockVelY: NO_LOCK_VEL, SelfLockVelY: NO_LOCK_VEL,
PushbackVelX: int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO), PushbackVelX: int32(float64(0.5) * WORLD_TO_VIRTUAL_GRID_RATIO),
PushbackVelY: int32(0), PushbackVelY: int32(0),
@@ -359,7 +366,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -393,7 +400,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(45), RecoveryFramesOnHit: int32(45),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(8), StartupFrames: int32(8),
@@ -422,7 +429,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK1, BoundChState: ATK_CHARACTER_STATE_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -457,7 +464,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(36), RecoveryFramesOnHit: int32(36),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK2, BoundChState: ATK_CHARACTER_STATE_ATK2,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(18), StartupFrames: int32(18),
@@ -491,7 +498,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(40), RecoveryFramesOnHit: int32(40),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK3, BoundChState: ATK_CHARACTER_STATE_ATK3,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(7), StartupFrames: int32(7),
@@ -520,7 +527,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(38), RecoveryFramesOnHit: int32(38),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(6) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(6) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -550,7 +557,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(60), RecoveryFramesOnHit: int32(60),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK5, BoundChState: ATK_CHARACTER_STATE_ATK5,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -579,7 +586,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(10), RecoveryFramesOnHit: int32(10),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -606,7 +613,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(12), RecoveryFramesOnHit: int32(12),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -633,7 +640,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(8), RecoveryFramesOnHit: int32(8),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_DASHING, BoundChState: ATK_CHARACTER_STATE_DASHING,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(4), StartupFrames: int32(4),
@@ -660,7 +667,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(48), RecoveryFramesOnHit: int32(48),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -690,7 +697,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(60), RecoveryFramesOnHit: int32(60),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_ATK4, BoundChState: ATK_CHARACTER_STATE_ATK4,
Hits: []interface{}{ Hits: []AnyBullet{
&FireballBullet{ &FireballBullet{
Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO), Speed: int32(float64(4) * WORLD_TO_VIRTUAL_GRID_RATIO),
Bullet: &BulletConfig{ Bullet: &BulletConfig{
@@ -720,7 +727,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -749,7 +756,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(20), RecoveryFramesOnHit: int32(20),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(3), StartupFrames: int32(3),
@@ -778,7 +785,7 @@ var skills = map[int]*Skill{
RecoveryFramesOnHit: int32(30), RecoveryFramesOnHit: int32(30),
ReleaseTriggerType: int32(1), ReleaseTriggerType: int32(1),
BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1, BoundChState: ATK_CHARACTER_STATE_INAIR_ATK1,
Hits: []interface{}{ Hits: []AnyBullet{
&MeleeBullet{ &MeleeBullet{
Bullet: &BulletConfig{ Bullet: &BulletConfig{
StartupFrames: int32(4), StartupFrames: int32(4),

264
jsexport/battle/getter.go Normal file
View File

@@ -0,0 +1,264 @@
package battle
// CharacterConfig
func (c *CharacterConfig) GetSpeed() int32 {
return c.Speed
}
func (c *CharacterConfig) GetSpeciesId() int {
return c.SpeciesId
}
func (c *CharacterConfig) GetSpeciesName() string {
return c.SpeciesName
}
// InputFrameDownsync
func (ifd *InputFrameDownsync) GetInputFrameId() int32 {
return ifd.InputFrameId
}
func (ifd *InputFrameDownsync) GetInputList() []uint64 {
return ifd.InputList
}
func (ifd *InputFrameDownsync) GetConfirmedList() uint64 {
return ifd.ConfirmedList
}
// PlayerDownsync
func (p *PlayerDownsync) GetId() int32 {
return p.Id
}
func (p *PlayerDownsync) GetJoinIndex() int32 {
return p.JoinIndex
}
func (p *PlayerDownsync) GetVirtualGridX() int32 {
return p.VirtualGridX
}
func (p *PlayerDownsync) GetVirtualGridY() int32 {
return p.VirtualGridY
}
func (p *PlayerDownsync) GetDirX() int32 {
return p.DirX
}
func (p *PlayerDownsync) GetDirY() int32 {
return p.DirY
}
func (p *PlayerDownsync) GetVelX() int32 {
return p.VelX
}
func (p *PlayerDownsync) GetVelY() int32 {
return p.VelY
}
func (p *PlayerDownsync) GetSpeed() int32 {
return p.Speed
}
func (p *PlayerDownsync) GetHp() int32 {
return p.Hp
}
func (p *PlayerDownsync) GetMaxHp() int32 {
return p.MaxHp
}
func (p *PlayerDownsync) GetCharacterState() int32 {
return p.CharacterState
}
func (p *PlayerDownsync) GetFramesToRecover() int32 {
return p.FramesToRecover
}
func (p *PlayerDownsync) GetFramesInChState() int32 {
return p.FramesInChState
}
func (p *PlayerDownsync) GetInAir() bool {
return p.InAir
}
func (p *PlayerDownsync) GetOnWall() bool {
return p.OnWall
}
func (p *PlayerDownsync) GetOnWallNormX() int32 {
return p.OnWallNormX
}
func (p *PlayerDownsync) GetColliderRadius() int32 {
return p.ColliderRadius
}
// MeleeBullet
func (b *MeleeBullet) GetBlState() int32 {
return b.BlState
}
func (b *MeleeBullet) GetFramesInBlState() int32 {
return b.FramesInBlState
}
func (b *MeleeBullet) GetBulletLocalId() int32 {
return b.BattleAttr.BulletLocalId
}
func (b *MeleeBullet) GetOffenderJoinIndex() int32 {
return b.BattleAttr.OffenderJoinIndex
}
func (b *MeleeBullet) GetOriginatedRenderFrameId() int32 {
return b.BattleAttr.OriginatedRenderFrameId
}
func (b *MeleeBullet) GetStartupFrames() int32 {
return b.Bullet.StartupFrames
}
func (b *MeleeBullet) GetActiveFrames() int32 {
return b.Bullet.ActiveFrames
}
func (b *MeleeBullet) GetHitboxSizeX() int32 {
return b.Bullet.HitboxSizeX
}
func (b *MeleeBullet) GetHitboxSizeY() int32 {
return b.Bullet.HitboxSizeY
}
func (b *MeleeBullet) GetHitboxOffsetX() int32 {
return b.Bullet.HitboxOffsetX
}
func (b *MeleeBullet) GetHitboxOffsetY() int32 {
return b.Bullet.HitboxOffsetY
}
func (b *MeleeBullet) GetExplosionFrames() int32 {
return b.Bullet.ExplosionFrames
}
func (b *MeleeBullet) GetSpeciesId() int32 {
return b.Bullet.SpeciesId
}
// FireballBullet
func (p *FireballBullet) GetVirtualGridX() int32 {
return p.VirtualGridX
}
func (p *FireballBullet) GetVirtualGridY() int32 {
return p.VirtualGridY
}
func (p *FireballBullet) GetDirX() int32 {
return p.DirX
}
func (p *FireballBullet) GetDirY() int32 {
return p.DirY
}
func (p *FireballBullet) GetVelX() int32 {
return p.VelX
}
func (p *FireballBullet) GetVelY() int32 {
return p.VelY
}
func (p *FireballBullet) GetSpeed() int32 {
return p.Speed
}
func (b *FireballBullet) GetBlState() int32 {
return b.BlState
}
func (b *FireballBullet) GetFramesInBlState() int32 {
return b.FramesInBlState
}
func (b *FireballBullet) GetBulletLocalId() int32 {
return b.BattleAttr.BulletLocalId
}
func (b *FireballBullet) GetOffenderJoinIndex() int32 {
return b.BattleAttr.OffenderJoinIndex
}
func (b *FireballBullet) GetOriginatedRenderFrameId() int32 {
return b.BattleAttr.OriginatedRenderFrameId
}
func (b *FireballBullet) GetStartupFrames() int32 {
return b.Bullet.StartupFrames
}
func (b *FireballBullet) GetActiveFrames() int32 {
return b.Bullet.ActiveFrames
}
func (b *FireballBullet) GetHitboxSizeX() int32 {
return b.Bullet.HitboxSizeX
}
func (b *FireballBullet) GetHitboxSizeY() int32 {
return b.Bullet.HitboxSizeY
}
func (b *FireballBullet) GetHitboxOffsetX() int32 {
return b.Bullet.HitboxOffsetX
}
func (b *FireballBullet) GetHitboxOffsetY() int32 {
return b.Bullet.HitboxOffsetY
}
func (b *FireballBullet) GetExplosionFrames() int32 {
return b.Bullet.ExplosionFrames
}
func (b *FireballBullet) GetSpeciesId() int32 {
return b.Bullet.SpeciesId
}
// RoomDownsyncFrame
func (r *RoomDownsyncFrame) GetId() int32 {
return r.Id
}
func (r *RoomDownsyncFrame) GetCountdownNanos() int64 {
return r.CountdownNanos
}
func (r *RoomDownsyncFrame) GetBackendUnconfirmedMask() uint64 {
return r.BackendUnconfirmedMask
}
func (r *RoomDownsyncFrame) GetBulletLocalIdCounter() int32 {
return r.BulletLocalIdCounter
}
func (r *RoomDownsyncFrame) GetShouldForceResync() bool {
return r.ShouldForceResync
}
func (r *RoomDownsyncFrame) GetPlayersArr() []*PlayerDownsync {
return r.PlayersArr
}
func (r *RoomDownsyncFrame) GetMeleeBullets() []*MeleeBullet {
return r.MeleeBullets
}
func (r *RoomDownsyncFrame) GetFireballBullets() []*FireballBullet {
return r.FireballBullets
}

View File

@@ -2,6 +2,8 @@ package battle
// TODO: Replace all "int32", "int64", "uint32" and "uint64" with just "int" for better performance in JavaScript! Reference https://github.com/gopherjs/gopherjs#performance-tips // TODO: Replace all "int32", "int64", "uint32" and "uint64" with just "int" for better performance in JavaScript! Reference https://github.com/gopherjs/gopherjs#performance-tips
type AnyBullet interface{}
type Vec2D struct { type Vec2D struct {
X float64 X float64
Y float64 Y float64
@@ -132,7 +134,7 @@ type Skill struct {
RecoveryFramesOnHit int32 RecoveryFramesOnHit int32
ReleaseTriggerType int32 // 1: rising-edge, 2: falling-edge ReleaseTriggerType int32 // 1: rising-edge, 2: falling-edge
BoundChState int32 BoundChState int32
Hits []interface{} // Hits within a "Skill" are automatically triggered Hits []AnyBullet // Hits within a "Skill" are automatically triggered
// [WARN] Multihit of a fireball is more difficult to handle than that of melee, because we have to count from the fireball's first hit; the situation becomes even more complicated when a multihit fireball is in a crowd -- remains to be designed // [WARN] Multihit of a fireball is more difficult to handle than that of melee, because we have to count from the fireball's first hit; the situation becomes even more complicated when a multihit fireball is in a crowd -- remains to be designed
} }

View File

@@ -6,8 +6,23 @@ import (
"resolv" "resolv"
) )
/*
[WARNING] Should avoid using "MakeFullWrapper" as much as possible, and completely remove its usage in 60fps calls like "update(dt)" on frontend!
*/
func NewDynamicRectangleColliders(cnt int) []*js.Object {
ret := make([]*js.Object, cnt)
for i := 0; i < cnt; i++ {
ret[i] = js.MakeWrapper(GenerateRectCollider(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, nil, ""))
}
return ret
}
func NewCollisionHolder() *js.Object {
return js.MakeWrapper(resolv.NewCollision())
}
func NewInputFrameDownsync(inputFrameId int32, inputList []uint64, confirmedList uint64) *js.Object { func NewInputFrameDownsync(inputFrameId int32, inputList []uint64, confirmedList uint64) *js.Object {
return js.MakeFullWrapper(&InputFrameDownsync{ return js.MakeWrapper(&InputFrameDownsync{
InputFrameId: inputFrameId, InputFrameId: inputFrameId,
InputList: inputList, InputList: inputList,
ConfirmedList: confirmedList, ConfirmedList: confirmedList,
@@ -15,7 +30,7 @@ func NewInputFrameDownsync(inputFrameId int32, inputList []uint64, confirmedList
} }
func NewRingBufferJs(n int32) *js.Object { func NewRingBufferJs(n int32) *js.Object {
return js.MakeFullWrapper(NewRingBuffer(n)) return js.MakeWrapper(resolv.NewRingBuffer(n))
} }
func NewCollisionSpaceJs(spaceW, spaceH, minStepW, minStepH int) *js.Object { func NewCollisionSpaceJs(spaceW, spaceH, minStepW, minStepH int) *js.Object {
@@ -23,14 +38,14 @@ func NewCollisionSpaceJs(spaceW, spaceH, minStepW, minStepH int) *js.Object {
} }
func NewVec2DJs(x, y float64) *js.Object { func NewVec2DJs(x, y float64) *js.Object {
return js.MakeFullWrapper(&Vec2D{ return js.MakeWrapper(&Vec2D{
X: x, X: x,
Y: y, Y: y,
}) })
} }
func NewPolygon2DJs(anchor *Vec2D, points []*Vec2D) *js.Object { func NewPolygon2DJs(anchor *Vec2D, points []*Vec2D) *js.Object {
return js.MakeFullWrapper(&Polygon2D{ return js.MakeWrapper(&Polygon2D{
Anchor: anchor, Anchor: anchor,
Points: points, Points: points,
}) })
@@ -43,36 +58,7 @@ func NewBarrierJs(boundary *Polygon2D) *js.Object {
} }
func NewPlayerDownsyncJs(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, framesToRecover, framesInChState, activeSkillId, activeSkillHit, framesInvinsible, speed, battleState, characterState, joinIndex, hp, maxHp, colliderRadius int32, inAir, onWall bool, onWallNormX, onWallNormY int32, capturedByInertia bool, bulletTeamId, chCollisionTeamId int32, revivalVirtualGridX, revivalVirtualGridY int32) *js.Object { func NewPlayerDownsyncJs(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, framesToRecover, framesInChState, activeSkillId, activeSkillHit, framesInvinsible, speed, battleState, characterState, joinIndex, hp, maxHp, colliderRadius int32, inAir, onWall bool, onWallNormX, onWallNormY int32, capturedByInertia bool, bulletTeamId, chCollisionTeamId int32, revivalVirtualGridX, revivalVirtualGridY int32) *js.Object {
return js.MakeWrapper(&PlayerDownsync{ return js.MakeWrapper(NewPlayerDownsync(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, framesToRecover, framesInChState, activeSkillId, activeSkillHit, framesInvinsible, speed, battleState, characterState, joinIndex, hp, maxHp, colliderRadius, inAir, onWall, onWallNormX, onWallNormY, capturedByInertia, bulletTeamId, chCollisionTeamId, revivalVirtualGridX, revivalVirtualGridY))
Id: id,
VirtualGridX: virtualGridX,
VirtualGridY: virtualGridY,
DirX: dirX,
DirY: dirY,
VelX: velX,
VelY: velY,
FramesToRecover: framesToRecover,
FramesInChState: framesInChState,
ActiveSkillId: activeSkillId,
ActiveSkillHit: activeSkillHit,
FramesInvinsible: framesInvinsible,
Speed: speed,
BattleState: battleState,
CharacterState: characterState,
JoinIndex: joinIndex,
Hp: hp,
MaxHp: maxHp,
ColliderRadius: colliderRadius,
InAir: inAir,
OnWall: onWall,
OnWallNormX: onWallNormX,
OnWallNormY: onWallNormY,
CapturedByInertia: capturedByInertia,
BulletTeamId: bulletTeamId,
ChCollisionTeamId: chCollisionTeamId,
RevivalVirtualGridX: revivalVirtualGridX,
RevivalVirtualGridY: revivalVirtualGridY,
})
} }
func NewMeleeBulletJs(bulletLocalId, originatedRenderFrameId, offenderJoinIndex, startupFrames, cancellableStFrame, cancellableEdFrame, activeFrames, hitStunFrames, blockStunFrames, pushbackVelX, pushbackVelY, damage, selfLockVelX, selfLockVelY, hitboxOffsetX, hitboxOffsetY, hitboxSizeX, hitboxSizeY int32, blowUp bool, teamId, blState, framesInBlState, explosionFrames, speciesId int32) *js.Object { func NewMeleeBulletJs(bulletLocalId, originatedRenderFrameId, offenderJoinIndex, startupFrames, cancellableStFrame, cancellableEdFrame, activeFrames, hitStunFrames, blockStunFrames, pushbackVelX, pushbackVelY, damage, selfLockVelX, selfLockVelY, hitboxOffsetX, hitboxOffsetY, hitboxSizeX, hitboxSizeY int32, blowUp bool, teamId, blState, framesInBlState, explosionFrames, speciesId int32) *js.Object {
@@ -84,7 +70,7 @@ func NewFireballBulletJs(bulletLocalId, originatedRenderFrameId, offenderJoinInd
} }
func NewNpcPatrolCue(flAct, frAct uint64, x, y float64) *js.Object { func NewNpcPatrolCue(flAct, frAct uint64, x, y float64) *js.Object {
return js.MakeFullWrapper(&NpcPatrolCue{ return js.MakeWrapper(&NpcPatrolCue{
FlAct: flAct, FlAct: flAct,
FrAct: frAct, FrAct: frAct,
X: x, X: x,
@@ -93,45 +79,23 @@ func NewNpcPatrolCue(flAct, frAct uint64, x, y float64) *js.Object {
} }
func NewRoomDownsyncFrameJs(id int32, playersArr []*PlayerDownsync, bulletLocalIdCounter int32, meleeBullets []*MeleeBullet, fireballBullets []*FireballBullet) *js.Object { func NewRoomDownsyncFrameJs(id int32, playersArr []*PlayerDownsync, bulletLocalIdCounter int32, meleeBullets []*MeleeBullet, fireballBullets []*FireballBullet) *js.Object {
// [WARNING] Avoid using "pb.RoomDownsyncFrame" here, in practive "MakeFullWrapper" doesn't expose the public fields for a "protobuf struct" as expected and requires helper functions like "GetCollisionSpaceObjsJs". preallocatedRdf := NewPreallocatedRoomDownsyncFrame(len(playersArr), 64, 64)
return js.MakeFullWrapper(&RoomDownsyncFrame{ CloneRoomDownsyncFrame(id, playersArr, bulletLocalIdCounter, meleeBullets, fireballBullets, preallocatedRdf)
Id: id, return js.MakeWrapper(preallocatedRdf)
PlayersArr: playersArr,
BulletLocalIdCounter: bulletLocalIdCounter,
MeleeBullets: meleeBullets,
FireballBullets: fireballBullets,
})
} }
func GetCollisionSpaceObjsJs(space *resolv.Space) []*js.Object { func GetCollisionSpaceObjsJs(space *resolv.Space) []*js.Object {
// [WARNING] We couldn't just use the existing method "space.Objects()" to access them in JavaScript, there'd a stackoverflow error // [WARNING] We couldn't just use the existing method "space.Objects()" to access them in JavaScript, there'd a stackoverflow error
objs := space.Objects() objs := space.Objects()
ret := make([]*js.Object, 0, len(objs)) ret := make([]*js.Object, len(objs))
for _, obj := range objs { for i, obj := range objs {
ret = append(ret, js.MakeFullWrapper(obj)) ret[i] = js.MakeWrapper(obj)
} }
return ret return ret
} }
func GenerateRectColliderJs(wx, wy, w, h, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *js.Object {
/*
[WARNING] It's important to note that we don't need "js.MakeFullWrapper" for a call sequence as follows.
```
var space = gopkgs.NewCollisionSpaceJs(2048, 2048, 8, 8);
var a = gopkgs.GenerateRectColliderJs(189, 497, 48, 48, spaceOffsetX, spaceOffsetY, "Player");
space.Add(a);
```
The "space" variable doesn't need access to the field of "a" in JavaScript level to run "space.Add(...)" method, which is good.
However, the full wrapper access here is used for updating "collider.X/collider.Y" at JavaScript runtime.
*/
topPadding, bottomPadding, leftPadding, rightPadding := SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP, SNAP_INTO_PLATFORM_OVERLAP
return js.MakeFullWrapper(GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, data, tag))
}
func GenerateConvexPolygonColliderJs(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *js.Object { func GenerateConvexPolygonColliderJs(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *js.Object {
return js.MakeFullWrapper(GenerateConvexPolygonCollider(unalignedSrc, spaceOffsetX, spaceOffsetY, data, tag)) return js.MakeWrapper(GenerateConvexPolygonCollider(unalignedSrc, spaceOffsetX, spaceOffsetY, data, tag))
} }
func GetCharacterConfigsOrderedByJoinIndex(speciesIdList []int) []*js.Object { func GetCharacterConfigsOrderedByJoinIndex(speciesIdList []int) []*js.Object {
@@ -142,41 +106,111 @@ func GetCharacterConfigsOrderedByJoinIndex(speciesIdList []int) []*js.Object {
return ret return ret
} }
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(inputsBuffer *RingBuffer, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig) *js.Object { func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
// We need access to all fields of RoomDownsyncFrame for displaying in frontend // We need access to all fields of RoomDownsyncFrame for displaying in frontend
return js.MakeFullWrapper(ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrame, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex)) return ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrameId, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex, renderFrameBuffer, collision, effPushbacks, hardPushbackNormsArr, jumpedOrNotList, dynamicRectangleColliders, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
}
func GetRoomDownsyncFrame(renderFrameBuffer *resolv.RingBuffer, frameId int32) *js.Object {
// [WARNING] Calling "renderFrameBuffer.GetByFrameId(frameId)" directly from transpiled frontend code would automatically invoke the expensive "$externalize" and "$mapArray"! See profiling result for more details.
candidate := renderFrameBuffer.GetByFrameId(frameId)
if nil == candidate {
return nil
}
return js.MakeWrapper(candidate.(*RoomDownsyncFrame))
}
func GetInputFrameDownsync(inputsBuffer *resolv.RingBuffer, inputFrameId int32) *js.Object {
candidate := inputsBuffer.GetByFrameId(inputFrameId)
if nil == candidate {
return nil
}
return js.MakeWrapper(candidate.(*InputFrameDownsync))
}
func GetInput(ifd *InputFrameDownsync, i int) uint64 {
// [WARNING] Calling "ifd.GetInputList()" directly from transpiled frontend code would make a copy of the array.
return ifd.InputList[i]
}
func SetInputFrameId(ifd *InputFrameDownsync, newVal int32) bool {
// [WARNING] This function should be only used by frontend which is single-threaded; on the backend more rigorous thread-safety concerns are taken care of by proper locking.
ifd.InputFrameId = newVal
return true
}
func SetInput(ifd *InputFrameDownsync, i int, newVal uint64) bool {
// [WARNING] This function should be only used by frontend which is single-threaded; on the backend more rigorous thread-safety concerns are taken care of by proper locking.
if i >= len(ifd.InputList) {
return false
}
ifd.InputList[i] = newVal
return true
}
func SetConfirmedList(ifd *InputFrameDownsync, newVal uint64) bool {
// [WARNING] This function should be only used by frontend which is single-threaded; on the backend more rigorous thread-safety concerns are taken care of by proper locking.
ifd.ConfirmedList = newVal
return true
}
func GetPlayer(rdf *RoomDownsyncFrame, i int) *js.Object {
// [WARNING] Calling "rdf.GetPlayersArr()" directly from transpiled frontend code would automatically invoke the expensive "$externalize" and "$mapArray"! See profiling result for more details.
return js.MakeWrapper(rdf.PlayersArr[i])
}
func GetMeleeBullet(rdf *RoomDownsyncFrame, i int) *js.Object {
if TERMINATING_BULLET_LOCAL_ID == rdf.MeleeBullets[i].GetBulletLocalId() {
return nil
}
return js.MakeWrapper(rdf.MeleeBullets[i])
}
func GetFireballBullet(rdf *RoomDownsyncFrame, i int) *js.Object {
if TERMINATING_BULLET_LOCAL_ID == rdf.FireballBullets[i].GetBulletLocalId() {
return nil
}
return js.MakeWrapper(rdf.FireballBullets[i])
} }
func main() { func main() {
js.Global.Set("gopkgs", map[string]interface{}{ js.Global.Set("gopkgs", map[string]interface{}{
"NewVec2DJs": NewVec2DJs, "NewVec2DJs": NewVec2DJs,
"NewPolygon2DJs": NewPolygon2DJs, "NewPolygon2DJs": NewPolygon2DJs,
"NewBarrierJs": NewBarrierJs, "NewBarrierJs": NewBarrierJs,
"NewPlayerDownsyncJs": NewPlayerDownsyncJs, "NewPlayerDownsyncJs": NewPlayerDownsyncJs,
"NewMeleeBulletJs": NewMeleeBulletJs, "NewMeleeBulletJs": NewMeleeBulletJs,
"NewFireballBulletJs": NewFireballBulletJs, "NewFireballBulletJs": NewFireballBulletJs,
"NewNpcPatrolCue": NewNpcPatrolCue, "NewNpcPatrolCue": NewNpcPatrolCue,
"NewRoomDownsyncFrameJs": NewRoomDownsyncFrameJs, "NewRoomDownsyncFrameJs": NewRoomDownsyncFrameJs,
"NewCollisionSpaceJs": NewCollisionSpaceJs, "NewCollisionSpaceJs": NewCollisionSpaceJs,
"NewInputFrameDownsync": NewInputFrameDownsync, "NewCollisionHolder": NewCollisionHolder,
"NewRingBufferJs": NewRingBufferJs, "NewInputFrameDownsync": NewInputFrameDownsync,
"GenerateRectColliderJs": GenerateRectColliderJs, "NewRingBufferJs": NewRingBufferJs,
"GenerateConvexPolygonColliderJs": GenerateConvexPolygonColliderJs, "GenerateConvexPolygonColliderJs": GenerateConvexPolygonColliderJs,
"GetCollisionSpaceObjsJs": GetCollisionSpaceObjsJs, "GetCollisionSpaceObjsJs": GetCollisionSpaceObjsJs,
"WorldToPolygonColliderBLPos": WorldToPolygonColliderBLPos, // No need to wrap primitive return types "WorldToPolygonColliderBLPos": WorldToPolygonColliderBLPos, // No need to wrap primitive return types
"PolygonColliderBLToWorldPos": PolygonColliderBLToWorldPos, "PolygonColliderBLToWorldPos": PolygonColliderBLToWorldPos,
"WorldToVirtualGridPos": WorldToVirtualGridPos, "WorldToVirtualGridPos": WorldToVirtualGridPos,
"VirtualGridToWorldPos": VirtualGridToWorldPos, "VirtualGridToWorldPos": VirtualGridToWorldPos,
"GetCharacterConfigsOrderedByJoinIndex": GetCharacterConfigsOrderedByJoinIndex, "GetCharacterConfigsOrderedByJoinIndex": GetCharacterConfigsOrderedByJoinIndex,
"ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs": ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs, "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs": ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs,
"ConvertToDelayedInputFrameId": ConvertToDelayedInputFrameId, "ConvertToDelayedInputFrameId": ConvertToDelayedInputFrameId,
"ConvertToNoDelayInputFrameId": ConvertToNoDelayInputFrameId, "ConvertToNoDelayInputFrameId": ConvertToNoDelayInputFrameId,
"ConvertToFirstUsedRenderFrameId": ConvertToFirstUsedRenderFrameId, "ConvertToFirstUsedRenderFrameId": ConvertToFirstUsedRenderFrameId,
"ConvertToLastUsedRenderFrameId": ConvertToLastUsedRenderFrameId, "ConvertToLastUsedRenderFrameId": ConvertToLastUsedRenderFrameId,
"ShouldGenerateInputFrameUpsync": ShouldGenerateInputFrameUpsync, "ShouldGenerateInputFrameUpsync": ShouldGenerateInputFrameUpsync,
"IsMeleeBulletActive": IsMeleeBulletActive, "IsGeneralBulletActive": IsGeneralBulletActive,
"IsMeleeBulletAlive": IsMeleeBulletAlive, "GetRoomDownsyncFrame": GetRoomDownsyncFrame,
"IsFireballBulletActive": IsFireballBulletActive, "GetInputFrameDownsync": GetInputFrameDownsync,
"IsFireballBulletAlive": IsFireballBulletAlive, "GetPlayer": GetPlayer,
"GetMeleeBullet": GetMeleeBullet,
"GetFireballBullet": GetFireballBullet,
"GetInput": GetInput,
"NewDynamicRectangleColliders": NewDynamicRectangleColliders,
"SetInputFrameId": SetInputFrameId,
"SetInput": SetInput,
"SetConfirmedList": SetConfirmedList,
}) })
} }

View File

@@ -1,164 +0,0 @@
// This file contains code from the gonum repository:
// https://github.com/gonum/gonum/blob/master/internal/asm/f64/scalunitaryto_amd64.s
// it is distributed under the 3-Clause BSD license:
//
// Copyright ©2013 The Gonum Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the Gonum project nor the names of its authors and
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Some of the loop unrolling code is copied from:
// http://golang.org/src/math/big/arith_amd64.s
// which is distributed under these terms:
//
// Copyright (c) 2012 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build !noasm
#include "textflag.h"
#define X_PTR SI
#define Y_PTR DX
#define DST_PTR DI
#define IDX AX
#define LEN CX
#define TAIL BX
#define ALPHA X0
#define ALPHA_2 X1
// func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64)
TEXT ·axpyUnitaryTo(SB), NOSPLIT, $0
MOVQ dst_base+0(FP), DST_PTR // DST_PTR := &dst
MOVQ x_base+32(FP), X_PTR // X_PTR := &x
MOVQ y_base+56(FP), Y_PTR // Y_PTR := &y
MOVQ x_len+40(FP), LEN // LEN = min( len(x), len(y), len(dst) )
CMPQ y_len+64(FP), LEN
CMOVQLE y_len+64(FP), LEN
CMPQ dst_len+8(FP), LEN
CMOVQLE dst_len+8(FP), LEN
CMPQ LEN, $0
JE end // if LEN == 0 { return }
XORQ IDX, IDX // IDX = 0
MOVSD alpha+24(FP), ALPHA
SHUFPD $0, ALPHA, ALPHA // ALPHA := { alpha, alpha }
MOVQ Y_PTR, TAIL // Check memory alignment
ANDQ $15, TAIL // TAIL = &y % 16
JZ no_trim // if TAIL == 0 { goto no_trim }
// Align on 16-byte boundary
MOVSD (X_PTR), X2 // X2 := x[0]
MULSD ALPHA, X2 // X2 *= a
ADDSD (Y_PTR), X2 // X2 += y[0]
MOVSD X2, (DST_PTR) // y[0] = X2
INCQ IDX // i++
DECQ LEN // LEN--
JZ end // if LEN == 0 { return }
no_trim:
MOVQ LEN, TAIL
ANDQ $7, TAIL // TAIL := n % 8
SHRQ $3, LEN // LEN = floor( n / 8 )
JZ tail_start // if LEN == 0 { goto tail_start }
MOVUPS ALPHA, ALPHA_2 // ALPHA_2 := ALPHA for pipelining
loop: // do {
// y[i] += alpha * x[i] unrolled 8x.
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MOVUPS 16(X_PTR)(IDX*8), X3
MOVUPS 32(X_PTR)(IDX*8), X4
MOVUPS 48(X_PTR)(IDX*8), X5
MULPD ALPHA, X2 // X_i *= alpha
MULPD ALPHA_2, X3
MULPD ALPHA, X4
MULPD ALPHA_2, X5
ADDPD (Y_PTR)(IDX*8), X2 // X_i += y[i]
ADDPD 16(Y_PTR)(IDX*8), X3
ADDPD 32(Y_PTR)(IDX*8), X4
ADDPD 48(Y_PTR)(IDX*8), X5
MOVUPS X2, (DST_PTR)(IDX*8) // y[i] = X_i
MOVUPS X3, 16(DST_PTR)(IDX*8)
MOVUPS X4, 32(DST_PTR)(IDX*8)
MOVUPS X5, 48(DST_PTR)(IDX*8)
ADDQ $8, IDX // i += 8
DECQ LEN
JNZ loop // } while --LEN > 0
CMPQ TAIL, $0 // if TAIL == 0 { return }
JE end
tail_start: // Reset loop registers
MOVQ TAIL, LEN // Loop counter: LEN = TAIL
SHRQ $1, LEN // LEN = floor( TAIL / 2 )
JZ tail_one // if LEN == 0 { goto tail }
tail_two: // do {
MOVUPS (X_PTR)(IDX*8), X2 // X2 = x[i]
MULPD ALPHA, X2 // X2 *= alpha
ADDPD (Y_PTR)(IDX*8), X2 // X2 += y[i]
MOVUPS X2, (DST_PTR)(IDX*8) // y[i] = X2
ADDQ $2, IDX // i += 2
DECQ LEN
JNZ tail_two // } while --LEN > 0
ANDQ $1, TAIL
JZ end // if TAIL == 0 { goto end }
tail_one:
MOVSD (X_PTR)(IDX*8), X2 // X2 = x[i]
MULSD ALPHA, X2 // X2 *= a
ADDSD (Y_PTR)(IDX*8), X2 // X2 += y[i]
MOVSD X2, (DST_PTR)(IDX*8) // y[i] = X2
end:
RET

View File

@@ -1,137 +0,0 @@
// This file contains code from the gonum repository:
// https://github.com/gonum/gonum/blob/master/internal/asm/f64/axpyunitaryto_amd64.s
// it is distributed under the 3-Clause BSD license:
//
// Copyright ©2013 The Gonum Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the Gonum project nor the names of its authors and
// contributors may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Some of the loop unrolling code is copied from:
// http://golang.org/src/math/big/arith_amd64.s
// which is distributed under these terms:
//
// Copyright (c) 2012 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// +build !noasm
#include "textflag.h"
#define MOVDDUP_ALPHA LONG $0x44120FF2; WORD $0x2024 // @ MOVDDUP 32(SP), X0 /*XMM0, 32[RSP]*/
#define X_PTR SI
#define DST_PTR DI
#define IDX AX
#define LEN CX
#define TAIL BX
#define ALPHA X0
#define ALPHA_2 X1
// func scalUnitaryTo(dst []float64, alpha float64, x []float64)
// This function assumes len(dst) >= len(x).
TEXT ·scalUnitaryTo(SB), NOSPLIT, $0
MOVQ x_base+32(FP), X_PTR // X_PTR = &x
MOVQ dst_base+0(FP), DST_PTR // DST_PTR = &dst
MOVDDUP_ALPHA // ALPHA = { alpha, alpha }
MOVQ x_len+40(FP), LEN // LEN = len(x)
CMPQ LEN, $0
JE end // if LEN == 0 { return }
XORQ IDX, IDX // IDX = 0
MOVQ LEN, TAIL
ANDQ $7, TAIL // TAIL = LEN % 8
SHRQ $3, LEN // LEN = floor( LEN / 8 )
JZ tail_start // if LEN == 0 { goto tail_start }
MOVUPS ALPHA, ALPHA_2 // ALPHA_2 = ALPHA for pipelining
loop: // do { // dst[i] = alpha * x[i] unrolled 8x.
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MOVUPS 16(X_PTR)(IDX*8), X3
MOVUPS 32(X_PTR)(IDX*8), X4
MOVUPS 48(X_PTR)(IDX*8), X5
MULPD ALPHA, X2 // X_i *= ALPHA
MULPD ALPHA_2, X3
MULPD ALPHA, X4
MULPD ALPHA_2, X5
MOVUPS X2, (DST_PTR)(IDX*8) // dst[i] = X_i
MOVUPS X3, 16(DST_PTR)(IDX*8)
MOVUPS X4, 32(DST_PTR)(IDX*8)
MOVUPS X5, 48(DST_PTR)(IDX*8)
ADDQ $8, IDX // i += 8
DECQ LEN
JNZ loop // while --LEN > 0
CMPQ TAIL, $0
JE end // if TAIL == 0 { return }
tail_start: // Reset loop counters
MOVQ TAIL, LEN // Loop counter: LEN = TAIL
SHRQ $1, LEN // LEN = floor( TAIL / 2 )
JZ tail_one // if LEN == 0 { goto tail_one }
tail_two: // do {
MOVUPS (X_PTR)(IDX*8), X2 // X_i = x[i]
MULPD ALPHA, X2 // X_i *= ALPHA
MOVUPS X2, (DST_PTR)(IDX*8) // dst[i] = X_i
ADDQ $2, IDX // i += 2
DECQ LEN
JNZ tail_two // while --LEN > 0
ANDQ $1, TAIL
JZ end // if TAIL == 0 { return }
tail_one:
MOVSD (X_PTR)(IDX*8), X2 // X_i = x[i]
MULSD ALPHA, X2 // X_i *= ALPHA
MOVSD X2, (DST_PTR)(IDX*8) // dst[i] = X_i
end:
RET

View File

@@ -1,9 +0,0 @@
//go:build !noasm
// +build !noasm
package resolv
// functions from the gonum package that optimizes arithmetic
// operations on lists of float64 values
func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64)
func scalUnitaryTo(dst []float64, alpha float64, x []float64)

View File

@@ -2,34 +2,36 @@ package resolv
// Cell is used to contain and organize Object information. // Cell is used to contain and organize Object information.
type Cell struct { type Cell struct {
X, Y int // The X and Y position of the cell in the Space - note that this is in Grid position, not World position. X, Y int // The X and Y position of the cell in the Space - note that this is in Grid position, not World position.
Objects []*Object // The Objects that a Cell contains. Objects *RingBuffer // The Objects that a Cell contains.
} }
// newCell creates a new cell at the specified X and Y position. Should not be used directly. // newCell creates a new cell at the specified X and Y position. Should not be used directly.
func newCell(x, y int) *Cell { func newCell(x, y int) *Cell {
return &Cell{ c := &Cell{}
X: x, c.X = x
Y: y, c.Y = y
Objects: []*Object{}, c.Objects = NewRingBuffer(16) // A single cell is so small thus wouldn't have many touching objects simultaneously
} return c
} }
// register registers an object with a Cell. Should not be used directly. // register registers an object with a Cell. Should not be used directly.
func (cell *Cell) register(obj *Object) { func (cell *Cell) register(obj *Object) {
if !cell.Contains(obj) { if !cell.Contains(obj) {
cell.Objects = append(cell.Objects, obj) cell.Objects.Put(obj)
} }
} }
// unregister unregisters an object from a Cell. Should not be used directly. // unregister unregisters an object from a Cell. Should not be used directly.
func (cell *Cell) unregister(obj *Object) { func (cell *Cell) unregister(obj *Object) {
rb := cell.Objects
for i, o := range cell.Objects { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == obj { if o == obj {
cell.Objects[i] = cell.Objects[len(cell.Objects)-1] // swap with the st element
cell.Objects = cell.Objects[:len(cell.Objects)-1] rb.SetByFrameId(rb.GetByFrameId(rb.StFrameId), i)
// pop the current st element
rb.Pop()
break break
} }
@@ -39,7 +41,9 @@ func (cell *Cell) unregister(obj *Object) {
// Contains returns whether a Cell contains the specified Object at its position. // Contains returns whether a Cell contains the specified Object at its position.
func (cell *Cell) Contains(obj *Object) bool { func (cell *Cell) Contains(obj *Object) bool {
for _, o := range cell.Objects { rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == obj { if o == obj {
return true return true
} }
@@ -49,7 +53,9 @@ func (cell *Cell) Contains(obj *Object) bool {
// ContainsTags returns whether a Cell contains an Object that has the specified tag at its position. // ContainsTags returns whether a Cell contains an Object that has the specified tag at its position.
func (cell *Cell) ContainsTags(tags ...string) bool { func (cell *Cell) ContainsTags(tags ...string) bool {
for _, o := range cell.Objects { rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o.HasTags(tags...) { if o.HasTags(tags...) {
return true return true
} }
@@ -59,5 +65,5 @@ func (cell *Cell) ContainsTags(tags ...string) bool {
// Occupied returns whether a Cell contains any Objects at all. // Occupied returns whether a Cell contains any Objects at all.
func (cell *Cell) Occupied() bool { func (cell *Cell) Occupied() bool {
return len(cell.Objects) > 0 return 0 < cell.Objects.Cnt
} }

View File

@@ -3,23 +3,39 @@ package resolv
// Collision contains the results of an Object.Check() call, and represents a collision between an Object and cells that contain other Objects. // Collision contains the results of an Object.Check() call, and represents a collision between an Object and cells that contain other Objects.
// The Objects array indicate the Objects collided with. // The Objects array indicate the Objects collided with.
type Collision struct { type Collision struct {
checkingObject *Object // The checking object checkingObject *Object // The checking object
dx, dy float64 // The delta the checking object was moving on that caused this collision dx, dy float64 // The delta the checking object was moving on that caused this collision
Objects []*Object // Slice of objects that were collided with; sorted according to distance to calling Object. Objects *RingBuffer // Slice of objects that were collided with; sorted according to distance to calling Object.
Cells []*Cell // Slice of cells that were collided with; sorted according to distance to calling Object. Cells *RingBuffer // Slice of cells that were collided with; sorted according to distance to calling Object.
} }
func NewCollision() *Collision { func NewCollision() *Collision {
return &Collision{ c := &Collision{}
Objects: []*Object{}, c.Objects = NewRingBuffer(16) // I don't expect it to exceed 10 actually
c.Cells = NewRingBuffer(16)
return c
}
func (cc *Collision) Clear() {
cc.checkingObject = nil
cc.dx = 0
cc.dy = 0
cc.Objects.Clear()
cc.Cells.Clear()
}
func (cc *Collision) PopFirstCollidedObject() *Object {
if 0 >= cc.Objects.Cnt {
return nil
} }
return cc.Objects.Pop().(*Object)
} }
// HasTags returns whether any objects within the Collision have all of the specified tags. This slice does not contain the Object that called Check(). // HasTags returns whether any objects within the Collision have all of the specified tags. This slice does not contain the Object that called Check().
func (cc *Collision) HasTags(tags ...string) bool { func (cc *Collision) HasTags(tags ...string) bool {
rb := cc.Objects
for _, o := range cc.Objects { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == cc.checkingObject { if o == cc.checkingObject {
continue continue
} }
@@ -36,38 +52,39 @@ func (cc *Collision) HasTags(tags ...string) bool {
// This slice does not contain the Object that called Check(). // This slice does not contain the Object that called Check().
func (cc *Collision) ObjectsByTags(tags ...string) []*Object { func (cc *Collision) ObjectsByTags(tags ...string) []*Object {
objects := []*Object{} objs := []*Object{}
for _, o := range cc.Objects {
rb := cc.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if o == cc.checkingObject { if o == cc.checkingObject {
continue continue
} }
if o.HasTags(tags...) { if o.HasTags(tags...) {
objects = append(objects, o) objs = append(objs, o)
} }
} }
return objects return objs
} }
// ContactWithObject returns the delta to move to come into contact with the specified Object. // ContactWithObject returns the delta to move to come into contact with the specified Object.
func (cc *Collision) ContactWithObject(object *Object) Vector { func (cc *Collision) ContactWithObject(obj *Object) Vector {
delta := Vector{0, 0} delta := Vector{0, 0}
if cc.dx < 0 { if cc.dx < 0 {
delta[0] = object.X + object.W - cc.checkingObject.X delta[0] = obj.X + obj.W - cc.checkingObject.X
} else if cc.dx > 0 { } else if cc.dx > 0 {
delta[0] = object.X - cc.checkingObject.W - cc.checkingObject.X delta[0] = obj.X - cc.checkingObject.W - cc.checkingObject.X
} }
if cc.dy < 0 { if cc.dy < 0 {
delta[1] = object.Y + object.H - cc.checkingObject.Y delta[1] = obj.Y + obj.H - cc.checkingObject.Y
} else if cc.dy > 0 { } else if cc.dy > 0 {
delta[1] = object.Y - cc.checkingObject.H - cc.checkingObject.Y delta[1] = obj.Y - cc.checkingObject.H - cc.checkingObject.Y
} }
return delta return delta
@@ -105,7 +122,7 @@ func (cc *Collision) SlideAgainstCell(cell *Cell, avoidTags ...string) Vector {
sp := cc.checkingObject.Space sp := cc.checkingObject.Space
collidingCell := cc.Cells[0] collidingCell := cc.Cells.GetByFrameId(cc.Cells.StFrameId).(*Cell)
ccX, ccY := sp.SpaceToWorld(collidingCell.X, collidingCell.Y) ccX, ccY := sp.SpaceToWorld(collidingCell.X, collidingCell.Y)
hX := float64(sp.CellWidth) / 2.0 hX := float64(sp.CellWidth) / 2.0
hY := float64(sp.CellHeight) / 2.0 hY := float64(sp.CellHeight) / 2.0
@@ -118,10 +135,10 @@ func (cc *Collision) SlideAgainstCell(cell *Cell, avoidTags ...string) Vector {
diffX := oX - ccX diffX := oX - ccX
diffY := oY - ccY diffY := oY - ccY
left := sp.Cell(collidingCell.X-1, collidingCell.Y) left := sp.GetCell(collidingCell.X-1, collidingCell.Y)
right := sp.Cell(collidingCell.X+1, collidingCell.Y) right := sp.GetCell(collidingCell.X+1, collidingCell.Y)
up := sp.Cell(collidingCell.X, collidingCell.Y-1) up := sp.GetCell(collidingCell.X, collidingCell.Y-1)
down := sp.Cell(collidingCell.X, collidingCell.Y+1) down := sp.GetCell(collidingCell.X, collidingCell.Y+1)
slide := Vector{0, 0} slide := Vector{0, 0}

View File

@@ -1,24 +0,0 @@
//go:build !amd64 || noasm
// +build !amd64 noasm
package resolv
// This function is from the gonum repository:
// https://github.com/gonum/gonum/blob/c3867503e73e5c3fee7ab93e3c2c562eb2be8178/internal/asm/f64/axpy.go#L23
func axpyUnitaryTo(dst []float64, alpha float64, x, y []float64) {
dim := len(y)
for i, v := range x {
if i == dim {
return
}
dst[i] = alpha*v + y[i]
}
}
// This function is from the gonum repository:
// https://github.com/gonum/gonum/blob/c3867503e73e5c3fee7ab93e3c2c562eb2be8178/internal/asm/f64/scal.go#L23
func scalUnitaryTo(dst []float64, alpha float64, x []float64) {
for i := range x {
dst[i] *= alpha
}
}

View File

@@ -1,31 +1,39 @@
package resolv package resolv
import (
"math"
//"sort"
)
// Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle. // Object represents an object that can be spread across one or more Cells in a Space. An Object is essentially an AABB (Axis-Aligned Bounding Box) Rectangle.
type Object struct { type Object struct {
Shape Shape // A shape for more specific collision-checking. Shape Shape // A shape for more specific collision-checking.
Space *Space // Reference to the Space the Object exists within Space *Space // Reference to the Space the Object exists within
X, Y, W, H float64 // Position and size of the Object in the Space X, Y, W, H float64 // Position and size of the Object in the Space
TouchingCells []*Cell // An array of Cells the Object is touching TouchingCells *RingBuffer // An array of Cells the Object is touching
Data interface{} // A pointer to a user-definable object Data interface{} // A pointer to a user-definable object
ignoreList map[*Object]bool // Set of Objects to ignore when checking for collisions ignoreList map[*Object]bool // Set of Objects to ignore when checking for collisions
tags []string // A list of tags the Object has tags []string // A list of tags the Object has
} }
// NewObject returns a new Object of the specified position and size. // NewObject returns a new Object of the specified position and size.
func NewObjectSingleTag(x, y, w, h float64, tag string) *Object {
o := &Object{}
o.X = x
o.Y = y
o.W = w
o.H = h
o.TouchingCells = NewRingBuffer(512) // [WARNING] Should make N large enough to cover all "TouchingCells", otherwise some cells would fail to unregister an object, resulting in memory corruption and incorrect detection result!
o.tags = []string{tag}
o.ignoreList = make(map[*Object]bool)
return o
}
func NewObject(x, y, w, h float64, tags ...string) *Object { func NewObject(x, y, w, h float64, tags ...string) *Object {
o := &Object{ o := &Object{}
X: x, o.X = x
Y: y, o.Y = y
W: w, o.W = w
H: h, o.H = h
tags: []string{}, o.TouchingCells = NewRingBuffer(512)
ignoreList: map[*Object]bool{}, o.tags = []string{}
} o.ignoreList = make(map[*Object]bool)
if len(tags) > 0 { if len(tags) > 0 {
o.AddTags(tags...) o.AddTags(tags...)
@@ -34,6 +42,18 @@ func NewObject(x, y, w, h float64, tags ...string) *Object {
return o return o
} }
func (obj *Object) GetData() interface{} {
return obj.Data
}
func (obj *Object) GetShape() *Shape {
return &(obj.Shape)
}
func (obj *Object) Position() (float64, float64) {
return obj.X, obj.Y
}
// Clone clones the Object with its properties into another Object. It also clones the Object's Shape (if it has one). // Clone clones the Object with its properties into another Object. It also clones the Object's Shape (if it has one).
func (obj *Object) Clone() *Object { func (obj *Object) Clone() *Object {
newObj := NewObject(obj.X, obj.Y, obj.W, obj.H, obj.Tags()...) newObj := NewObject(obj.X, obj.Y, obj.W, obj.H, obj.Tags()...)
@@ -41,7 +61,7 @@ func (obj *Object) Clone() *Object {
if obj.Shape != nil { if obj.Shape != nil {
newObj.SetShape(obj.Shape.Clone()) newObj.SetShape(obj.Shape.Clone())
} }
for k := range obj.ignoreList { for k, _ := range obj.ignoreList {
newObj.AddToIgnoreList(k) newObj.AddToIgnoreList(k)
} }
return newObj return newObj
@@ -59,7 +79,7 @@ func (obj *Object) Update() {
space := obj.Space space := obj.Space
obj.Space.Remove(obj) obj.Space.RemoveSingle(obj)
obj.Space = space obj.Space = space
@@ -69,11 +89,11 @@ func (obj *Object) Update() {
for x := cx; x <= ex; x++ { for x := cx; x <= ex; x++ {
c := obj.Space.Cell(x, y) c := obj.Space.GetCell(x, y)
if c != nil { if c != nil {
c.register(obj) c.register(obj)
obj.TouchingCells = append(obj.TouchingCells, c) obj.TouchingCells.Put(c)
} }
} }
@@ -154,17 +174,22 @@ func (obj *Object) BoundsToSpace(dx, dy float64) (int, int, int, int) {
// SharesCells returns whether the Object occupies a cell shared by the specified other Object. // SharesCells returns whether the Object occupies a cell shared by the specified other Object.
func (obj *Object) SharesCells(other *Object) bool { func (obj *Object) SharesCells(other *Object) bool {
for _, cell := range obj.TouchingCells { rb := obj.TouchingCells
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
cell := rb.GetByFrameId(i).(*Cell)
if cell.Contains(other) { if cell.Contains(other) {
return true return true
} }
} }
return false return false
} }
// SharesCellsTags returns if the Cells the Object occupies have an object with the specified tags. // SharesCellsTags returns if the Cells the Object occupies have an object with the specified tags.
func (obj *Object) SharesCellsTags(tags ...string) bool { func (obj *Object) SharesCellsTags(tags ...string) bool {
for _, cell := range obj.TouchingCells { rb := obj.TouchingCells
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
cell := rb.GetByFrameId(i).(*Cell)
if cell.ContainsTags(tags...) { if cell.ContainsTags(tags...) {
return true return true
} }
@@ -218,25 +243,24 @@ func (obj *Object) SetBounds(topLeft, bottomRight Vector) {
// Check checks the space around the object using the designated delta movement (dx and dy). This is done by querying the containing Space's Cells // Check checks the space around the object using the designated delta movement (dx and dy). This is done by querying the containing Space's Cells
// so that it can see if moving it would coincide with a cell that houses another Object (filtered using the given selection of tag strings). If so, // so that it can see if moving it would coincide with a cell that houses another Object (filtered using the given selection of tag strings). If so,
// Check returns a Collision. If no objects are found or the Object does not exist within a Space, this function returns nil. // Check returns a Collision. If no objects are found or the Object does not exist within a Space, this function returns nil.
func (obj *Object) Check(dx, dy float64, tags ...string) *Collision { func (obj *Object) CheckAllWithHolder(dx, dy float64, cc *Collision) bool {
if obj.Space == nil { if obj.Space == nil {
return nil return false
} }
cc.Clear()
cc := NewCollision()
cc.checkingObject = obj cc.checkingObject = obj
if dx < 0 { if dx < 0 {
dx = math.Min(dx, -1) dx = Min(dx, -1)
} else if dx > 0 { } else if dx > 0 {
dx = math.Max(dx, 1) dx = Max(dx, 1)
} }
if dy < 0 { if dy < 0 {
dy = math.Min(dy, -1) dy = Min(dy, -1)
} else if dy > 0 { } else if dy > 0 {
dy = math.Max(dy, 1) dy = Max(dy, 1)
} }
cc.dx = dx cc.dx = dx
@@ -251,65 +275,38 @@ func (obj *Object) Check(dx, dy float64, tags ...string) *Collision {
for x := cx; x <= ex; x++ { for x := cx; x <= ex; x++ {
if c := obj.Space.Cell(x, y); c != nil { if c := obj.Space.GetCell(x, y); c != nil {
for _, o := range c.Objects {
rb := c.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
// We only want cells that have objects other than the checking object, or that aren't on the ignore list. // We only want cells that have objects other than the checking object, or that aren't on the ignore list.
if ignored := obj.ignoreList[o]; o == obj || ignored { if ignored := obj.ignoreList[o]; o == obj || ignored {
continue continue
} }
if _, added := objectsAdded[o]; (len(tags) == 0 || o.HasTags(tags...)) && !added { if _, added := objectsAdded[o]; !added {
cc.Objects.Put(o)
cc.Objects = append(cc.Objects, o)
objectsAdded[o] = true objectsAdded[o] = true
if _, added := cellsAdded[c]; !added { if _, added := cellsAdded[c]; !added {
cc.Cells = append(cc.Cells, c) cc.Cells.Put(c)
cellsAdded[c] = true cellsAdded[c] = true
} }
continue continue
} }
} }
} }
} }
} }
if len(cc.Objects) == 0 { if 0 >= cc.Objects.Cnt {
return nil return false
} }
/* return true
// In my use case, order of objects within a collision instance is not needed, and this also favors both runtime performance & size reduction of `jsexport.js`.
ox, oy := cc.checkingObject.Center()
oc := Vector{ox, oy}
sort.Slice(cc.Objects, func(i, j int) bool {
ix, iy := cc.Objects[i].Center()
jx, jy := cc.Objects[j].Center()
return Vector{ix, iy}.Sub(oc).Magnitude2() < Vector{jx, jy}.Sub(oc).Magnitude2()
})
cw := cc.checkingObject.Space.CellWidth
ch := cc.checkingObject.Space.CellHeight
sort.Slice(cc.Cells, func(i, j int) bool {
return Vector{float64(cc.Cells[i].X*cw + (cw / 2)), float64(cc.Cells[i].Y*ch + (ch / 2))}.Sub(oc).Magnitude2() <
Vector{float64(cc.Cells[j].X*cw + (cw / 2)), float64(cc.Cells[j].Y*ch + (ch / 2))}.Sub(oc).Magnitude2()
})
*/
return cc
} }
// Overlaps returns if an Object overlaps another Object. // Overlaps returns if an Object overlaps another Object.

View File

@@ -1,11 +1,14 @@
package battle package resolv
const ( const (
RING_BUFF_CONSECUTIVE_SET = int32(0) // Declare type "int32" explicitly to prevent go2cs from transpiling them to "var"
RING_BUFF_NON_CONSECUTIVE_SET = int32(1) RING_BUFF_CONSECUTIVE_SET int32 = 0
RING_BUFF_FAILED_TO_SET = int32(2) RING_BUFF_NON_CONSECUTIVE_SET int32 = 1
RING_BUFF_FAILED_TO_SET int32 = 2
) )
type AnyObj interface{}
type RingBuffer struct { type RingBuffer struct {
Ed int32 // write index, open index Ed int32 // write index, open index
St int32 // read index, closed index St int32 // read index, closed index
@@ -13,20 +16,35 @@ type RingBuffer struct {
StFrameId int32 StFrameId int32
N int32 N int32
Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods
Eles []interface{} Eles []AnyObj
} }
func NewRingBuffer(n int32) *RingBuffer { func NewRingBuffer(n int32) *RingBuffer {
return &RingBuffer{ ret := &RingBuffer{}
Ed: 0, ret.Ed = 0
St: 0, ret.St = 0
N: n, ret.EdFrameId = 0
Cnt: 0, ret.StFrameId = 0
Eles: make([]interface{}, n), ret.N = n
ret.Cnt = 0
ret.Eles = make([]AnyObj, n)
return ret
}
func (rb *RingBuffer) DryPut() {
for 0 < rb.Cnt && rb.Cnt >= rb.N {
// Make room for the new element
rb.Pop()
}
rb.EdFrameId++
rb.Cnt++
rb.Ed++
if rb.Ed >= rb.N {
rb.Ed -= rb.N // Deliberately not using "%" operator for performance concern
} }
} }
func (rb *RingBuffer) Put(pItem interface{}) { func (rb *RingBuffer) Put(pItem AnyObj) {
for 0 < rb.Cnt && rb.Cnt >= rb.N { for 0 < rb.Cnt && rb.Cnt >= rb.N {
// Make room for the new element // Make room for the new element
rb.Pop() rb.Pop()
@@ -40,7 +58,7 @@ func (rb *RingBuffer) Put(pItem interface{}) {
} }
} }
func (rb *RingBuffer) Pop() interface{} { func (rb *RingBuffer) Pop() AnyObj {
if 0 == rb.Cnt { if 0 == rb.Cnt {
return nil return nil
} }
@@ -78,7 +96,7 @@ func (rb *RingBuffer) GetArrIdxByOffset(offsetFromSt int32) int32 {
return -1 return -1
} }
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { func (rb *RingBuffer) GetByOffset(offsetFromSt int32) AnyObj {
arrIdx := rb.GetArrIdxByOffset(offsetFromSt) arrIdx := rb.GetArrIdxByOffset(offsetFromSt)
if -1 == arrIdx { if -1 == arrIdx {
return nil return nil
@@ -86,7 +104,7 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
return rb.Eles[arrIdx] return rb.Eles[arrIdx]
} }
func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} { func (rb *RingBuffer) GetByFrameId(frameId int32) AnyObj {
if frameId >= rb.EdFrameId || frameId < rb.StFrameId { if frameId >= rb.EdFrameId || frameId < rb.StFrameId {
return nil return nil
} }
@@ -94,7 +112,7 @@ func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} {
} }
// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. // [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly.
func (rb *RingBuffer) SetByFrameId(pItem interface{}, frameId int32) (int32, int32, int32) { func (rb *RingBuffer) SetByFrameId(pItem AnyObj, frameId int32) (int32, int32, int32) {
oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId
if frameId < oldStFrameId { if frameId < oldStFrameId {
return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId
@@ -122,3 +140,25 @@ func (rb *RingBuffer) SetByFrameId(pItem interface{}, frameId int32) (int32, int
return ret, oldStFrameId, oldEdFrameId return ret, oldStFrameId, oldEdFrameId
} }
func (rb *RingBuffer) Clear() {
for 0 < rb.Cnt {
rb.Pop()
}
rb.St = 0
rb.Ed = 0
rb.StFrameId = 0
rb.EdFrameId = 0
}
func (rb *RingBuffer) GetStFrameId() int32 {
return rb.StFrameId
}
func (rb *RingBuffer) GetEdFrameId() int32 {
return rb.EdFrameId
}
func (rb *RingBuffer) GetCnt() int32 {
return rb.Cnt
}

View File

@@ -1,9 +1,5 @@
package resolv package resolv
import (
"math"
)
type Shape interface { type Shape interface {
// Intersection tests to see if a Shape intersects with the other given Shape. dx and dy are delta movement variables indicating // Intersection tests to see if a Shape intersects with the other given Shape. dx and dy are delta movement variables indicating
// movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it // movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it
@@ -27,23 +23,16 @@ type Line struct {
} }
func NewLine(x, y, x2, y2 float64) *Line { func NewLine(x, y, x2, y2 float64) *Line {
return &Line{ l := &Line{}
Start: Vector{x, y}, l.Start = Vector{x, y}
End: Vector{x2, y2}, l.End = Vector{x2, y2}
} return l
}
func (line *Line) Project(axis Vector) Vector {
return line.Vector().Scale(axis.Dot(line.Start.Sub(line.End)))
} }
func (line *Line) Normal() Vector { func (line *Line) Normal() Vector {
v := line.Vector() dy := line.End[1] - line.Start[1]
return Vector{v[1], -v[0]}.Unit() dx := line.End[0] - line.Start[0]
} return Vector{dy, -dx}.Unit()
func (line *Line) Vector() Vector {
return line.End.Clone().Sub(line.Start).Unit()
} }
// IntersectionPointsLine returns the intersection point of a Line with another Line as a Vector. If no intersection is found, it will return nil. // IntersectionPointsLine returns the intersection point of a Line with another Line as a Vector. If no intersection is found, it will return nil.
@@ -78,53 +67,8 @@ func (line *Line) IntersectionPointsLine(other *Line) Vector {
} }
// IntersectionPointsCircle returns a slice of Vectors, each indicating the intersection point. If no intersection is found, it will return an empty slice.
func (line *Line) IntersectionPointsCircle(circle *Circle) []Vector {
points := []Vector{}
cp := Vector{circle.X, circle.Y}
lStart := line.Start.Sub(cp)
lEnd := line.End.Sub(cp)
diff := lEnd.Sub(lStart)
a := diff[0]*diff[0] + diff[1]*diff[1]
b := 2 * ((diff[0] * lStart[0]) + (diff[1] * lStart[1]))
c := (lStart[0] * lStart[0]) + (lStart[1] * lStart[1]) - (circle.Radius * circle.Radius)
det := b*b - (4 * a * c)
if det < 0 {
// Do nothing, no intersections
} else if det == 0 {
t := -b / (2 * a)
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
} else {
t := (-b + math.Sqrt(det)) / (2 * a)
// We have to ensure t is between 0 and 1; otherwise, the collision points are on the circle as though the lines were infinite in length.
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
t = (-b - math.Sqrt(det)) / (2 * a)
if t >= 0 && t <= 1 {
points = append(points, Vector{line.Start[0] + t*diff[0], line.Start[1] + t*diff[1]})
}
}
return points
}
type ConvexPolygon struct { type ConvexPolygon struct {
Points []Vector Points *RingBuffer
X, Y float64 X, Y float64
Closed bool Closed bool
} }
@@ -134,67 +78,88 @@ type ConvexPolygon struct {
// polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}. // polygon square, with the vertices at {0,0}, {10,0}, {10, 10}, and {0, 10}.
func NewConvexPolygon(points ...float64) *ConvexPolygon { func NewConvexPolygon(points ...float64) *ConvexPolygon {
// if len(points)/2 < 2 { cp := &ConvexPolygon{}
// return nil cp.Points = NewRingBuffer(6) // I don't expected more points to be coped with in this particular game
// } cp.Closed = true
cp := &ConvexPolygon{Points: []Vector{}, Closed: true}
cp.AddPoints(points...) cp.AddPoints(points...)
return cp return cp
} }
func (cp *ConvexPolygon) Clone() Shape { func (cp *ConvexPolygon) GetPointByOffset(offset int32) Vector {
if cp.Points.Cnt <= offset {
points := []Vector{} return nil
for _, point := range cp.Points {
points = append(points, point.Clone())
} }
return cp.Points.GetByFrameId(cp.Points.StFrameId + offset).(Vector)
}
func (cp *ConvexPolygon) Clone() Shape {
newPoly := NewConvexPolygon() newPoly := NewConvexPolygon()
newPoly.X = cp.X newPoly.X = cp.X
newPoly.Y = cp.Y newPoly.Y = cp.Y
newPoly.AddPointsVec(points...) for i := int32(0); i < cp.Points.Cnt; i++ {
newPoly.Points.Put(cp.GetPointByOffset(i))
}
newPoly.Closed = cp.Closed newPoly.Closed = cp.Closed
return newPoly return newPoly
} }
// AddPointsVec allows you to add points to the ConvexPolygon with a slice of Vectors, each indicating a point / vertex.
func (cp *ConvexPolygon) AddPointsVec(points ...Vector) {
cp.Points = append(cp.Points, points...)
}
// AddPoints allows you to add points to the ConvexPolygon with a slice or selection of float64s, with each pair indicating an X or Y value for // AddPoints allows you to add points to the ConvexPolygon with a slice or selection of float64s, with each pair indicating an X or Y value for
// a point / vertex (i.e. AddPoints(0, 1, 2, 3) would add two points - one at {0, 1}, and another at {2, 3}). // a point / vertex (i.e. AddPoints(0, 1, 2, 3) would add two points - one at {0, 1}, and another at {2, 3}).
func (cp *ConvexPolygon) AddPoints(vertexPositions ...float64) { func (cp *ConvexPolygon) AddPoints(vertexPositions ...float64) {
for v := 0; v < len(vertexPositions); v += 2 { for v := 0; v < len(vertexPositions); v += 2 {
cp.Points = append(cp.Points, Vector{vertexPositions[v], vertexPositions[v+1]}) // "resolv.Vector" is an alias of "[]float64", thus already a pointer type
cp.Points.Put(Vector{vertexPositions[v], vertexPositions[v+1]})
} }
} }
func (cp *ConvexPolygon) UpdateAsRectangle(x, y, w, h float64) bool {
// This function might look ugly but it's a fast in-place update!
if 4 != cp.Points.Cnt {
panic("ConvexPolygon not having exactly 4 vertices to form a rectangle#1!")
}
for i := int32(0); i < cp.Points.Cnt; i++ {
thatVec := cp.GetPointByOffset(i)
if nil == thatVec {
panic("ConvexPolygon not having exactly 4 vertices to form a rectangle#2!")
}
switch i {
case 0:
thatVec[0] = x
thatVec[1] = y
case 1:
thatVec[0] = x + w
thatVec[1] = y
case 2:
thatVec[0] = x + w
thatVec[1] = y + h
case 3:
thatVec[0] = x
thatVec[1] = y + h
}
}
return true
}
// Lines returns a slice of transformed Lines composing the ConvexPolygon. // Lines returns a slice of transformed Lines composing the ConvexPolygon.
func (cp *ConvexPolygon) Lines() []*Line { func (cp *ConvexPolygon) Lines() []*Line {
lines := []*Line{}
vertices := cp.Transformed() vertices := cp.Transformed()
linesCnt := len(vertices)
if !cp.Closed {
linesCnt -= 1
}
lines := make([]*Line, linesCnt)
for i := 0; i < len(vertices); i++ { for i := 0; i < linesCnt; i++ {
start, end := vertices[i], vertices[0] start, end := vertices[i], vertices[0]
if i < len(vertices)-1 { if i < len(vertices)-1 {
end = vertices[i+1] end = vertices[i+1]
} else if !cp.Closed {
break
} }
line := NewLine(start[0], start[1], end[0], end[1]) line := NewLine(start[0], start[1], end[0], end[1])
lines[i] = line
lines = append(lines, line)
} }
return lines return lines
@@ -203,9 +168,10 @@ func (cp *ConvexPolygon) Lines() []*Line {
// Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position. // Transformed returns the ConvexPolygon's points / vertices, transformed according to the ConvexPolygon's position.
func (cp *ConvexPolygon) Transformed() []Vector { func (cp *ConvexPolygon) Transformed() []Vector {
transformed := []Vector{} transformed := make([]Vector, cp.Points.Cnt)
for _, point := range cp.Points { for i := int32(0); i < cp.Points.Cnt; i++ {
transformed = append(transformed, Vector{point[0] + cp.X, point[1] + cp.Y}) point := cp.GetPointByOffset(i)
transformed[i] = Vector{point[0] + cp.X, point[1] + cp.Y}
} }
return transformed return transformed
} }
@@ -217,7 +183,7 @@ func (cp *ConvexPolygon) Bounds() (Vector, Vector) {
transformed := cp.Transformed() transformed := cp.Transformed()
topLeft := Vector{transformed[0][0], transformed[0][1]} topLeft := Vector{transformed[0][0], transformed[0][1]}
bottomRight := topLeft.Clone() bottomRight := Vector{transformed[0][0], transformed[0][1]}
for i := 0; i < len(transformed); i++ { for i := 0; i < len(transformed); i++ {
@@ -254,8 +220,8 @@ func (cp *ConvexPolygon) SetPosition(x, y float64) {
// SetPositionVec allows you to set the position of the ConvexPolygon using a Vector. The offset of the vertices compared to the X and Y // SetPositionVec allows you to set the position of the ConvexPolygon using a Vector. The offset of the vertices compared to the X and Y
// position is relative to however you initially defined the polygon and added the vertices. // position is relative to however you initially defined the polygon and added the vertices.
func (cp *ConvexPolygon) SetPositionVec(vec Vector) { func (cp *ConvexPolygon) SetPositionVec(vec Vector) {
cp.X = vec.X() cp.X = vec.GetX()
cp.Y = vec.Y() cp.Y = vec.GetY()
} }
// Move translates the ConvexPolygon by the designated X and Y values. // Move translates the ConvexPolygon by the designated X and Y values.
@@ -266,49 +232,16 @@ func (cp *ConvexPolygon) Move(x, y float64) {
// MoveVec translates the ConvexPolygon by the designated Vector. // MoveVec translates the ConvexPolygon by the designated Vector.
func (cp *ConvexPolygon) MoveVec(vec Vector) { func (cp *ConvexPolygon) MoveVec(vec Vector) {
cp.X += vec.X() cp.X += vec.GetX()
cp.Y += vec.Y() cp.Y += vec.GetY()
}
// Center returns the transformed Center of the ConvexPolygon.
func (cp *ConvexPolygon) Center() Vector {
pos := Vector{0, 0}
for _, v := range cp.Transformed() {
pos.Add(v)
}
pos[0] /= float64(len(cp.Transformed()))
pos[1] /= float64(len(cp.Transformed()))
return pos
}
// Project projects (i.e. flattens) the ConvexPolygon onto the provided axis.
func (cp *ConvexPolygon) Project(axis Vector) Projection {
axis = axis.Unit()
vertices := cp.Transformed()
min := axis.Dot(Vector{vertices[0][0], vertices[0][1]})
max := min
for i := 1; i < len(vertices); i++ {
p := axis.Dot(Vector{vertices[i][0], vertices[i][1]})
if p < min {
min = p
} else if p > max {
max = p
}
}
return Projection{min, max}
} }
// SATAxes returns the axes of the ConvexPolygon for SAT intersection testing. // SATAxes returns the axes of the ConvexPolygon for SAT intersection testing.
func (cp *ConvexPolygon) SATAxes() []Vector { func (cp *ConvexPolygon) SATAxes() []Vector {
lines := cp.Lines()
axes := []Vector{} axes := make([]Vector, len(lines))
for _, line := range cp.Lines() { for i, line := range lines {
axes = append(axes, line.Normal()) axes[i] = line.Normal()
} }
return axes return axes
@@ -339,11 +272,11 @@ type ContactSet struct {
} }
func NewContactSet() *ContactSet { func NewContactSet() *ContactSet {
return &ContactSet{ cs := &ContactSet{}
Points: []Vector{}, cs.Points = []Vector{}
MTV: Vector{0, 0}, cs.MTV = Vector{0, 0}
Center: Vector{0, 0}, cs.Center = Vector{}
} return cs
} }
// LeftmostPoint returns the left-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil. // LeftmostPoint returns the left-most point out of the ContactSet's Points slice. If the Points slice is empty somehow, this returns nil.
@@ -427,13 +360,7 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
cp.X += dx cp.X += dx
cp.Y += dy cp.Y += dy
if circle, isCircle := other.(*Circle); isCircle { if poly, isPoly := other.(*ConvexPolygon); isPoly {
for _, line := range cp.Lines() {
contactSet.Points = append(contactSet.Points, line.IntersectionPointsCircle(circle)...)
}
} else if poly, isPoly := other.(*ConvexPolygon); isPoly {
for _, line := range cp.Lines() { for _, line := range cp.Lines() {
@@ -450,29 +377,11 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
} }
if len(contactSet.Points) > 0 { if len(contactSet.Points) > 0 {
// Do nothing
for _, point := range contactSet.Points {
contactSet.Center = contactSet.Center.Add(point)
}
contactSet.Center[0] /= float64(len(contactSet.Points))
contactSet.Center[1] /= float64(len(contactSet.Points))
if mtv := cp.calculateMTV(contactSet, other); mtv != nil {
contactSet.MTV = mtv
}
} else { } else {
contactSet = nil contactSet = nil
} }
// If dx or dy aren't 0, then the MTV will be greater to compensate; this adjusts the vector back.
if contactSet != nil && (dx != 0 || dy != 0) {
deltaMagnitude := Vector{dx, dy}.Magnitude()
ogMagnitude := contactSet.MTV.Magnitude()
contactSet.MTV = contactSet.MTV.Unit().Scale(ogMagnitude - deltaMagnitude)
}
cp.X = ogX cp.X = ogX
cp.Y = ogY cp.Y = ogY
@@ -480,112 +389,6 @@ func (cp *ConvexPolygon) Intersection(dx, dy float64, other Shape) *ContactSet {
} }
// calculateMTV returns the MTV, if possible, and a bool indicating whether it was possible or not.
func (cp *ConvexPolygon) calculateMTV(contactSet *ContactSet, otherShape Shape) Vector {
delta := Vector{0, 0}
smallest := Vector{math.MaxFloat64, 0}
switch other := otherShape.(type) {
case *ConvexPolygon:
for _, axis := range cp.SATAxes() {
if !cp.Project(axis).Overlapping(other.Project(axis)) {
return nil
}
overlap := cp.Project(axis).Overlap(other.Project(axis))
if smallest.Magnitude() > overlap {
smallest = axis.Scale(overlap)
}
}
for _, axis := range other.SATAxes() {
if !cp.Project(axis).Overlapping(other.Project(axis)) {
return nil
}
overlap := cp.Project(axis).Overlap(other.Project(axis))
if smallest.Magnitude() > overlap {
smallest = axis.Scale(overlap)
}
}
// Removed support of "Circle" to remove dependency of "sort" module
}
delta[0] = smallest[0]
delta[1] = smallest[1]
return delta
}
// ContainedBy returns if the ConvexPolygon is wholly contained by the other shape provided.
func (cp *ConvexPolygon) ContainedBy(otherShape Shape) bool {
switch other := otherShape.(type) {
case *ConvexPolygon:
for _, axis := range cp.SATAxes() {
if !cp.Project(axis).IsInside(other.Project(axis)) {
return false
}
}
for _, axis := range other.SATAxes() {
if !cp.Project(axis).IsInside(other.Project(axis)) {
return false
}
}
}
return true
}
// FlipH flips the ConvexPolygon's vertices horizontally according to their initial offset when adding the points.
func (cp *ConvexPolygon) FlipH() {
for _, v := range cp.Points {
v[0] = -v[0]
}
// We have to reverse vertex order after flipping the vertices to ensure the winding order is consistent between Objects (so that the normals are consistently outside or inside, which is important
// when doing Intersection tests). If we assume that the normal of a line, going from vertex A to vertex B, is one direction, then the normal would be inverted if the vertices were flipped in position,
// but not in order. This would make Intersection tests drive objects into each other, instead of giving the delta to move away.
cp.ReverseVertexOrder()
}
// FlipV flips the ConvexPolygon's vertices vertically according to their initial offset when adding the points.
func (cp *ConvexPolygon) FlipV() {
for _, v := range cp.Points {
v[1] = -v[1]
}
cp.ReverseVertexOrder()
}
// ReverseVertexOrder reverses the vertex ordering of the ConvexPolygon.
func (cp *ConvexPolygon) ReverseVertexOrder() {
verts := []Vector{cp.Points[0]}
for i := len(cp.Points) - 1; i >= 1; i-- {
verts = append(verts, cp.Points[i])
}
cp.Points = verts
}
// NewRectangle returns a rectangular ConvexPolygon with the vertices in clockwise order. In actuality, an AABBRectangle should be its own // NewRectangle returns a rectangular ConvexPolygon with the vertices in clockwise order. In actuality, an AABBRectangle should be its own
// "thing" with its own optimized Intersection code check. // "thing" with its own optimized Intersection code check.
func NewRectangle(x, y, w, h float64) *ConvexPolygon { func NewRectangle(x, y, w, h float64) *ConvexPolygon {
@@ -596,159 +399,3 @@ func NewRectangle(x, y, w, h float64) *ConvexPolygon {
x, y+h, x, y+h,
) )
} }
type Circle struct {
X, Y, Radius float64
}
// NewCircle returns a new Circle, with its center at the X and Y position given, and with the defined radius.
func NewCircle(x, y, radius float64) *Circle {
circle := &Circle{
X: x,
Y: y,
Radius: radius,
}
return circle
}
func (circle *Circle) Clone() Shape {
return NewCircle(circle.X, circle.Y, circle.Radius)
}
// Bounds returns the top-left and bottom-right corners of the Circle.
func (circle *Circle) Bounds() (Vector, Vector) {
return Vector{circle.X - circle.Radius, circle.Y - circle.Radius}, Vector{circle.X + circle.Radius, circle.Y + circle.Radius}
}
// Intersection tests to see if a Circle intersects with the other given Shape. dx and dy are delta movement variables indicating
// movement to be applied before the intersection check (thereby allowing you to see if a Shape would collide with another if it
// were in a different relative location). If an Intersection is found, a ContactSet will be returned, giving information regarding
// the intersection.
func (circle *Circle) Intersection(dx, dy float64, other Shape) *ContactSet {
var contactSet *ContactSet
ox := circle.X
oy := circle.Y
circle.X += dx
circle.Y += dy
// here
switch shape := other.(type) {
case *ConvexPolygon:
// Maybe this would work?
contactSet = shape.Intersection(-dx, -dy, circle)
if contactSet != nil {
contactSet.MTV = contactSet.MTV.Scale(-1)
}
case *Circle:
contactSet = NewContactSet()
contactSet.Points = circle.IntersectionPointsCircle(shape)
if len(contactSet.Points) == 0 {
return nil
}
contactSet.MTV = Vector{circle.X - shape.X, circle.Y - shape.Y}
dist := contactSet.MTV.Magnitude()
contactSet.MTV = contactSet.MTV.Unit().Scale(circle.Radius + shape.Radius - dist)
for _, point := range contactSet.Points {
contactSet.Center = contactSet.Center.Add(point)
}
contactSet.Center[0] /= float64(len(contactSet.Points))
contactSet.Center[1] /= float64(len(contactSet.Points))
// if contactSet != nil {
// contactSet.MTV[0] -= dx
// contactSet.MTV[1] -= dy
// }
// contactSet.MTV = Vector{circle.X - shape.X, circle.Y - shape.Y}
}
circle.X = ox
circle.Y = oy
return contactSet
}
// Move translates the Circle by the designated X and Y values.
func (circle *Circle) Move(x, y float64) {
circle.X += x
circle.Y += y
}
// MoveVec translates the Circle by the designated Vector.
func (circle *Circle) MoveVec(vec Vector) {
circle.X += vec.X()
circle.Y += vec.Y()
}
// SetPosition sets the center position of the Circle using the X and Y values given.
func (circle *Circle) SetPosition(x, y float64) {
circle.X = x
circle.Y = y
}
// SetPosition sets the center position of the Circle using the Vector given.
func (circle *Circle) SetPositionVec(vec Vector) {
circle.X = vec.X()
circle.Y = vec.Y()
}
// Position() returns the X and Y position of the Circle.
func (circle *Circle) Position() (float64, float64) {
return circle.X, circle.Y
}
// PointInside returns if the given Vector is inside of the circle.
func (circle *Circle) PointInside(point Vector) bool {
return point.Sub(Vector{circle.X, circle.Y}).Magnitude() <= circle.Radius
}
// IntersectionPointsCircle returns the intersection points of the two circles provided.
func (circle *Circle) IntersectionPointsCircle(other *Circle) []Vector {
d := math.Sqrt(math.Pow(other.X-circle.X, 2) + math.Pow(other.Y-circle.Y, 2))
if d > circle.Radius+other.Radius || d < math.Abs(circle.Radius-other.Radius) || d == 0 && circle.Radius == other.Radius {
return nil
}
a := (math.Pow(circle.Radius, 2) - math.Pow(other.Radius, 2) + math.Pow(d, 2)) / (2 * d)
h := math.Sqrt(math.Pow(circle.Radius, 2) - math.Pow(a, 2))
x2 := circle.X + a*(other.X-circle.X)/d
y2 := circle.Y + a*(other.Y-circle.Y)/d
return []Vector{
{x2 + h*(other.Y-circle.Y)/d, y2 - h*(other.X-circle.X)/d},
{x2 - h*(other.Y-circle.Y)/d, y2 + h*(other.X-circle.X)/d},
}
}
type Projection struct {
Min, Max float64
}
// Overlapping returns whether a Projection is overlapping with the other, provided Projection. Credit to https://www.sevenson.com.au/programming/sat/
func (projection Projection) Overlapping(other Projection) bool {
return projection.Overlap(other) > 0
}
// Overlap returns the amount that a Projection is overlapping with the other, provided Projection. Credit to https://dyn4j.org/2010/01/sat/#sat-nointer
func (projection Projection) Overlap(other Projection) float64 {
return math.Min(projection.Max, other.Max) - math.Max(projection.Min, other.Min)
}
// IsInside returns whether the Projection is wholly inside of the other, provided Projection.
func (projection Projection) IsInside(other Projection) bool {
return projection.Min >= other.Min && projection.Max <= other.Max
}

View File

@@ -0,0 +1,112 @@
package resolv
import "unsafe"
const (
uvnan = 0x7FF8000000000001
uvinf = 0x7FF0000000000000
uvneginf = 0xFFF0000000000000
uvone = 0x3FF0000000000000
mask = 0x7FF
shift = 64 - 11 - 1
bias = 1023
signMask = 1 << 63
fracMask = 1<<shift - 1
MaxFloat64 = 1.79e+308
magic32 = 0x5f3759df
magic64 = 0x5fe6eb50c7b537a9
)
func Max(a, b float64) float64 {
if a > b {
return a
} else {
return b
}
}
func Min(a, b float64) float64 {
if a < b {
return a
} else {
return b
}
}
func Floor(x float64) float64 {
if x == 0 || IsInf(x, 0) || IsNaN(x) {
return x
}
if x < 0 {
d, fract := Modf(-x)
if fract != 0.0 {
d = d + 1
}
return -d
}
d, _ := Modf(x)
return d
}
func Modf(f float64) (outval float64, frac float64) {
if f < 1 {
if f < 0 {
outval1, frac1 := Modf(-f)
return -outval1, -frac1
} else if f == 0 {
return f, f // Return -0, -0 when f == -0
}
return 0, f
}
x := Float64bits(f)
e := ((uint)(x>>shift))&mask - bias
// Keep the top 12+e bits, the integer part; clear the rest.
if e < 64-12 {
x &^= 1<<(64-12-e) - 1
}
outval = Float64frombits(x)
frac = f - outval
return
}
func Float32bits(f float32) uint32 { return *(*uint32)(unsafe.Pointer(&f)) }
func Float32frombits(b uint32) float32 { return *(*float32)(unsafe.Pointer(&b)) }
func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }
func NaN() float64 { return Float64frombits(uvnan) }
func IsNaN(f float64) (is bool) {
return f != f
}
func IsInf(f float64, sign int) bool {
return sign >= 0 && f > MaxFloat64 || sign <= 0 && f < -MaxFloat64
}
// FastInvSqrt reference https://medium.com/@adrien.za/fast-inverse-square-root-in-go-and-javascript-for-fun-6b891e74e5a8
func FastInvSqrt32(n float32) float32 {
if n < 0 {
return float32(NaN())
}
n2, th := n*0.5, float32(1.5)
b := Float32bits(n)
b = magic32 - (b >> 1)
f := Float32frombits(b)
f *= th - (n2 * f * f)
return f
}
func FastInvSqrt64(n float64) float64 {
if n < 0 {
return NaN()
}
n2, th := n*0.5, float64(1.5)
b := Float64bits(n)
b = magic64 - (b >> 1)
f := Float64frombits(b)
f *= th - (n2 * f * f)
return f
}

View File

@@ -1,9 +1,5 @@
package resolv package resolv
import (
"math"
)
// Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which // Space represents a collision space. Internally, each Space contains a 2D array of Cells, with each Cell being the same size. Cells contain information on which
// Objects occupy those spaces. // Objects occupy those spaces.
type Space struct { type Space struct {
@@ -16,21 +12,30 @@ type Space struct {
// speed of one cell size per collision check to avoid missing any possible collisions. // speed of one cell size per collision check to avoid missing any possible collisions.
func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space { func NewSpace(spaceWidth, spaceHeight, cellWidth, cellHeight int) *Space {
sp := &Space{ sp := &Space{}
CellWidth: cellWidth, sp.CellWidth = cellWidth
CellHeight: cellHeight, sp.CellHeight = cellHeight
}
sp.Resize(spaceWidth/cellWidth, spaceHeight/cellHeight) sp.Resize(spaceWidth/cellWidth, spaceHeight/cellHeight)
// sp.Resize(int(math.Ceil(float64(spaceWidth)/float64(cellWidth))),
// int(math.Ceil(float64(spaceHeight)/float64(cellHeight))))
return sp return sp
} }
// [WARNING] The slice type boxing/unboxing is proved by profiling to be heavy after transpiled to JavaScript, thus adding some "XxxSingle" shortcuts here.
// Add adds the specified Objects to the Space, updating the Space's cells to refer to the Object. // Add adds the specified Objects to the Space, updating the Space's cells to refer to the Object.
func (sp *Space) AddSingle(obj *Object) {
if sp == nil {
panic("ERROR: space is nil")
}
obj.Space = sp
// We call Update() once to make sure the object gets its cells added.
obj.Update()
}
func (sp *Space) Add(objects ...*Object) { func (sp *Space) Add(objects ...*Object) {
if sp == nil { if sp == nil {
@@ -50,6 +55,20 @@ func (sp *Space) Add(objects ...*Object) {
// Remove removes the specified Objects from being associated with the Space. This should be done whenever an Object is removed from the // Remove removes the specified Objects from being associated with the Space. This should be done whenever an Object is removed from the
// game. // game.
func (sp *Space) RemoveSingle(obj *Object) {
if sp == nil {
panic("ERROR: space is nil")
}
for 0 < obj.TouchingCells.Cnt {
cell := obj.TouchingCells.Pop().(*Cell)
cell.unregister(obj)
}
obj.Space = nil
}
func (sp *Space) Remove(objects ...*Object) { func (sp *Space) Remove(objects ...*Object) {
if sp == nil { if sp == nil {
@@ -57,13 +76,11 @@ func (sp *Space) Remove(objects ...*Object) {
} }
for _, obj := range objects { for _, obj := range objects {
for 0 < obj.TouchingCells.Cnt {
for _, cell := range obj.TouchingCells { cell := obj.TouchingCells.Pop().(*Cell)
cell.unregister(obj) cell.unregister(obj)
} }
obj.TouchingCells = []*Cell{}
obj.Space = nil obj.Space = nil
} }
@@ -76,20 +93,18 @@ func (sp *Space) Objects() []*Object {
objectsAdded := map[*Object]bool{} objectsAdded := map[*Object]bool{}
objects := []*Object{} objects := []*Object{}
cyUpper := len(sp.Cells)
for cy := range sp.Cells { for cy := 0; cy < cyUpper; cy++ {
cxUpper := len(sp.Cells[cy])
for cx := range sp.Cells[cy] { for cx := 0; cx < cxUpper; cx++ {
rb := sp.Cells[cy][cx].Objects
for _, o := range sp.Cells[cy][cx].Objects { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
if _, added := objectsAdded[o]; !added { if _, added := objectsAdded[o]; !added {
objects = append(objects, o) objects = append(objects, o)
objectsAdded[o] = true objectsAdded[o] = true
} }
} }
} }
} }
@@ -100,24 +115,16 @@ func (sp *Space) Objects() []*Object {
// Resize resizes the internal Cells array. // Resize resizes the internal Cells array.
func (sp *Space) Resize(width, height int) { func (sp *Space) Resize(width, height int) {
sp.Cells = make([][]*Cell, height)
sp.Cells = [][]*Cell{}
for y := 0; y < height; y++ { for y := 0; y < height; y++ {
sp.Cells[y] = make([]*Cell, width)
sp.Cells = append(sp.Cells, []*Cell{})
for x := 0; x < width; x++ { for x := 0; x < width; x++ {
sp.Cells[y] = append(sp.Cells[y], newCell(x, y)) sp.Cells[y][x] = newCell(x, y)
} }
} }
} }
// Cell returns the Cell at the given cellular / spatial (not world) X and Y position in the Space. If the X and Y position are func (sp *Space) GetCell(x, y int) *Cell {
// out of bounds, Cell() will return nil.
func (sp *Space) Cell(x, y int) *Cell {
if y >= 0 && y < len(sp.Cells) && x >= 0 && x < len(sp.Cells[y]) { if y >= 0 && y < len(sp.Cells) && x >= 0 && x < len(sp.Cells[y]) {
return sp.Cells[y][x] return sp.Cells[y][x]
@@ -134,28 +141,26 @@ func (sp *Space) CheckCells(x, y, w, h int, tags ...string) *Object {
for iy := y; iy < y+h; iy++ { for iy := y; iy < y+h; iy++ {
cell := sp.Cell(ix, iy) cell := sp.GetCell(ix, iy)
if cell != nil { if cell != nil {
rb := cell.Objects
if len(tags) > 0 { if len(tags) > 0 {
if cell.ContainsTags(tags...) { if cell.ContainsTags(tags...) {
for _, obj := range cell.Objects { for i := rb.StFrameId; i < rb.EdFrameId; i++ {
obj := rb.GetByFrameId(i).(*Object)
if obj.HasTags(tags...) { if obj.HasTags(tags...) {
return obj return obj
} }
} }
} }
} else if cell.Occupied() { } else if cell.Occupied() {
return cell.Objects[0] return rb.GetByFrameId(rb.StFrameId).(*Object)
} }
} }
} }
} }
return nil return nil
@@ -178,10 +183,13 @@ func (sp *Space) CheckCellsWorld(x, y, w, h float64, tags ...string) *Object {
func (sp *Space) UnregisterAllObjects() { func (sp *Space) UnregisterAllObjects() {
for y := 0; y < len(sp.Cells); y++ { for y := 0; y < len(sp.Cells); y++ {
for x := 0; x < len(sp.Cells[y]); x++ { for x := 0; x < len(sp.Cells[y]); x++ {
cell := sp.Cells[y][x] cell := sp.Cells[y][x]
sp.Remove(cell.Objects...) rb := cell.Objects
for i := rb.StFrameId; i < rb.EdFrameId; i++ {
o := rb.GetByFrameId(i).(*Object)
sp.RemoveSingle(o)
}
} }
} }
@@ -190,8 +198,9 @@ func (sp *Space) UnregisterAllObjects() {
// WorldToSpace converts from a world position (x, y) to a position in the Space (a grid-based position). // WorldToSpace converts from a world position (x, y) to a position in the Space (a grid-based position).
func (sp *Space) WorldToSpace(x, y float64) (int, int) { func (sp *Space) WorldToSpace(x, y float64) (int, int) {
fx := int(math.Floor(x / float64(sp.CellWidth))) // [WARNING] DON'T use "int(...)" syntax to convert float to int, it's not supported by go2cs!
fy := int(math.Floor(y / float64(sp.CellHeight))) var fx int = (int)(Floor(x / float64(sp.CellWidth)))
var fy int = (int)(Floor(y / float64(sp.CellHeight)))
return fx, fy return fx, fy
} }
@@ -218,8 +227,8 @@ func (sp *Space) Width() int {
func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell { func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell {
cells := []*Cell{} cells := []*Cell{}
cell := sp.Cell(startX, startY) cell := sp.GetCell(startX, startY)
endCell := sp.Cell(endX, endY) endCell := sp.GetCell(endX, endY)
if cell != nil && endCell != nil { if cell != nil && endCell != nil {
@@ -248,7 +257,7 @@ func (sp *Space) CellsInLine(startX, startY, endX, endY int) []*Cell {
} }
cx, cy := sp.WorldToSpace(p[0], p[1]) cx, cy := sp.WorldToSpace(p[0], p[1])
c := sp.Cell(cx, cy) c := sp.GetCell(cx, cy)
if c != cell { if c != cell {
cell = c cell = c
} }

View File

@@ -1,8 +1,6 @@
package resolv package resolv
import ( import "math"
"math"
)
// Vector is the definition of a row vector that contains scalars as // Vector is the definition of a row vector that contains scalars as
// 64 bit floats // 64 bit floats
@@ -14,106 +12,13 @@ type Axis int
const ( const (
// the consts below are used to represent vector axis, they are useful // the consts below are used to represent vector axis, they are useful
// to lookup values within the vector. // to lookup values within the vector.
X Axis = iota X Axis = 0
Y Y Axis = 1
Z Z Axis = 2
) )
// Clone a vector
func Clone(v Vector) Vector {
return v.Clone()
}
// Clone a vector
func (v Vector) Clone() Vector {
clone := make(Vector, len(v))
copy(clone, v)
return clone
}
// Add a vector with a vector or a set of vectors
func Add(v1 Vector, vs ...Vector) Vector {
return v1.Clone().Add(vs...)
}
// Add a vector with a vector or a set of vectors
func (v Vector) Add(vs ...Vector) Vector {
dim := len(v)
for i := range vs {
if len(vs[i]) > dim {
axpyUnitaryTo(v, 1, v, vs[i][:dim])
} else {
axpyUnitaryTo(v, 1, v, vs[i])
}
}
return v
}
// Sub subtracts a vector with another vector or a set of vectors
func Sub(v1 Vector, vs ...Vector) Vector {
return v1.Clone().Sub(vs...)
}
// Sub subtracts a vector with another vector or a set of vectors
func (v Vector) Sub(vs ...Vector) Vector {
dim := len(v)
for i := range vs {
if len(vs[i]) > dim {
axpyUnitaryTo(v, -1, vs[i][:dim], v)
} else {
axpyUnitaryTo(v, -1, vs[i], v)
}
}
return v
}
// Scale vector with a given size
func Scale(v Vector, size float64) Vector {
return v.Clone().Scale(size)
}
// Scale vector with a given size
func (v Vector) Scale(size float64) Vector {
scalUnitaryTo(v, size, v)
return v
}
// Equal compares that two vectors are equal to each other
func Equal(v1, v2 Vector) bool {
return v1.Equal(v2)
}
// Equal compares that two vectors are equal to each other
func (v Vector) Equal(v2 Vector) bool {
if len(v) != len(v2) {
return false
}
for i := range v {
if math.Abs(v[i]-v2[i]) > 1e-8 {
return false
}
}
return true
}
// Magnitude of a vector
func Magnitude(v Vector) float64 {
return v.Magnitude()
}
// Magnitude of a vector
func (v Vector) Magnitude() float64 {
return math.Sqrt(v.Magnitude2())
}
func (v Vector) Magnitude2() float64 { func (v Vector) Magnitude2() float64 {
var result float64 var result float64 = 0.
for _, scalar := range v { for _, scalar := range v {
result += scalar * scalar result += scalar * scalar
@@ -122,129 +27,19 @@ func (v Vector) Magnitude2() float64 {
return result return result
} }
// Unit returns a direction vector with the length of one.
func Unit(v Vector) Vector {
return v.Clone().Unit()
}
// Unit returns a direction vector with the length of one. // Unit returns a direction vector with the length of one.
func (v Vector) Unit() Vector { func (v Vector) Unit() Vector {
l := v.Magnitude() l2 := v.Magnitude2()
if l2 < 1e-16 {
if l < 1e-8 {
return v return v
} }
for i := range v { l := math.Sqrt(l2)
//inv := FastInvSqrt64(l2) // "Fast Inverse Square Root" is arch dependent, it's by far non-trivial to use it in Golang as well as make it feasible in the transpiled JavaScript.
for i := 0; i < len(v); i++ {
v[i] = v[i] / l v[i] = v[i] / l
} //v[i] = v[i] * inv
return v
}
// Dot product of two vectors
func Dot(v1, v2 Vector) float64 {
result, dim1, dim2 := 0., len(v1), len(v2)
if dim1 > dim2 {
v2 = append(v2, make(Vector, dim1-dim2)...)
}
if dim1 < dim2 {
v1 = append(v1, make(Vector, dim2-dim1)...)
}
for i := range v1 {
result += v1[i] * v2[i]
}
return result
}
// Dot product of two vectors
func (v Vector) Dot(v2 Vector) float64 {
return Dot(v, v2)
}
// Cross product of two vectors
func Cross(v1, v2 Vector) Vector {
return v1.Cross(v2)
}
// Cross product of two vectors
func (v Vector) Cross(v2 Vector) Vector {
if len(v) != 3 || len(v2) != 3 {
return nil
}
return Vector{
v[Y]*v2[Z] - v[Z]*v2[Y],
v[Z]*v2[X] - v[X]*v2[Z],
v[X]*v2[Z] - v[Z]*v2[X],
}
}
// Rotate is rotating a vector around a specified axis.
// If no axis are specified, it will default to the Z axis.
//
// If a vector with more than 3-dimensions is rotated, it will cut the extra
// dimensions and return a 3-dimensional vector.
//
// NOTE: the ...Axis is just syntactic sugar that allows the axis to not be
// specified and default to Z, if multiple axis is passed the first will be
// set as the rotation axis
func Rotate(v Vector, angle float64, as ...Axis) Vector {
return v.Clone().Rotate(angle, as...)
}
// Rotate is rotating a vector around a specified axis.
// If no axis are specified, it will default to the Z axis.
//
// If a vector with more than 3-dimensions is rotated, it will cut the extra
// dimensions and return a 3-dimensional vector.
//
// NOTE: the ...Axis is just syntactic sugar that allows the axis to not be
// specified and default to Z, if multiple axis is passed the first will be
// set as the rotation axis
func (v Vector) Rotate(angle float64, as ...Axis) Vector {
axis, dim := Z, len(v)
if dim == 0 {
return v
}
if len(as) > 0 {
axis = as[0]
}
if dim == 1 && axis != Z {
v = append(v, 0, 0)
}
if (dim < 2 && axis == Z) || (dim == 2 && axis != Z) {
v = append(v, 0)
}
x, y := v[X], v[Y]
cos, sin := math.Cos(angle), math.Sin(angle)
switch axis {
case X:
z := v[Z]
v[Y] = y*cos - z*sin
v[Z] = y*sin + z*cos
case Y:
z := v[Z]
v[X] = x*cos + z*sin
v[Z] = -x*sin + z*cos
case Z:
v[X] = x*cos - y*sin
v[Y] = x*sin + y*cos
}
if dim > 3 {
return v[:3]
} }
return v return v
@@ -252,7 +47,7 @@ func (v Vector) Rotate(angle float64, as ...Axis) Vector {
// X is corresponding to doing a v[0] lookup, if index 0 does not exist yet, a // X is corresponding to doing a v[0] lookup, if index 0 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) X() float64 { func (v Vector) GetX() float64 {
if len(v) < 1 { if len(v) < 1 {
return 0. return 0.
} }
@@ -262,7 +57,7 @@ func (v Vector) X() float64 {
// Y is corresponding to doing a v[1] lookup, if index 1 does not exist yet, a // Y is corresponding to doing a v[1] lookup, if index 1 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) Y() float64 { func (v Vector) GetY() float64 {
if len(v) < 2 { if len(v) < 2 {
return 0. return 0.
} }
@@ -272,7 +67,7 @@ func (v Vector) Y() float64 {
// Z is corresponding to doing a v[2] lookup, if index 2 does not exist yet, a // Z is corresponding to doing a v[2] lookup, if index 2 does not exist yet, a
// 0 will be returned instead // 0 will be returned instead
func (v Vector) Z() float64 { func (v Vector) GetZ() float64 {
if len(v) < 3 { if len(v) < 3 {
return 0. return 0.
} }