Compare commits

...

57 Commits
v0.2 ... v0.6

Author SHA1 Message Date
Wing
63164569b1 Merge pull request #5 from genxium/integer_positioning
Integer positioning.
2022-11-13 14:14:50 +08:00
genxium
8a4efd023b Updated README. 2022-11-13 14:13:19 +08:00
genxium
b031fc1c61 Minor fix on backend coordinate ratio conversion. 2022-11-13 13:06:52 +08:00
genxium
4369729d9c Fixed recovery upon reconnection. 2022-11-13 11:37:30 +08:00
genxium
2d080ad134 Fixed backend collision constants. 2022-11-12 22:53:35 +08:00
genxium
bd9beec5e5 Added more helper functions for backend collision debugging. 2022-11-12 20:34:38 +08:00
genxium
89a54211e1 Aligned constants used for backend collision unit-testing. 2022-11-12 12:18:00 +08:00
genxium
a4ebde3e07 Updated CLI unit tests again. 2022-11-12 11:41:18 +08:00
genxium
41967b11f7 Updated CLI unit tests. 2022-11-12 11:20:16 +08:00
genxium
98daeff408 Updated frontend logging. 2022-11-11 23:43:51 +08:00
genxium
320e98361e Fixes for resync. 2022-11-11 20:10:43 +08:00
genxium
15a062af10 Completed frontend refactoring for more convenient unit testing. 2022-11-11 13:27:48 +08:00
yflu
3f4e49656a Temp broken commit during frontend refactoring. 2022-11-11 00:04:00 +08:00
genxium
f97ce22cef Refactored backend for convenience of unit-testing. 2022-11-10 21:28:46 +08:00
genxium
901b189c5a Improvements on using integer positioning. 2022-11-10 18:18:00 +08:00
genxium
e5ed8124e8 Minor updates to frontend collider position reset pace. 2022-11-09 23:53:45 +08:00
genxium
885443c2b1 Fixes to frontend coordinate conversion. 2022-11-09 23:46:11 +08:00
genxium
aa795fcee5 Fixed frontend compilation. 2022-11-09 18:13:53 +08:00
genxium
cb3c19a339 Fixed Golang part compilation. 2022-11-09 14:20:26 +08:00
yflu
d3d3629618 A broken commit. 2022-11-09 12:19:29 +08:00
genxium
f37f4337de Minor update to floating number precision. 2022-11-08 21:38:23 +08:00
genxium
1a3b3a0a7a Fixed multi-barrier collision handling in backend. 2022-11-06 13:42:40 +08:00
genxium
4f1ce0d71a Fixed typos. 2022-11-03 10:06:21 +08:00
genxium
1f728071a9 Updated charts. 2022-11-03 00:16:03 +08:00
Wing
4b68917337 Fixed typo. 2022-11-02 12:40:26 +08:00
genxium
0cbf968228 Updated README. 2022-11-02 10:57:37 +08:00
genxium
ec2a21dbe7 Minor fix for "false == Room.BackendDynamicsEnabled". 2022-10-31 09:02:14 +08:00
Wing
dc6402c2b7 Updated README. 2022-10-26 10:28:32 +08:00
yflu
8038b393e0 Formatted codes. 2022-10-25 23:36:55 +08:00
yflu
4e0f7b52d4 Fixed frontend ws session onclose handling. 2022-10-25 23:02:39 +08:00
genxium
486c46f608 Added backend dynamics toggle. 2022-10-25 10:42:36 +08:00
genxium
6d075877ec Fixed frontend reconnection on page refresh for Firefox and Safari. 2022-10-25 09:52:38 +08:00
yflu
fe826b393b Updated logs in frontend. 2022-10-25 00:05:38 +08:00
Wing
c69aa25353 Merge pull request #4 from genxium/backend_collision_pushback
Backend collision pushback synchronization.
2022-10-22 13:52:52 +08:00
yflu
0f4d067c06 Updated test cases for frontend-backend-collision-reconciliation. 2022-10-22 13:38:10 +08:00
yflu
cff31d295c Simplified frontend log. 2022-10-22 00:03:26 +08:00
yflu
150e30db2a Drafted backend collision with pushback calculations. 2022-10-21 22:39:08 +08:00
genxium
bc8989a0e6 Minor update. 2022-10-19 17:56:52 +08:00
genxium
1959a7fd9a Refactored use of SAT collision checking. 2022-10-19 17:32:18 +08:00
genxium
3baaf1d52c Updated collider utility encapsulation for visualization subproject. 2022-10-19 15:10:11 +08:00
genxium
62f10e0877 Added collision test in collider visualizer. 2022-10-19 09:48:52 +08:00
genxium
c3c7854e92 Simplified frontend codes. 2022-10-16 10:38:38 +08:00
genxium
cb794d70c7 Updated video demo link. 2022-10-15 22:00:59 +08:00
Wing
0047259e9f Merge pull request #3 from genxium/backend_collision
Added backend collision handling.
2022-10-15 21:56:23 +08:00
genxium
232d8ad148 Fixes for backend collision handling. 2022-10-15 21:39:22 +08:00
genxium
d49e7830d4 Fixes for collider polygonal shape drawing. 2022-10-15 16:51:38 +08:00
genxium
1122f4d71c Added simple map for calibration. 2022-10-15 00:35:32 +08:00
genxium
f2c8d4cd65 Minor updates on logging. 2022-10-14 18:47:09 +08:00
genxium
4e7f9e63ac Made collider visualizer show the actual collider geometry from config file. 2022-10-14 18:02:03 +08:00
genxium
e762d257a6 Refactored module structure for ease of testing backend colliders. 2022-10-14 16:08:22 +08:00
genxium
286944b88c Added basic backend collider visualizer. 2022-10-14 11:49:04 +08:00
genxium
05dc593d2c Temp commit. 2022-10-10 21:58:29 +08:00
genxium
5f9aaddc9c Fixed clock sync and camera tracking. 2022-10-10 14:33:04 +08:00
genxium
e224aaf680 Updated pb schema. 2022-10-10 12:17:23 +08:00
yflu
9c07b43157 Minor update. 2022-10-07 10:36:19 +08:00
yflu
3203ea9f1e Fixed typo. 2022-10-07 10:10:33 +08:00
genxium
d90c4ead91 Updated README. 2022-10-05 23:43:38 +08:00
105 changed files with 4801 additions and 12612 deletions

View File

@@ -1,47 +1,91 @@
# 0. Preface
If you'd like to play with the backend code seriously, please read the detailed explanation of its important "lifecycle events" in [this note](https://app.yinxiang.com/fx/5c575124-01db-419b-9c02-ec81f78c6ddc).
# Preface
There could be some left over wechat-game related code pieces, but they're neither meant to work nor supported anymore.
This project is a demo for a websocket-based input synchronization method inspired by [GGPO](https://www.ggpo.net/).
_(how input delay roughly works)_
![input_delay_intro](./charts/InputDelayIntro.jpg)
_(how rollback-and-chase in this project roughly works)_
![rollback_and_chase_intro](./charts/RollbackAndChase.jpg)
![floating_point_accumulation_err](./charts/AvoidingFloatingPointAccumulationErr.jpg)
_(in game screenshot)_
![screenshot-1](./charts/screenshot-1.png)
Please checkout [this demo video](https://pan.baidu.com/s/1YkfuHjNLzlFVnKiEj6wrDQ?pwd=tkr5) to see whether the source codes are doing what you expect for synchronization.
The video mainly shows the following features.
- The backend receives inputs from frontend peers and [by a GGPO-alike manner](https://github.com/pond3r/ggpo/blob/master/doc/README.md) broadcasts back for synchronization.
- The game is recovered for a player upon reconnection.
- Both backend(Golang) and frontend(JavaScript) execute collision detection and handle collision contacts by the same algorithm. The backend dynamics can be toggled off by [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.5.2/battle_srv/models/room.go#L813), but **when turned off the game couldn't support recovery upon reconnection**.
# 1. Building & running
## 1.1 Golang1.19.1
Documentation TBD.
## 1.1 Tools to install
### Backend
- [Command Line Tools for Xcode](https://developer.apple.com/download/all/?q=command%20line%20tools) (on OSX) or [TDM-GCC](https://jmeubank.github.io/tdm-gcc/download/) (on Windows) (a `make` executable mandatory)
- [Golang1.19.1](https://golang.org/dl/) (mandatory, in China please try a mirror site like [that of ustc](https://mirrors.ustc.edu.cn/golang/))
- [MySQL 5.7](https://dev.mysql.com/downloads/windows/installer/5.7.html) (mandatory, for OSX not all versions of 5.7 can be found thus 5.7.24 is recommended)
- [Redis 3.0.503 or above](https://redis.io/download/) (mandatory)
- [skeema](https://www.skeema.io/) (optional, only for convenient MySQL schema provisioning)
- [protobuf CLI](https://developers.google.com/protocol-buffers/docs/downloads) (optional, only for development)
## 1.2 MySQL
The database product to be used for this project is MySQL 5.7, you can install and manage `MySQL` server by [these scripts](https://github.com/genxium/Ubuntu14InitScripts/tree/master/database/mysql).
### Frontend
- [CocosCreator v2.2.1](https://www.cocos.com/en/cocos-creator-2-2-1-released-with-performance-improvements) (mandatory, **ONLY AVAILABLE on Windows or OSX and should be exactly this version**, DON'T use any other version because CocosCreator is well-known for new versions not being backward compatible)
- [protojs](https://www.npmjs.com/package/protojs) (optional, only for development)
We use [skeema](https://github.com/skeema/skeema) for schematic synchronization under `<proj-root>/database/skeema-repo-root/` which intentionally doesn't contain a `.skeema` file. Please read [this tutorial](https://shimo.im/doc/wQ0LvB0rlZcbHF5V) for more information. For `Windows 10/11`, you can compile `skeema` from source and config the host to be `127.0.0.1` instead of `localhost` to use it, i.e. circumventing the pitfall for MySQL unix socket connection on Windows.
The following command(s)
## 1.2 Provisioning
### Backend/Database
It's strongly recommended that `skeema` is used for provisioning [the required schema](https://github.com/genxium/DelayNoMore/tree/main/database/skeema-repo-root) in MySQL instance. When using `skeema` the steps are as follows.
```
### Optional.
### Mandatory after an initial clone
user@proj-root/database/skeema-repo-root> cp .skeema.template .skeema
###
user@proj-root/database/skeema-repo-root> skeema diff
### Mandatory
user@proj-root/database/skeema-repo-root> skeema push
```
is recommended to be used for checking difference from your "live MySQL server" to the latest expected schema tracked in git.
## 1.3 Required Config Files
On `Windows 10/11`, you can compile `skeema` from source and config the host to be `127.0.0.1` instead of `localhost` to use it, i.e. circumventing the pitfall for MySQL unix socket connection on Windows.
### 1.3.1 Backend
- It needs `<proj-root>/battle_srv/configs/*` which is generated by `cd <proj-root>/battle_srv && cp -r ./configs.template ./configs` and necessary customization.
### Backend/Golang
```
user@proj-root/battle_srv/configs> cp -r ./configs.template ./configs
```
### 1.3.2 Frontend
- It needs CocosCreator v2.2.1 to build.
- A required "CocosCreator plugin `i18n`" is already enclosed in the project, if you have a globally installed "CocosCreator plugin `i18n`"(often located at `$HOME/.CocosCreator/packages/`) they should be OK to co-exist.
- It needs `<proj-root>/frontend/assets/plugin_scripts/conf.js` which is generated by `cd <proj-root>/frontend/assets/plugin_scripts && cp conf.js.template conf.js`.
### Frontend
```
user@proj-root/frontend/assets/plugin_scripts> cp ./conf.js.template ./conf.js
```
## 1.4 Troubleshooting
## 1.2 Actual building & running
### Backend
```
### The following command runs mysql-server in foreground, it's almost NEVER run in such a way, please find a proper way to run it for yourself
user@anywhere> mysqld
### 1.4.1 Redis snapshot writing failure
### The following command runs redis-server in foreground, it's OK to put it in background
user@anywhere> redis-server
### on Windows using TDM-GCC: mingw32-make run-test
user@proj-root/battle_srv> make run-test
```
### Frontend
The easy way is to try out 2 players with test accounts on a same machine.
- Open CocosCreator v2.2.1 (mandatory, it serves the web content of the following steps)
- 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.
- Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync.
![screenshot-2](./charts/screenshot-2.png)
## 2 Troubleshooting
### 2.1 Redis snapshot writing failure
```
ErrFatal {"err": "MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error."}
```
Just restart your `redis-server` process.
# 2. Git configs cautions
Please make sure that you've set `ignorecase = false` in your `[core] section of <proj-root>/.git/config`.

View File

@@ -2,10 +2,10 @@ package api
import (
"bytes"
. "dnmshared"
"github.com/gin-gonic/gin"
"io"
"io/ioutil"
. "server/common"
)
func RequestLogger() gin.HandlerFunc {

View File

@@ -1,6 +1,11 @@
package v1
import (
"battle_srv/api"
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/models"
"battle_srv/storage"
"bytes"
"crypto/sha256"
"encoding/hex"
@@ -10,12 +15,9 @@ import (
"go.uber.org/zap"
"io/ioutil"
"net/http"
"server/api"
. "server/common"
"server/common/utils"
"server/models"
"server/storage"
"strconv"
. "dnmshared"
)
var Player = playerController{}
@@ -77,7 +79,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
c.Set(api.RET, Constants.RetCode.UnknownError)
return
}
// Redis剩余时长校验
if ttl >= ConstVals.Player.CaptchaMaxTTL {
Logger.Info("There's an existing SmsCaptcha record in Redis-server: ", zap.String("key", redisKey), zap.Duration("ttl", ttl))
c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently)
@@ -87,7 +88,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
pass := false
var succRet int
if Conf.General.ServerEnv == SERVER_ENV_TEST {
// 测试环境,优先从数据库校验`player.name`不通过再走机器人magic name校验
player, err := models.GetPlayerByName(req.Num)
if nil == err && nil != player {
pass = true
@@ -96,7 +96,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
}
if !pass {
// 机器人magic name校验不通过再走手机号校验
player, err := models.GetPlayerByName(req.Num)
if nil == err && nil != player {
pass = true
@@ -109,7 +108,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
succRet = Constants.RetCode.Ok
pass = true
}
// Hardecoded 只验证国内手机号格式
if req.CountryCode == "86" {
if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
succRet = Constants.RetCode.Ok
@@ -131,7 +129,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
}{Ret: succRet}
var captcha string
if ttl >= 0 {
// 已有未过期的旧验证码记录,续验证码有效期。
storage.RedisManagerIns.Expire(redisKey, ConstVals.Player.CaptchaExpire)
captcha = storage.RedisManagerIns.Get(redisKey).Val()
if ttl >= ConstVals.Player.CaptchaExpire/4 {
@@ -145,7 +142,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
}
Logger.Info("Extended ttl of existing SMSCaptcha record in Redis:", zap.String("key", redisKey), zap.String("captcha", captcha))
} else {
// 校验通过,进行验证码生成处理
captcha = strconv.Itoa(utils.Rand.Number(1000, 9999))
if succRet == Constants.RetCode.Ok {
getSmsCaptchaRespErrorCode := sendSMSViaVendor(req.Num, req.CountryCode, captcha)
@@ -232,7 +228,6 @@ func (p *playerController) WechatLogin(c *gin.Context) {
return
}
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
baseInfo, err := utils.WechatIns.GetOauth2Basic(req.Authcode)
if err != nil {
@@ -248,7 +243,6 @@ func (p *playerController) WechatLogin(c *gin.Context) {
c.Set(api.RET, Constants.RetCode.WechatServerError)
return
}
//fserver不会返回openId
userInfo.OpenID = baseInfo.OpenID
player, err := p.maybeCreatePlayerWechatAuthBinding(userInfo)
@@ -314,7 +308,6 @@ func (p *playerController) WechatGameLogin(c *gin.Context) {
return
}
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
baseInfo, err := utils.WechatGameIns.GetOauth2Basic(req.Authcode)
if err != nil {
@@ -335,7 +328,6 @@ func (p *playerController) WechatGameLogin(c *gin.Context) {
c.Set(api.RET, Constants.RetCode.WechatServerError)
return
}
//fserver不会返回openId
userInfo.OpenID = baseInfo.OpenID
player, err := p.maybeCreatePlayerWechatGameAuthBinding(userInfo)
@@ -393,7 +385,6 @@ func (p *playerController) IntAuthTokenLogin(c *gin.Context) {
return
}
//kobako: 从player获取display name等
player, err := models.GetPlayerById(playerLogin.PlayerID)
if err != nil {
Logger.Error("Get player by id in IntAuthTokenLogin function error: ", zap.Error(err))
@@ -477,7 +468,6 @@ func (p *playerController) TokenAuth(c *gin.Context) {
c.Abort()
}
// 以下是内部私有函数
func (p *playerController) maybeCreateNewPlayer(req smsCaptchaReq) (*models.Player, error) {
extAuthID := req.extAuthID()
if Conf.General.ServerEnv == SERVER_ENV_TEST {
@@ -490,7 +480,7 @@ 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))
return player, nil
}
} else { //正式环境检查是否为bot用户
} else {
botPlayer, err := models.GetPlayerByName(req.Num)
if err != nil {
Logger.Error("Seeking bot player error:", zap.Error(err))
@@ -535,19 +525,17 @@ func (p *playerController) maybeCreatePlayerWechatAuthBinding(userInfo utils.Use
return nil, err
}
if player != nil {
{ //更新玩家姓名及头像
updateInfo := models.Player{
Avatar: userInfo.HeadImgURL,
DisplayName: userInfo.Nickname,
}
tx := storage.MySQLManagerIns.MustBegin()
defer tx.Rollback()
ok, err := models.Update(tx, player.Id, &updateInfo)
if err != nil && ok != true {
return nil, err
} else {
tx.Commit()
}
updateInfo := models.Player{
Avatar: userInfo.HeadImgURL,
DisplayName: userInfo.Nickname,
}
tx := storage.MySQLManagerIns.MustBegin()
defer tx.Rollback()
ok, err := models.Update(tx, player.Id, &updateInfo)
if err != nil && ok != true {
return nil, err
} else {
tx.Commit()
}
return player, nil
}
@@ -573,19 +561,17 @@ func (p *playerController) maybeCreatePlayerWechatGameAuthBinding(userInfo utils
return nil, err
}
if player != nil {
{ //更新玩家姓名及头像
updateInfo := models.Player{
Avatar: userInfo.HeadImgURL,
DisplayName: userInfo.Nickname,
}
tx := storage.MySQLManagerIns.MustBegin()
defer tx.Rollback()
ok, err := models.Update(tx, player.Id, &updateInfo)
if err != nil && ok != true {
return nil, err
} else {
tx.Commit()
}
updateInfo := models.Player{
Avatar: userInfo.HeadImgURL,
DisplayName: userInfo.Nickname,
}
tx := storage.MySQLManagerIns.MustBegin()
defer tx.Rollback()
ok, err := models.Update(tx, player.Id, &updateInfo)
if err != nil && ok != true {
return nil, err
} else {
tx.Commit()
}
return player, nil
}
@@ -670,15 +656,13 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
Nationcode: nationcode,
}
var captchaExpireMin string
//短信有效期hardcode
if Conf.General.ServerEnv == SERVER_ENV_TEST {
//测试环境下有效期为20秒 先hardcode
captchaExpireMin = "0.5"
captchaExpireMin = "0.5" // Hardcoded
} else {
captchaExpireMin = strconv.Itoa(int(ConstVals.Player.CaptchaExpire) / 60000000000)
}
params := [2]string{captchaCode, captchaExpireMin}
appkey := "41a5142feff0b38ade02ea12deee9741" // TODO: Should read from config file!
appkey := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // TODO: Should read from config file!
rand := strconv.Itoa(utils.Rand.Number(1000, 9999))
now := utils.UnixtimeSec()
@@ -692,7 +676,7 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
Extend: "",
Params: &params,
Sig: sig,
Sign: "洛克互娱",
Sign: "YYYYYYYYYYYYYYYYY",
Tel: tel,
Time: now,
Tpl_id: 207399,
@@ -703,7 +687,7 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
Logger.Info("json marshal", zap.Any("err:", err))
return -1
}
resp, err := http.Post("https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=1400150185&random="+rand,
resp, err := http.Post("https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=uuuuuuuuuuuuuuuuuuuuuuuu&random="+rand,
"application/json",
req)
if err != nil {

View File

@@ -1,16 +1,15 @@
package common
import (
. "dnmshared"
"encoding/json"
"fmt"
"go.uber.org/zap"
"os"
"path/filepath"
"strings"
"go.uber.org/zap"
)
// 隐式导入
var Conf *config
const (
@@ -72,11 +71,15 @@ func MustParseConfig() {
BotServer: new(botServerConf),
}
execPath, err := os.Executable()
ErrFatal(err)
if nil != err {
panic(err)
}
pwd, err := os.Getwd()
Logger.Debug("os.GetWd", zap.String("pwd", pwd))
ErrFatal(err)
if nil != err {
panic(err)
}
appRoot := pwd
confDir := filepath.Join(appRoot, "configs")
@@ -129,22 +132,21 @@ func loadJSON(fp string, v interface{}) {
fp = filepath.Join(Conf.General.ConfDir, fp)
}
_, err := os.Stat(fp)
ErrFatal(err)
if nil != err {
panic(err)
}
fd, err := os.Open(fp)
ErrFatal(err)
if nil != err {
panic(err)
}
defer fd.Close()
Logger.Info("Opened json file successfully.", zap.String("fp", fp))
err = json.NewDecoder(fd).Decode(v)
ErrFatal(err)
Logger.Info("Loaded json file successfully.", zap.String("fp", fp))
}
// Please only use this auxiliary function before server is fully started up, but not afterwards (启动过程可以使用,运行时不准使用).
func ErrFatal(err error) {
if err != nil {
Logger.Fatal("ErrFatal", zap.NamedError("err", err))
if nil != err {
panic(err)
}
Logger.Info("Loaded json file successfully.", zap.String("fp", fp))
}
func isNotExist(p string) bool {

View File

@@ -5,9 +5,10 @@ import (
"github.com/imdario/mergo"
"go.uber.org/zap"
. "dnmshared"
)
// 隐式导入
var Constants *constants
func MustParseConstants() {
@@ -24,13 +25,11 @@ func MustParseConstants() {
if !isNotExist(fp) {
testConstants := new(constants)
loadJSON(fp, testConstants)
//Logger.Debug(spew.Sdump(Constants))
//Logger.Debug(spew.Sdump(testConstants))
err := mergo.Merge(testConstants, Constants)
ErrFatal(err)
if nil != err {
panic(err)
}
Constants = testConstants
//Logger.Debug("mergo.Merge", zap.Error(err))
//Logger.Debug(spew.Sdump(testConstants))
}
}
constantsPost()

View File

@@ -1,8 +1,11 @@
package utils
import (
. "battle_srv/common"
. "battle_srv/configs"
"bytes"
"crypto/sha1"
. "dnmshared"
"encoding/json"
"fmt"
"go.uber.org/zap"
@@ -10,8 +13,6 @@ import (
"io/ioutil"
"math/rand"
"net/http"
. "server/common"
. "server/configs"
"sort"
"time"
)
@@ -249,10 +250,6 @@ func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
return
}
//jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", w.config.AppID)
//expires := ticket.ExpiresIn - 1500
//set
//err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
return
}
@@ -275,9 +272,6 @@ func (w *wechat) getAccessTokenFromServer() (accessToken string, err error) {
return
}
//accessTokenCacheKey := fmt.Sprintf("access_token_%s", w.config.AppID)
//expires := r.ExpiresIn - 1500
//set to redis err = ctx.Cache.Set(accessTokenCacheKey, r.AccessToken, time.Duration(expires)*time.Second)
accessToken = r.AccessToken
return
}

View File

@@ -11,7 +11,6 @@ var (
RE_CHINA_PHONE_NUM = regexp.MustCompile(`^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$`)
)
// 隐式导入
var ConstVals = &struct {
Player struct {
CaptchaExpire time.Duration

View File

@@ -1,21 +1,24 @@
package env_tools
import (
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/models"
"battle_srv/storage"
. "dnmshared"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"go.uber.org/zap"
. "server/common"
"server/common/utils"
"server/models"
"server/storage"
)
func LoadPreConf() {
Logger.Info(`Merging PreConfSQLite data into MySQL`,
zap.String("PreConfSQLitePath", Conf.General.PreConfSQLitePath))
db, err := sqlx.Connect("sqlite3", Conf.General.PreConfSQLitePath)
ErrFatal(err)
if nil != err {
panic(err)
}
defer db.Close()
loadPreConfToMysql(db)
@@ -39,7 +42,9 @@ func loadPreConfToMysql(db *sqlx.DB) {
func loadSqlite(db *sqlx.DB, tbs []string) {
for _, v := range tbs {
result, err := storage.MySQLManagerIns.Exec("truncate " + v)
ErrFatal(err)
if nil != err {
panic(err)
}
Logger.Info("truncate", zap.Any("truncate "+v, result))
query, args, err := sq.Select("*").From(v).ToSql()
if err != nil {
@@ -70,19 +75,25 @@ func createMysqlData(rows *sqlx.Rows, v string) {
func maybeCreateNewPlayerFromBotTable(db *sqlx.DB, tableName string) {
var ls []*dbBotPlayer
err := db.Select(&ls, "SELECT name, magic_phone_country_code, magic_phone_num, display_name FROM "+tableName)
ErrFatal(err)
if nil != err {
panic(err)
}
names := make([]string, len(ls), len(ls))
for i, v := range ls {
names[i] = v.Name
}
sql := "SELECT name FROM `player` WHERE name in (?)"
query, args, err := sqlx.In(sql, names)
ErrFatal(err)
if nil != err {
panic(err)
}
query = storage.MySQLManagerIns.Rebind(query)
// existNames := make([]string, len(ls), len(ls))
var existPlayers []*models.Player
err = storage.MySQLManagerIns.Select(&existPlayers, query, args...)
ErrFatal(err)
if nil != err {
panic(err)
}
for _, botPlayer := range ls {
var flag bool

View File

@@ -1,10 +1,11 @@
package env_tools
import (
. "server/common"
"server/common/utils"
"server/models"
"server/storage"
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/models"
"battle_srv/storage"
. "dnmshared"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
@@ -15,7 +16,9 @@ func MergeTestPlayerAccounts() {
fp := Conf.General.TestEnvSQLitePath
Logger.Info(`Initializing TestPlayerAccounts in runtime MySQLServer from SQLite file:`, zap.String("fp", fp))
db, err := sqlx.Connect("sqlite3", fp)
ErrFatal(err)
if nil != err {
panic(err)
}
defer db.Close()
maybeCreateNewPlayer(db, "test_player")
}
@@ -29,31 +32,35 @@ type dbTestPlayer struct {
func maybeCreateNewPlayer(db *sqlx.DB, tableName string) {
var ls []*dbTestPlayer
err := db.Select(&ls, "SELECT name, magic_phone_country_code, magic_phone_num FROM "+tableName)
ErrFatal(err)
if nil != err {
panic(err)
}
names := make([]string, len(ls), len(ls))
for i, v := range ls {
names[i] = v.Name
}
sql := "SELECT name FROM `player` WHERE name in (?)"
query, args, err := sqlx.In(sql, names)
ErrFatal(err)
if nil != err {
panic(err)
}
query = storage.MySQLManagerIns.Rebind(query)
// existNames := make([]string, len(ls), len(ls))
var existPlayers []*models.Player
err = storage.MySQLManagerIns.Select(&existPlayers, query, args...)
ErrFatal(err)
if nil != err {
panic(err)
}
for _, testPlayer := range ls {
var flag bool
for _, v := range existPlayers {
if testPlayer.Name == v.Name {
// 已有数据,合并处理
flag = true
break
}
}
if !flag {
// 找不到,新增
Logger.Debug("create", zap.Any(tableName, testPlayer))
err := createNewPlayer(testPlayer)
if err != nil {

View File

@@ -1,9 +1,8 @@
module server
module battle_srv
go 1.19
require (
github.com/ByteArena/box2d v1.0.2
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414
github.com/davecgh/go-spew v1.1.1
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be
@@ -21,6 +20,8 @@ require (
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713
go.uber.org/zap v1.9.1
google.golang.org/protobuf v1.28.1
dnmshared v0.0.0
)
require (
@@ -44,3 +45,7 @@ require (
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
)
replace (
dnmshared => ../dnmshared
)

View File

@@ -1,24 +1,24 @@
package main
import (
"battle_srv/api"
"battle_srv/api/v1"
. "battle_srv/common"
"battle_srv/env_tools"
"battle_srv/models"
"battle_srv/storage"
"battle_srv/ws"
"context"
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"server/api"
"server/api/v1"
. "server/common"
"server/common/utils"
"server/configs"
"server/env_tools"
"server/models"
"server/storage"
"server/ws"
"syscall"
"time"
. "dnmshared"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/robfig/cron"
@@ -30,8 +30,6 @@ func main() {
MustParseConstants()
storage.Init()
env_tools.LoadPreConf()
utils.InitWechat(configs.WechatConfigIns)
utils.InitWechatGame(configs.WechatGameConfigIns)
if Conf.General.ServerEnv == SERVER_ENV_TEST {
env_tools.MergeTestPlayerAccounts()
}

View File

@@ -1,5 +1,9 @@
package models
import (
. "dnmshared/sharedprotos"
)
type Barrier struct {
Boundary *Polygon2D
}

View File

@@ -1,132 +0,0 @@
package models
import (
"fmt"
"github.com/ByteArena/box2d"
"math"
)
// Use type `float64` for json unmarshalling of numbers.
type Direction struct {
Dx int32 `json:"dx,omitempty"`
Dy int32 `json:"dy,omitempty"`
}
type Vec2D struct {
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
}
func CreateVec2DFromB2Vec2(b2V2 box2d.B2Vec2) *Vec2D {
return &Vec2D{
X: b2V2.X,
Y: b2V2.Y,
}
}
func (v2 *Vec2D) ToB2Vec2() box2d.B2Vec2 {
return box2d.MakeB2Vec2(v2.X, v2.Y)
}
type Polygon2D struct {
Anchor *Vec2D `json:"-"` // This "Polygon2D.Anchor" is used to be assigned to "B2BodyDef.Position", which in turn is used as the position of the FIRST POINT of the polygon.
Points []*Vec2D `json:"-"`
/*
When used to represent a "polyline directly drawn in a `Tmx file`", we can initialize both "Anchor" and "Points" simultaneously.
Yet when used to represent a "polyline drawn in a `Tsx file`", we have to first initialize "Points w.r.t. center of the tile-rectangle", and then "Anchor(initially nil) of the tile positioned in the `Tmx file`".
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for more information.
*/
/*
[WARNING] Used to cache "`TileWidth & TileHeight` of a Tsx file" only.
*/
TileWidth int
TileHeight int
/*
[WARNING] Used to cache "`Width & TileHeight` of an object in Tmx file" only.
*/
TmxObjectWidth float64
TmxObjectHeight float64
}
func MoveDynamicBody(body *box2d.B2Body, pToTargetPos *box2d.B2Vec2, inSeconds float64) {
if body.GetType() != box2d.B2BodyType.B2_dynamicBody {
return
}
body.SetTransform(*pToTargetPos, 0.0)
body.SetLinearVelocity(box2d.MakeB2Vec2(0.0, 0.0))
body.SetAngularVelocity(0.0)
}
func PrettyPrintFixture(fix *box2d.B2Fixture) {
fmt.Printf("\t\tfriction:\t%v\n", fix.M_friction)
fmt.Printf("\t\trestitution:\t%v\n", fix.M_restitution)
fmt.Printf("\t\tdensity:\t%v\n", fix.M_density)
fmt.Printf("\t\tisSensor:\t%v\n", fix.M_isSensor)
fmt.Printf("\t\tfilter.categoryBits:\t%d\n", fix.M_filter.CategoryBits)
fmt.Printf("\t\tfilter.maskBits:\t%d\n", fix.M_filter.MaskBits)
fmt.Printf("\t\tfilter.groupIndex:\t%d\n", fix.M_filter.GroupIndex)
switch fix.M_shape.GetType() {
case box2d.B2Shape_Type.E_circle:
{
s := fix.M_shape.(*box2d.B2CircleShape)
fmt.Printf("\t\tb2CircleShape shape: {\n")
fmt.Printf("\t\t\tradius:\t%v\n", s.M_radius)
fmt.Printf("\t\t\toffset:\t%v\n", s.M_p)
fmt.Printf("\t\t}\n")
}
break
case box2d.B2Shape_Type.E_polygon:
{
s := fix.M_shape.(*box2d.B2PolygonShape)
fmt.Printf("\t\tb2PolygonShape shape: {\n")
for i := 0; i < s.M_count; i++ {
fmt.Printf("\t\t\t%v\n", s.M_vertices[i])
}
fmt.Printf("\t\t}\n")
}
break
default:
break
}
}
func PrettyPrintBody(body *box2d.B2Body) {
bodyIndex := body.M_islandIndex
fmt.Printf("{\n")
fmt.Printf("\tHeapRAM addr:\t%p\n", body)
fmt.Printf("\ttype:\t%d\n", body.M_type)
fmt.Printf("\tposition:\t%v\n", body.GetPosition())
fmt.Printf("\tangle:\t%v\n", body.M_sweep.A)
fmt.Printf("\tlinearVelocity:\t%v\n", body.GetLinearVelocity())
fmt.Printf("\tangularVelocity:\t%v\n", body.GetAngularVelocity())
fmt.Printf("\tlinearDamping:\t%v\n", body.M_linearDamping)
fmt.Printf("\tangularDamping:\t%v\n", body.M_angularDamping)
fmt.Printf("\tallowSleep:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_autoSleepFlag)
fmt.Printf("\tawake:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_awakeFlag)
fmt.Printf("\tfixedRotation:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_fixedRotationFlag)
fmt.Printf("\tbullet:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_bulletFlag)
fmt.Printf("\tactive:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_activeFlag)
fmt.Printf("\tgravityScale:\t%v\n", body.M_gravityScale)
fmt.Printf("\tislandIndex:\t%v\n", bodyIndex)
fmt.Printf("\tfixtures: {\n")
for f := body.M_fixtureList; f != nil; f = f.M_next {
PrettyPrintFixture(f)
}
fmt.Printf("\t}\n")
fmt.Printf("}\n")
}
func Distance(pt1 *Vec2D, pt2 *Vec2D) float64 {
dx := pt1.X - pt2.X
dy := pt1.Y - pt2.Y
return math.Sqrt(dx*dx + dy*dy)
}

View File

@@ -1,76 +1,22 @@
package models
import (
pb "server/pb_output"
. "battle_srv/protos"
. "dnmshared/sharedprotos"
)
func toPbVec2D(modelInstance *Vec2D) *pb.Vec2D {
toRet := &pb.Vec2D{
X: modelInstance.X,
Y: modelInstance.Y,
}
return toRet
}
func toPbPolygon2D(modelInstance *Polygon2D) *pb.Polygon2D {
toRet := &pb.Polygon2D{
Anchor: toPbVec2D(modelInstance.Anchor),
Points: make([]*pb.Vec2D, len(modelInstance.Points)),
}
for index, p := range modelInstance.Points {
toRet.Points[index] = toPbVec2D(p)
}
return toRet
}
func toPbVec2DList(modelInstance *Vec2DList) *pb.Vec2DList {
toRet := &pb.Vec2DList{
Vec2DList: make([]*pb.Vec2D, len(*modelInstance)),
}
for k, v := range *modelInstance {
toRet.Vec2DList[k] = toPbVec2D(v)
}
return toRet
}
func ToPbVec2DListMap(modelInstances map[string]*Vec2DList) map[string]*pb.Vec2DList {
toRet := make(map[string]*pb.Vec2DList, len(modelInstances))
for k, v := range modelInstances {
toRet[k] = toPbVec2DList(v)
}
return toRet
}
func toPbPolygon2DList(modelInstance *Polygon2DList) *pb.Polygon2DList {
toRet := &pb.Polygon2DList{
Polygon2DList: make([]*pb.Polygon2D, len(*modelInstance)),
}
for k, v := range *modelInstance {
toRet.Polygon2DList[k] = toPbPolygon2D(v)
}
return toRet
}
func ToPbPolygon2DListMap(modelInstances map[string]*Polygon2DList) map[string]*pb.Polygon2DList {
toRet := make(map[string]*pb.Polygon2DList, len(modelInstances))
for k, v := range modelInstances {
toRet[k] = toPbPolygon2DList(v)
}
return toRet
}
func toPbPlayers(modelInstances map[int32]*Player) map[int32]*pb.Player {
toRet := make(map[int32]*pb.Player, 0)
func toPbPlayers(modelInstances map[int32]*Player) map[int32]*PlayerDownsync {
toRet := make(map[int32]*PlayerDownsync, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
toRet[k] = &pb.Player{
Id: last.Id,
X: last.X,
Y: last.Y,
Dir: &pb.Direction{
toRet[k] = &PlayerDownsync{
Id: last.Id,
VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY,
Dir: &Direction{
Dx: last.Dir.Dx,
Dy: last.Dir.Dy,
},

View File

@@ -2,6 +2,7 @@ package models
import (
"database/sql"
. "dnmshared/sharedprotos"
"fmt"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
@@ -32,30 +33,32 @@ func InitPlayerBattleStateIns() {
}
type Player struct {
Id int32 `json:"id,omitempty" db:"id"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Dir *Direction `json:"dir,omitempty"`
Speed float64 `json:"speed,omitempty"`
BattleState int32 `json:"battleState,omitempty"`
LastMoveGmtMillis int32 `json:"lastMoveGmtMillis,omitempty"`
Score int32 `json:"score,omitempty"`
Removed bool `json:"removed,omitempty"`
JoinIndex int32
// Meta info fields
Id int32 `json:"id,omitempty" db:"id"`
Name string `json:"name,omitempty" db:"name"`
DisplayName string `json:"displayName,omitempty" db:"display_name"`
Avatar string `json:"avatar,omitempty"`
ColliderRadius float64 `json:"-"`
Name string `json:"name,omitempty" db:"name"`
DisplayName string `json:"displayName,omitempty" db:"display_name"`
Avatar string `json:"avatar,omitempty"`
// DB only fields
CreatedAt int64 `db:"created_at"`
UpdatedAt int64 `db:"updated_at"`
DeletedAt NullInt64 `db:"deleted_at"`
TutorialStage int `db:"tutorial_stage"`
FrozenAtGmtMillis int64 `json:"-" db:"-"`
AddSpeedAtGmtMillis int64 `json:"-" db:"-"`
CreatedAt int64 `json:"-" db:"created_at"`
UpdatedAt int64 `json:"-" db:"updated_at"`
DeletedAt NullInt64 `json:"-" db:"deleted_at"`
TutorialStage int `json:"-" db:"tutorial_stage"`
AckingFrameId int32 `json:"ackingFrameId"`
AckingInputFrameId int32 `json:"-"`
LastSentInputFrameId int32 `json:"-"`
// in-battle info fields
VirtualGridX int32
VirtualGridY int32
Dir *Direction
Speed int32
BattleState int32
LastMoveGmtMillis int32
Score int32
Removed bool
JoinIndex int32
AckingFrameId int32
AckingInputFrameId int32
LastSentInputFrameId int32
}
func ExistPlayerByName(name string) (bool, error) {

View File

@@ -1,9 +1,9 @@
package models
import (
"battle_srv/storage"
"database/sql"
. "server/common"
"server/storage"
. "dnmshared"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"

View File

@@ -1,10 +1,10 @@
package models
import (
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/storage"
"database/sql"
. "server/common"
"server/common/utils"
"server/storage"
sq "github.com/Masterminds/squirrel"
)

View File

@@ -1,10 +1,11 @@
package models
import (
. "battle_srv/common"
"battle_srv/common/utils"
"database/sql"
. "dnmshared"
"errors"
. "server/common"
"server/common/utils"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"

View File

@@ -1,6 +1,11 @@
package models
import (
. "battle_srv/common"
"battle_srv/common/utils"
. "battle_srv/protos"
. "dnmshared"
. "dnmshared/sharedprotos"
"encoding/xml"
"fmt"
"github.com/golang/protobuf/proto"
@@ -12,9 +17,6 @@ import (
"math/rand"
"os"
"path/filepath"
. "server/common"
"server/common/utils"
pb "server/pb_output"
"strings"
"sync"
"sync/atomic"
@@ -59,36 +61,17 @@ const (
MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2
)
// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged.
var DIRECTION_DECODER = [][]int32{
{0, 0},
{0, +1},
{0, -1},
{0, +2},
{0, -2},
{+2, 0},
{-2, 0},
{+2, +1},
{-2, -1},
{+2, -1},
{-2, +1},
{+2, 0},
{-2, 0},
{0, +1},
{0, -1},
}
var DIRECTION_DECODER_INVERSE_LENGTH = []float64{
0.0,
1.0,
1.0,
0.5,
0.5,
0.4472,
0.4472,
0.4472,
0.4472,
0.5,
0.5,
1.0,
1.0,
{+1, +1},
{-1, -1},
{+1, -1},
{-1, +1},
}
type RoomBattleState struct {
@@ -127,11 +110,13 @@ func calRoomScore(inRoomPlayerCount int32, roomPlayerCnt int, currentRoomBattleS
}
type Room struct {
Id int32
Capacity int
Players map[int32]*Player
PlayersArr []*Player // ordered by joinIndex
CollisionSysMap map[int32]*resolv.Object
Id int32
Capacity int
collisionSpaceOffsetX float64
collisionSpaceOffsetY float64
Players map[int32]*Player
PlayersArr []*Player // ordered by joinIndex
CollisionSysMap map[int32]*resolv.Object
/**
* The following `PlayerDownsyncSessionDict` is NOT individually put
* under `type Player struct` for a reason.
@@ -158,6 +143,7 @@ type Room struct {
RenderFrameId int32
CurDynamicsRenderFrameId int32 // [WARNING] The dynamics of backend is ALWAYS MOVING FORWARD BY ALL-CONFIRMED INPUTFRAMES (either by upsync or forced), i.e. no rollback
ServerFps int32
BattleDurationFrames int32
BattleDurationNanos int64
InputFrameUpsyncDelayTolerance int32
MaxChasingRenderFramesPerUpdate int32
@@ -174,7 +160,14 @@ type Room struct {
NstDelayFrames int32 // network-single-trip delay in the count of render frames, proposed to be (InputDelayFrames >> 1) because we expect a round-trip delay to be exactly "InputDelayFrames"
InputScaleFrames uint32 // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
JoinIndexBooleanArr []bool
RollbackEstimatedDt float64
RollbackEstimatedDtMillis float64
RollbackEstimatedDtNanos int64
LastRenderFrameIdTriggeredAt int64
WorldToVirtualGridRatio float64
VirtualGridToWorldRatio float64
PlayerDefaultSpeed int32
StageName string
StageDiscreteW int32
@@ -183,13 +176,9 @@ type Room struct {
StageTileH int32
RawBattleStrToVec2DListMap StrToVec2DListMap
RawBattleStrToPolygon2DListMap StrToPolygon2DListMap
BackendDynamicsEnabled bool
}
const (
PLAYER_DEFAULT_SPEED = float64(200) // Hardcoded
ADD_SPEED = float64(100) // Hardcoded
)
func (pR *Room) updateScore() {
pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State)
}
@@ -211,9 +200,8 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.FrozenAtGmtMillis = -1 // Hardcoded temporarily.
pPlayerFromDbInit.Speed = PLAYER_DEFAULT_SPEED // Hardcoded temporarily.
pPlayerFromDbInit.AddSpeedAtGmtMillis = -1 // Hardcoded temporarily.
pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded
pPlayerFromDbInit.ColliderRadius = float64(24) // Hardcoded
pR.Players[playerId] = pPlayerFromDbInit
pR.PlayerDownsyncSessionDict[playerId] = session
@@ -245,6 +233,8 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
pEffectiveInRoomPlayerInstance.Speed = pR.PlayerDefaultSpeed // Hardcoded
pEffectiveInRoomPlayerInstance.ColliderRadius = float64(16) // Hardcoded
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
@@ -257,10 +247,12 @@ func (pR *Room) ChooseStage() error {
* -- YFLu, 2019-09-04
*/
pwd, err := os.Getwd()
ErrFatal(err)
if nil != err {
panic(err)
}
rand.Seed(time.Now().Unix())
stageNameList := []string{ /*"pacman" ,*/ "richsoil"}
stageNameList := []string{"simple" /* "richsoil" */}
chosenStageIndex := rand.Int() % len(stageNameList) // Hardcoded temporarily. -- YFLu
pR.StageName = stageNameList[chosenStageIndex]
@@ -320,7 +312,8 @@ func (pR *Room) ChooseStage() error {
barrierPolygon2DList := *(toRetStrToPolygon2DListMap["Barrier"])
var barrierLocalIdInBattle int32 = 0
for _, polygon2D := range barrierPolygon2DList {
for _, polygon2DUnaligned := range barrierPolygon2DList.Eles {
polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned)
/*
// For debug-printing only.
Logger.Info("ChooseStage printing polygon2D for barrierPolygon2DList", zap.Any("barrierLocalIdInBattle", barrierLocalIdInBattle), zap.Any("polygon2D.Anchor", polygon2D.Anchor), zap.Any("polygon2D.Points", polygon2D.Points))
@@ -352,7 +345,7 @@ func (pR *Room) ConvertToLastUsedRenderFrameId(inputFrameId int32, inputDelayFra
return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames + (1 << pR.InputScaleFrames) - 1)
}
func (pR *Room) EncodeUpsyncCmd(upsyncCmd *pb.InputFrameUpsync) uint64 {
func (pR *Room) EncodeUpsyncCmd(upsyncCmd *InputFrameUpsync) uint64 {
var ret uint64 = 0
// There're 13 possible directions, occupying the first 4 bits, no need to shift
ret += uint64(upsyncCmd.EncodedDir)
@@ -376,7 +369,7 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
if nil == tmp {
break
}
f := tmp.(*pb.InputFrameDownsync)
f := tmp.(*InputFrameDownsync)
s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList))
}
@@ -398,7 +391,7 @@ func (pR *Room) StartBattle() {
// Initialize the "collisionSys" as well as "RenderFrameBuffer"
pR.CurDynamicsRenderFrameId = 0
kickoffFrame := &pb.RoomDownsyncFrame{
kickoffFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players),
CountdownNanos: pR.BattleDurationNanos,
@@ -406,7 +399,11 @@ func (pR *Room) StartBattle() {
pR.RenderFrameBuffer.Put(kickoffFrame)
// Refresh "Colliders"
pR.refreshColliders()
spaceW := pR.StageDiscreteW * pR.StageTileW
spaceH := pR.StageDiscreteH * pR.StageTileH
pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY = float64(spaceW)*0.5, float64(spaceH)*0.5
pR.refreshColliders(spaceW, spaceH)
/**
* Will be triggered from a goroutine which executes the critical `Room.AddPlayerIfPossible`, thus the `battleMainLoop` should be detached.
@@ -422,16 +419,22 @@ func (pR *Room) StartBattle() {
pR.onBattleStoppedForSettlement()
}()
battleMainLoopStartedNanos := utils.UnixtimeNano()
totalElapsedNanos := int64(0)
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
Logger.Info("The `battleMainLoop` is started for:", zap.Any("roomId", pR.Id))
for {
stCalculation := utils.UnixtimeNano()
if totalElapsedNanos > pR.BattleDurationNanos {
Logger.Info(fmt.Sprintf("The `battleMainLoop` for roomId=%v is stopped:\n%v", pR.Id, pR.InputsBufferString(true)))
elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt
if elapsedNanosSinceLastFrameIdTriggered < pR.RollbackEstimatedDtNanos {
Logger.Debug(fmt.Sprintf("Avoiding too fast frame@roomId=%v, renderFrameId=%v: elapsedNanosSinceLastFrameIdTriggered=%v", pR.Id, pR.RenderFrameId, elapsedNanosSinceLastFrameIdTriggered))
continue
}
if pR.RenderFrameId > pR.BattleDurationFrames {
Logger.Info(fmt.Sprintf("The `battleMainLoop` for roomId=%v is stopped@renderFrameId=%v, with battleDurationFrames=%v:\n%v", pR.Id, pR.RenderFrameId, pR.BattleDurationFrames, pR.InputsBufferString(true)))
pR.StopBattleForSettlement()
return
}
if swapped := atomic.CompareAndSwapInt32(&pR.State, RoomBattleStateIns.IN_BATTLE, RoomBattleStateIns.IN_BATTLE); !swapped {
@@ -444,17 +447,11 @@ func (pR *Room) StartBattle() {
pR.prefabInputFrameDownsync(noDelayInputFrameId)
}
// Force setting all-confirmed of buffered inputFrames periodically
unconfirmedMask := pR.forceConfirmationIfApplicable()
dynamicsDuration := int64(0)
if 0 <= pR.LastAllConfirmedInputFrameId {
dynamicsStartedAt := utils.UnixtimeNano()
// Apply "all-confirmed inputFrames" to move forward "pR.CurDynamicsRenderFrameId"
nextDynamicsRenderFrameId := pR.ConvertToLastUsedRenderFrameId(pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames)
Logger.Debug(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, LastAllConfirmedInputFrameId=%v, InputDelayFrames=%v, nextDynamicsRenderFrameId=%v", pR.Id, pR.RenderFrameId, pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames, nextDynamicsRenderFrameId))
pR.applyInputFrameDownsyncDynamics(pR.CurDynamicsRenderFrameId, nextDynamicsRenderFrameId)
dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
pR.markConfirmationIfApplicable()
unconfirmedMask := uint64(0)
if pR.BackendDynamicsEnabled {
// Force setting all-confirmed of buffered inputFrames periodically
unconfirmedMask = pR.forceConfirmationIfApplicable()
}
upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId))
@@ -464,31 +461,45 @@ func (pR *Room) StartBattle() {
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more.
Hence even upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId".
Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed.
*/
refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1
// [WARNING] The following inequalities are seldom true, but just to avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId".
if refRenderFrameId > pR.RenderFrameId {
refRenderFrameId = pR.RenderFrameId
}
if refRenderFrameId > pR.CurDynamicsRenderFrameId {
refRenderFrameId = pR.CurDynamicsRenderFrameId
dynamicsDuration := int64(0)
if pR.BackendDynamicsEnabled {
if 0 <= pR.LastAllConfirmedInputFrameId {
dynamicsStartedAt := utils.UnixtimeNano()
// Apply "all-confirmed inputFrames" to move forward "pR.CurDynamicsRenderFrameId"
nextDynamicsRenderFrameId := pR.ConvertToLastUsedRenderFrameId(pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames)
Logger.Debug(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, LastAllConfirmedInputFrameId=%v, InputDelayFrames=%v, nextDynamicsRenderFrameId=%v", pR.Id, pR.RenderFrameId, pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames, nextDynamicsRenderFrameId))
pR.applyInputFrameDownsyncDynamics(pR.CurDynamicsRenderFrameId, nextDynamicsRenderFrameId, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY)
dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
}
// [WARNING] The following inequality are seldom true, but just to avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId".
if refRenderFrameId > pR.CurDynamicsRenderFrameId {
refRenderFrameId = pR.CurDynamicsRenderFrameId
}
}
for playerId, player := range pR.Players {
if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped {
// [WARNING] DON'T send anything if the player is disconnected, because it could jam the channel and cause significant delay upon "battle recovery for reconnected player".
continue
}
if 0 == pR.RenderFrameId {
kickoffFrame := pR.RenderFrameBuffer.GetByFrameId(0).(*pb.RoomDownsyncFrame)
kickoffFrame := pR.RenderFrameBuffer.GetByFrameId(0).(*RoomDownsyncFrame)
pR.sendSafely(kickoffFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId)
} else {
// [WARNING] Websocket is TCP-based, thus no need to re-send a previously sent inputFrame to a same player!
toSendInputFrames := make([]*pb.InputFrameDownsync, 0, pR.InputsBuffer.Cnt)
toSendInputFrames := make([]*InputFrameDownsync, 0, pR.InputsBuffer.Cnt)
candidateToSendInputFrameId := pR.Players[playerId].LastSentInputFrameId + 1
if candidateToSendInputFrameId < pR.InputsBuffer.StFrameId {
// [WARNING] As "player.LastSentInputFrameId <= lastAllConfirmedInputFrameIdWithChange" for each iteration, and "lastAllConfirmedInputFrameIdWithChange <= lastAllConfirmedInputFrameId" where the latter is used to "applyInputFrameDownsyncDynamics" and then evict "pR.InputsBuffer", thus there's a very high possibility that "player.LastSentInputFrameId" is already evicted.
// Logger.Debug(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false)))
Logger.Warn(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false)))
candidateToSendInputFrameId = pR.InputsBuffer.StFrameId
}
@@ -504,7 +515,7 @@ func (pR *Room) StartBattle() {
if nil == tmp {
panic(fmt.Sprintf("Required inputFrameId=%v for roomId=%v, playerId=%v doesn't exist! InputsBuffer=%v", candidateToSendInputFrameId, pR.Id, playerId, pR.InputsBufferString(false)))
}
f := tmp.(*pb.InputFrameDownsync)
f := tmp.(*InputFrameDownsync)
if pR.inputFrameIdDebuggable(candidateToSendInputFrameId) {
Logger.Debug("inputFrame lifecycle#3[sending]:", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("playerAckingInputFrameId", player.AckingInputFrameId), zap.Any("inputFrameId", candidateToSendInputFrameId), zap.Any("inputFrameId-doublecheck", f.InputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)), zap.Any("ConfirmedList", f.ConfirmedList))
}
@@ -514,22 +525,26 @@ func (pR *Room) StartBattle() {
if 0 >= len(toSendInputFrames) {
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"!
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId {
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, candidateToSendInputFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, candidateToSendInputFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
}
continue
}
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr)
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || 0 < (unconfirmedMask&joinMask) {
// [WARNING] Even upon "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED", it could be true that "0 == (unconfirmedMask & joinMask)"!
/*
Resync helps
1. when player with a slower frontend clock lags significantly behind and thus wouldn't get its inputUpsync recognized due to faster "forceConfirmation"
2. reconnection
*/
shouldResync1 := (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId)
// shouldResync2 := (0 < (unconfirmedMask & uint64(1 << uint32(player.JoinIndex-1)))) // This condition is critical, if we don't send resync upon this condition, the "reconnected or slowly-clocking player" might never get its input synced
shouldResync2 := (0 < unconfirmedMask) // An easier version of the above, might keep sending "refRenderFrame"s to still connected players when any player is disconnected
if pR.BackendDynamicsEnabled && (shouldResync1 || shouldResync2) {
tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId)
if nil == tmp {
panic(fmt.Sprintf("Required refRenderFrameId=%v for roomId=%v, playerId=%v, candidateToSendInputFrameId=%v doesn't exist! InputsBuffer=%v, RenderFrameBuffer=%v", refRenderFrameId, pR.Id, playerId, candidateToSendInputFrameId, pR.InputsBufferString(false), pR.RenderFrameBufferString()))
}
refRenderFrame := tmp.(*pb.RoomDownsyncFrame)
refRenderFrame := tmp.(*RoomDownsyncFrame)
pR.sendSafely(refRenderFrame, toSendInputFrames, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId)
} else {
pR.sendSafely(nil, toSendInputFrames, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId)
@@ -538,25 +553,40 @@ func (pR *Room) StartBattle() {
}
}
// Evict no longer required "RenderFrameBuffer"
for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) {
_ = pR.RenderFrameBuffer.Pop()
if pR.BackendDynamicsEnabled {
// Evict no longer required "RenderFrameBuffer"
for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) {
_ = pR.RenderFrameBuffer.Pop()
}
}
toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames)
/*
[WARNING]
The following updates to "toApplyInputFrameId" is necessary because when "false == pR.BackendDynamicsEnabled", the variable "refRenderFrameId" is not well defined.
*/
minLastSentInputFrameId := int32(math.MaxInt32)
for _, player := range pR.Players {
if player.LastSentInputFrameId >= minLastSentInputFrameId {
continue
}
minLastSentInputFrameId = player.LastSentInputFrameId
}
if minLastSentInputFrameId < toApplyInputFrameId {
toApplyInputFrameId = minLastSentInputFrameId
}
for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) {
f := pR.InputsBuffer.Pop().(*pb.InputFrameDownsync)
f := pR.InputsBuffer.Pop().(*InputFrameDownsync)
if pR.inputFrameIdDebuggable(f.InputFrameId) {
// Popping of an "inputFrame" would be AFTER its being all being confirmed, because it requires the "inputFrame" to be all acked
Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)))
Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("toApplyInputFrameId", toApplyInputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)))
}
}
pR.RenderFrameId++
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
totalElapsedNanos = (utils.UnixtimeNano() - battleMainLoopStartedNanos)
if elapsedInCalculation > nanosPerFrame {
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v, dynamicsDuration=%v, nanosPerFrame=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, nanosPerFrame))
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, expected nanosPerFrame=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, nanosPerFrame))
}
time.Sleep(time.Duration(nanosPerFrame - elapsedInCalculation))
}
@@ -572,7 +602,7 @@ func (pR *Room) toDiscreteInputsBufferIndex(inputFrameId int32, joinIndex int32)
return (inputFrameId << 2) + joinIndex // allowing joinIndex upto 15
}
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
func (pR *Room) OnBattleCmdReceived(pReq *WsReq) {
if swapped := atomic.CompareAndSwapInt32(&pR.State, RoomBattleStateIns.IN_BATTLE, RoomBattleStateIns.IN_BATTLE); !swapped {
return
}
@@ -610,13 +640,13 @@ func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
}
}
func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *pb.InputFrameDownsync, playerId int32) {
func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *InputFrameDownsync, playerId int32) {
inputFrameId := inputFrameDownsync.InputFrameId
if -1 == pR.LastAllConfirmedInputFrameIdWithChange || false == pR.equalInputLists(inputFrameDownsync.InputList, pR.LastAllConfirmedInputList) {
if -1 == playerId {
Logger.Info(fmt.Sprintf("Key inputFrame change: roomId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, InputsBuffer=%v", pR.Id, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("Key inputFrame change: roomId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, InputsBuffer=%v", pR.Id, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.InputsBufferString(false)))
} else {
Logger.Info(fmt.Sprintf("Key inputFrame change: roomId=%v, playerId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, InputsBuffer=%v", pR.Id, playerId, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("Key inputFrame change: roomId=%v, playerId=%v, newInputFrameId=%v, lastInputFrameId=%v, newInputList=%v, lastInputList=%v, InputsBuffer=%v", pR.Id, playerId, inputFrameId, pR.LastAllConfirmedInputFrameId, inputFrameDownsync.InputList, pR.LastAllConfirmedInputList, pR.InputsBufferString(false)))
}
atomic.StoreInt32(&(pR.LastAllConfirmedInputFrameIdWithChange), inputFrameId)
}
@@ -628,7 +658,7 @@ func (pR *Room) onInputFrameDownsyncAllConfirmed(inputFrameDownsync *pb.InputFra
if -1 == playerId {
Logger.Debug(fmt.Sprintf("inputFrame lifecycle#2[forced-allconfirmed]: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false)))
} else {
Logger.Info(fmt.Sprintf("inputFrame lifecycle#2[allconfirmed]: roomId=%v, playerId=%v, InputsBuffer=%v", pR.Id, playerId, pR.InputsBufferString(false)))
Logger.Debug(fmt.Sprintf("inputFrame lifecycle#2[allconfirmed]: roomId=%v, playerId=%v, InputsBuffer=%v", pR.Id, playerId, pR.InputsBufferString(false)))
}
}
@@ -652,7 +682,7 @@ func (pR *Room) StopBattleForSettlement() {
Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id))
pR.RenderFrameId++
for playerId, _ := range pR.Players {
assembledFrame := pb.RoomDownsyncFrame{
assembledFrame := RoomDownsyncFrame{
Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players),
CountdownNanos: -1, // TODO: Replace this magic constant!
@@ -678,18 +708,19 @@ func (pR *Room) onBattlePrepare(cb BattleStartCbType) {
pR.State = RoomBattleStateIns.PREPARE
Logger.Info("Battle state transitted to RoomBattleStateIns.PREPARE for:", zap.Any("roomId", pR.Id))
playerMetas := make(map[int32]*pb.PlayerMeta, 0)
playerMetas := make(map[int32]*PlayerDownsyncMeta, 0)
for _, player := range pR.Players {
playerMetas[player.Id] = &pb.PlayerMeta{
Id: player.Id,
Name: player.Name,
DisplayName: player.DisplayName,
Avatar: player.Avatar,
JoinIndex: player.JoinIndex,
playerMetas[player.Id] = &PlayerDownsyncMeta{
Id: player.Id,
Name: player.Name,
DisplayName: player.DisplayName,
Avatar: player.Avatar,
ColliderRadius: player.ColliderRadius, // hardcoded for now
JoinIndex: player.JoinIndex,
}
}
battleReadyToStartFrame := &pb.RoomDownsyncFrame{
battleReadyToStartFrame := &RoomDownsyncFrame{
Id: DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START,
Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas,
@@ -759,6 +790,9 @@ func (pR *Room) Dismiss() {
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.
pR.WorldToVirtualGridRatio = float64(1000)
pR.VirtualGridToWorldRatio = float64(1.0) / pR.WorldToVirtualGridRatio // this is a one-off computation, should avoid division in iterations
pR.PlayerDefaultSpeed = int32(float64(2) * pR.WorldToVirtualGridRatio) // in virtual grids per frame
pR.Players = make(map[int32]*Player)
pR.PlayersArr = make([]*Player, pR.Capacity)
pR.CollisionSysMap = make(map[int32]*resolv.Object)
@@ -780,10 +814,14 @@ func (pR *Room) OnDismissed() {
pR.NstDelayFrames = 8
pR.InputScaleFrames = uint32(2)
pR.ServerFps = 60
pR.RollbackEstimatedDt = float64(1.0) / float64(pR.ServerFps)
pR.BattleDurationNanos = int64(30 * 1000 * 1000 * 1000)
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 preventing FAST FRAME
pR.BattleDurationFrames = 30 * pR.ServerFps
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
pR.InputFrameUpsyncDelayTolerance = 2
pR.MaxChasingRenderFramesPerUpdate = 10
pR.MaxChasingRenderFramesPerUpdate = 5
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
pR.ChooseStage()
pR.EffectivePlayerCount = 0
@@ -894,16 +932,15 @@ func (pR *Room) onPlayerAdded(playerId int32) {
// Lazily assign the initial position of "Player" for "RoomDownsyncFrame".
playerPosList := *(pR.RawBattleStrToVec2DListMap["PlayerStartingPos"])
if index > len(playerPosList) {
if index > len(playerPosList.Eles) {
panic(fmt.Sprintf("onPlayerAdded error, index >= len(playerPosList), roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
playerPos := playerPosList[index]
playerPos := playerPosList.Eles[index]
if nil == playerPos {
panic(fmt.Sprintf("onPlayerAdded error, nil == playerPos, roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
}
pR.Players[playerId].X = playerPos.X
pR.Players[playerId].Y = playerPos.Y
pR.Players[playerId].VirtualGridX, pR.Players[playerId].VirtualGridY = WorldToVirtualGridPos(playerPos.X, playerPos.Y, pR.WorldToVirtualGridRatio)
break
}
@@ -931,37 +968,38 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
return false
}
playerMetas := make(map[int32]*pb.PlayerMeta, 0)
playerMetas := make(map[int32]*PlayerDownsyncMeta, 0)
for _, eachPlayer := range pR.Players {
playerMetas[eachPlayer.Id] = &pb.PlayerMeta{
Id: eachPlayer.Id,
Name: eachPlayer.Name,
DisplayName: eachPlayer.DisplayName,
Avatar: eachPlayer.Avatar,
JoinIndex: eachPlayer.JoinIndex,
playerMetas[eachPlayer.Id] = &PlayerDownsyncMeta{
Id: eachPlayer.Id,
Name: eachPlayer.Name,
DisplayName: eachPlayer.DisplayName,
Avatar: eachPlayer.Avatar,
JoinIndex: eachPlayer.JoinIndex,
ColliderRadius: eachPlayer.ColliderRadius,
}
}
// Broadcast added or readded player info to all players in the same room
for _, eachPlayer := range pR.Players {
/*
[WARNING]
This `playerAckedFrame` is the first ever "RoomDownsyncFrame" for every "PersistentSessionClient on the frontend", and it goes right after each "BattleColliderInfo".
[WARNING]
This `playerAckedFrame` is the first ever "RoomDownsyncFrame" for every "PersistentSessionClient on the frontend", and it goes right after each "BattleColliderInfo".
By making use of the sequential nature of each ws session, all later "RoomDownsyncFrame"s generated after `pRoom.StartBattle()` will be put behind this `playerAckedFrame`.
By making use of the sequential nature of each ws session, all later "RoomDownsyncFrame"s generated after `pRoom.StartBattle()` will be put behind this `playerAckedFrame`.
This function is triggered by an upsync message via WebSocket, thus downsync sending is also available by now.
This function is triggered by an upsync message via WebSocket, thus downsync sending is also available by now.
*/
switch targetPlayer.BattleState {
case PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK:
playerAckedFrame := &pb.RoomDownsyncFrame{
playerAckedFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas,
}
pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, eachPlayer.Id)
case PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
playerAckedFrame := &pb.RoomDownsyncFrame{
playerAckedFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas,
@@ -992,14 +1030,14 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
return true
}
func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendFrames []*pb.InputFrameDownsync, act int32, playerId int32) {
func (pR *Room) sendSafely(roomDownsyncFrame *RoomDownsyncFrame, toSendFrames []*InputFrameDownsync, act int32, playerId int32) {
defer func() {
if r := recover(); r != nil {
pR.PlayerSignalToCloseDict[playerId](Constants.RetCode.UnknownError, fmt.Sprintf("%v", r))
}
}()
pResp := &pb.WsResp{
pResp := &WsResp{
Ret: int32(Constants.RetCode.Ok),
Act: act,
Rdf: roomDownsyncFrame,
@@ -1020,16 +1058,16 @@ func (pR *Room) shouldPrefabInputFrameDownsync(renderFrameId int32) bool {
return ((renderFrameId & ((1 << pR.InputScaleFrames) - 1)) == 0)
}
func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDownsync {
func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *InputFrameDownsync {
/*
Kindly note that on backend the prefab is much simpler than its frontend counterpart, because frontend will upsync its latest command immediately if there's any change w.r.t. its own prev cmd, thus if no upsync received from a frontend,
- EITHER it's due to local lag and bad network,
- OR there's no change w.r.t. to its prev cmd.
*/
var currInputFrameDownsync *pb.InputFrameDownsync = nil
var currInputFrameDownsync *InputFrameDownsync = nil
if 0 == inputFrameId && 0 == pR.InputsBuffer.Cnt {
currInputFrameDownsync = &pb.InputFrameDownsync{
currInputFrameDownsync = &InputFrameDownsync{
InputFrameId: 0,
InputList: make([]uint64, pR.Capacity),
ConfirmedList: uint64(0),
@@ -1039,9 +1077,9 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDowns
if nil == tmp {
panic(fmt.Sprintf("Error prefabbing inputFrameDownsync: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false)))
}
prevInputFrameDownsync := tmp.(*pb.InputFrameDownsync)
prevInputFrameDownsync := tmp.(*InputFrameDownsync)
currInputList := prevInputFrameDownsync.InputList // Would be a clone of the values
currInputFrameDownsync = &pb.InputFrameDownsync{
currInputFrameDownsync = &InputFrameDownsync{
InputFrameId: inputFrameId,
InputList: currInputList,
ConfirmedList: uint64(0),
@@ -1052,6 +1090,42 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDowns
return currInputFrameDownsync
}
func (pR *Room) markConfirmationIfApplicable() {
inputFrameId1 := pR.LastAllConfirmedInputFrameId + 1
gap := int32(4) // This value is hardcoded and doesn't need be much bigger, because the backend side is supposed to never lag when "false == BackendDynamicsEnabled".
inputFrameId2 := inputFrameId1 + gap
if inputFrameId2 > pR.InputsBuffer.EdFrameId {
inputFrameId2 = pR.InputsBuffer.EdFrameId
}
totPlayerCnt := uint32(pR.Capacity)
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
for inputFrameId := inputFrameId1; inputFrameId < inputFrameId2; inputFrameId++ {
tmp := pR.InputsBuffer.GetByFrameId(inputFrameId)
if nil == tmp {
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", inputFrameId, pR.Id, pR.InputsBufferString(false)))
}
inputFrameDownsync := tmp.(*InputFrameDownsync)
for _, player := range pR.Players {
bufIndex := pR.toDiscreteInputsBufferIndex(inputFrameId, player.JoinIndex)
tmp, loaded := pR.DiscreteInputsBuffer.LoadAndDelete(bufIndex) // It's safe to "LoadAndDelete" here because the "inputFrameUpsync" of this player is already remembered by the corresponding "inputFrameDown".
if !loaded {
continue
}
inputFrameUpsync := tmp.(*InputFrameUpsync)
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr] = pR.EncodeUpsyncCmd(inputFrameUpsync)
inputFrameDownsync.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr)
}
if allConfirmedMask == inputFrameDownsync.ConfirmedList {
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
} else {
break
}
}
}
func (pR *Room) forceConfirmationIfApplicable() uint64 {
// Force confirmation of non-all-confirmed inputFrame EXACTLY ONE AT A TIME, returns the non-confirmed mask of players, e.g. in a 4-player-battle returning 1001 means that players with JoinIndex=1 and JoinIndex=4 are non-confirmed for inputFrameId2
renderFrameId1 := (pR.RenderFrameId - pR.NstDelayFrames) // the renderFrameId which should've been rendered on frontend
@@ -1074,24 +1148,12 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
if nil == tmp {
panic(fmt.Sprintf("inputFrameId2=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", inputFrameId2, pR.Id, pR.InputsBufferString(false)))
}
inputFrame2 := tmp.(*pb.InputFrameDownsync)
for _, player := range pR.Players {
// Enrich by already arrived player upsync commands
bufIndex := pR.toDiscreteInputsBufferIndex(inputFrame2.InputFrameId, player.JoinIndex)
tmp, loaded := pR.DiscreteInputsBuffer.LoadAndDelete(bufIndex)
if !loaded {
continue
}
inputFrameUpsync := tmp.(*pb.InputFrameUpsync)
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
inputFrame2.InputList[indiceInJoinIndexBooleanArr] = pR.EncodeUpsyncCmd(inputFrameUpsync)
inputFrame2.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr)
}
totPlayerCnt := uint32(pR.Capacity)
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
// Force confirmation of "inputFrame2"
inputFrame2 := tmp.(*InputFrameDownsync)
oldConfirmedList := inputFrame2.ConfirmedList
unconfirmedMask := (oldConfirmedList ^ allConfirmedMask)
inputFrame2.ConfirmedList = allConfirmedMask
@@ -1100,7 +1162,7 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
return unconfirmedMask
}
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32) {
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32, spaceOffsetX, spaceOffsetY float64) {
if fromRenderFrameId >= toRenderFrameId {
return
}
@@ -1110,7 +1172,13 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
for collisionSysRenderFrameId := fromRenderFrameId; collisionSysRenderFrameId < toRenderFrameId; collisionSysRenderFrameId++ {
currRenderFrameTmp := pR.RenderFrameBuffer.GetByFrameId(collisionSysRenderFrameId)
if nil == currRenderFrameTmp {
panic(fmt.Sprintf("collisionSysRenderFrameId=%v doesn't exist for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v)! RenderFrameBuffer=%v", collisionSysRenderFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, pR.RenderFrameBufferString()))
}
currRenderFrame := currRenderFrameTmp.(*RoomDownsyncFrame)
delayedInputFrameId := pR.ConvertToInputFrameId(collisionSysRenderFrameId, pR.InputDelayFrames)
var delayedInputFrame *InputFrameDownsync = nil
if 0 <= delayedInputFrameId {
if delayedInputFrameId > pR.LastAllConfirmedInputFrameId {
panic(fmt.Sprintf("delayedInputFrameId=%v is not yet all-confirmed for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! InputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.InputsBufferString(false)))
@@ -1119,64 +1187,126 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
if nil == tmp {
panic(fmt.Sprintf("delayedInputFrameId=%v doesn't exist for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! InputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.InputsBufferString(false)))
}
delayedInputFrame := tmp.(*pb.InputFrameDownsync)
delayedInputFrame = tmp.(*InputFrameDownsync)
// [WARNING] It's possible that by now "allConfirmedMask != delayedInputFrame.ConfirmedList && delayedInputFrameId <= pR.LastAllConfirmedInputFrameId", we trust "pR.LastAllConfirmedInputFrameId" as the TOP AUTHORITY.
atomic.StoreUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask)
}
inputList := delayedInputFrame.InputList
// Ordered by joinIndex to guarantee determinism
for _, player := range pR.PlayersArr {
joinIndex := player.JoinIndex
encodedInput := inputList[joinIndex-1]
decodedInput := DIRECTION_DECODER[encodedInput]
decodedInputSpeedFactor := DIRECTION_DECODER_INVERSE_LENGTH[encodedInput]
baseChange := player.Speed * pR.RollbackEstimatedDt * decodedInputSpeedFactor
dx := baseChange * float64(decodedInput[0])
dy := baseChange * float64(decodedInput[1])
nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap)
// Update in the latest player pointers
for playerId, playerDownsync := range nextRenderFrame.Players {
pR.Players[playerId].VirtualGridX = playerDownsync.VirtualGridX
pR.Players[playerId].VirtualGridY = playerDownsync.VirtualGridY
pR.Players[playerId].Dir.Dx = playerDownsync.Dir.Dx
pR.Players[playerId].Dir.Dy = playerDownsync.Dir.Dy
}
pR.RenderFrameBuffer.Put(nextRenderFrame)
pR.CurDynamicsRenderFrameId++
}
}
/*
// The collision lib seems very slow at worst cases, omitting for now
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := pR.CollisionSysMap[collisionPlayerIndex]
if collision := playerCollider.Check(dx, dy, "Barrier"); collision != nil {
changeWithCollision := collision.ContactWithObject(collision.Objects[0])
dx = changeWithCollision.X()
dy = changeWithCollision.Y()
}
playerCollider.X += dx
playerCollider.Y += dy
// Update in "collision space"
playerCollider.Update()
*/
// TODO: Write unit-test for this function to compare with its frontend counter part
func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame {
nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity)
// Make a copy first
for playerId, currPlayerDownsync := range currRenderFrame.Players {
nextRenderFramePlayers[playerId] = &PlayerDownsync{
Id: playerId,
VirtualGridX: currPlayerDownsync.VirtualGridX,
VirtualGridY: currPlayerDownsync.VirtualGridY,
Dir: &Direction{
Dx: currPlayerDownsync.Dir.Dx,
Dy: currPlayerDownsync.Dir.Dy,
},
Speed: currPlayerDownsync.Speed,
BattleState: currPlayerDownsync.BattleState,
Score: currPlayerDownsync.Score,
Removed: currPlayerDownsync.Removed,
JoinIndex: currPlayerDownsync.JoinIndex,
}
}
player.Dir.Dx = decodedInput[0]
player.Dir.Dy = decodedInput[1]
player.X += dx
player.Y += dy
toRet := &RoomDownsyncFrame{
Id: currRenderFrame.Id + 1,
Players: nextRenderFramePlayers,
CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos),
}
if nil != delayedInputFrame {
inputList := delayedInputFrame.InputList
effPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order
for playerId, player := range pR.Players {
joinIndex := player.JoinIndex
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
currPlayerDownsync := currRenderFrame.Players[playerId]
encodedInput := inputList[joinIndex-1]
decodedInput := DIRECTION_DECODER[encodedInput]
proposedVirtualGridDx, proposedVirtualGridDy := (decodedInput[0] + decodedInput[0]*currPlayerDownsync.Speed), (decodedInput[1] + decodedInput[1]*currPlayerDownsync.Speed)
newVx, newVy := (currPlayerDownsync.VirtualGridX + proposedVirtualGridDx), (currPlayerDownsync.VirtualGridY + proposedVirtualGridDy)
// Reset playerCollider position from the "virtual grid position"
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio)
// Update in the collision system
playerCollider.Update()
if 0 < encodedInput {
Logger.Debug(fmt.Sprintf("Checking collision for playerId=%v: virtual (%d, %d) -> (%d, %d), now playerShape=%v", playerId, currPlayerDownsync.VirtualGridX, currPlayerDownsync.VirtualGridY, newVx, newVy, ConvexPolygonStr(playerCollider.Shape.(*resolv.ConvexPolygon))))
nextRenderFramePlayers[playerId].Dir.Dx = decodedInput[0]
nextRenderFramePlayers[playerId].Dir.Dy = decodedInput[1]
}
}
newRenderFrame := pb.RoomDownsyncFrame{
Id: collisionSysRenderFrameId + 1,
Players: toPbPlayers(pR.Players),
CountdownNanos: (pR.BattleDurationNanos - int64(collisionSysRenderFrameId)*int64(pR.RollbackEstimatedDt*1000000000)), // TODO: Make this number more synced with frontend!
// handle pushbacks upon collision after all movements treated as simultaneous
for _, player := range pR.Players {
joinIndex := player.JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
if collision := playerCollider.Check(0, 0); collision != nil {
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
for _, obj := range collision.Objects {
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape); overlapped {
Logger.Debug(fmt.Sprintf("Overlapped: a=%v, b=%v, pushbackX=%v, pushbackY=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY))
effPushbacks[joinIndex-1].X += pushbackX
effPushbacks[joinIndex-1].Y += pushbackY
} else {
Logger.Debug(fmt.Sprintf("Collided BUT not overlapped: a=%v, b=%v, overlapResult=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), overlapResult))
}
}
}
}
pR.RenderFrameBuffer.Put(&newRenderFrame)
pR.CurDynamicsRenderFrameId++
for playerId, player := range pR.Players {
joinIndex := player.JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
// Update "virtual grid position"
newVx, newVy := PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio)
nextRenderFramePlayers[playerId].VirtualGridX = newVx
nextRenderFramePlayers[playerId].VirtualGridY = newVy
}
Logger.Debug(fmt.Sprintf("After applyInputFrameDownsyncDynamicsOnSingleRenderFrame: currRenderFrame.Id=%v, inputList=%v, currRenderFrame.Players=%v, nextRenderFramePlayers=%v, toRet.Players=%v", currRenderFrame.Id, inputList, currRenderFrame.Players, nextRenderFramePlayers, toRet.Players))
}
return toRet
}
func (pR *Room) inputFrameIdDebuggable(inputFrameId int32) bool {
return 0 == (inputFrameId % 10)
}
func (pR *Room) refreshColliders() {
func (pR *Room) refreshColliders(spaceW, spaceH int32) {
// Kindly note that by now, we've already got all the shapes in the tmx file into "pR.(Players | Barriers)" from "ParseTmxLayersAndGroups"
space := resolv.NewSpace(int(pR.StageDiscreteW), int(pR.StageDiscreteH), int(pR.StageTileW), int(pR.StageTileH)) // allocate a new collision space everytime after a battle is settled
minStep := (int(float64(pR.PlayerDefaultSpeed)*pR.VirtualGridToWorldRatio) << 2) // the approx minimum distance a player can move per frame in world coordinate
space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep) // allocate a new collision space everytime after a battle is settled
for _, player := range pR.Players {
playerCollider := resolv.NewObject(player.X, player.Y, 12, 12) // Radius=12 is hardcoded
playerColliderShape := resolv.NewCircle(player.X, player.Y, 12)
playerCollider.SetShape(playerColliderShape)
wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio)
playerCollider := GenerateRectCollider(wx, wy, player.ColliderRadius*2, player.ColliderRadius*2, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player")
space.Add(playerCollider)
// Keep track of the collider in "pR.CollisionSysMap"
joinIndex := player.JoinIndex
@@ -1186,29 +1316,12 @@ func (pR *Room) refreshColliders() {
}
for _, barrier := range pR.Barriers {
var w float64 = 0
var h float64 = 0
for i, pi := range barrier.Boundary.Points {
for j, pj := range barrier.Boundary.Points {
if i == j {
continue
}
if math.Abs(pj.X-pi.X) > w {
w = math.Abs(pj.X - pi.X)
}
if math.Abs(pj.Y-pi.Y) > h {
h = math.Abs(pj.Y - pi.Y)
}
}
}
barrierColliderShape := resolv.NewConvexPolygon()
for _, p := range barrier.Boundary.Points {
barrierColliderShape.AddPoints(p.X+barrier.Boundary.Anchor.X, p.Y+barrier.Boundary.Anchor.Y)
}
barrierCollider := resolv.NewObject(barrier.Boundary.Anchor.X, barrier.Boundary.Anchor.Y, w, h, "Barrier")
barrierCollider.SetShape(barrierColliderShape)
boundaryUnaligned := barrier.Boundary
barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier")
space.Add(barrierCollider)
}
}
func (pR *Room) printBarrier(barrierCollider *resolv.Object) {
Logger.Info(fmt.Sprintf("Barrier in roomId=%v: w=%v, h=%v, shape=%v", pR.Id, barrierCollider.W, barrierCollider.H, barrierCollider.Shape))
}

View File

@@ -2,9 +2,9 @@ package models
import (
"container/heap"
. "dnmshared"
"fmt"
"go.uber.org/zap"
. "server/common"
"sync"
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,13 @@
package storage
import (
. "server/common"
. "battle_srv/common"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
. "dnmshared"
)
var (
@@ -15,8 +17,12 @@ var (
func initMySQL() {
var err error
MySQLManagerIns, err = sqlx.Connect("mysql", Conf.MySQL.DSN+"?charset=utf8mb4")
ErrFatal(err)
if nil != err {
panic(err)
}
err = MySQLManagerIns.Ping()
ErrFatal(err)
if nil != err {
panic(err)
}
Logger.Info("MySQLManagerIns", zap.Any("mysql", MySQLManagerIns))
}

View File

@@ -1,12 +1,14 @@
package storage
import (
. "battle_srv/common"
"fmt"
. "server/common"
"github.com/go-redis/redis"
_ "github.com/go-sql-driver/mysql"
"go.uber.org/zap"
. "dnmshared"
)
var (
@@ -20,6 +22,8 @@ func initRedis() {
DB: Conf.Redis.Dbname, // use default DB
})
pong, err := RedisManagerIns.Ping().Result()
ErrFatal(err)
if nil != err {
panic(err)
}
Logger.Info("Redis", zap.String("ping", pong))
}

View File

@@ -17,7 +17,9 @@ func loadTMX(fp string, pTmxMapIns *models.TmxMap) {
}
byteArr, err := ioutil.ReadFile(fp)
ErrFatal(err)
if nil != err {
panic(err)
}
models.DeserializeToTmxMapIns(byteArr, pTmxMapIns)
for _, info := range pTmxMapIns.TreasuresInfo {
fmt.Printf("treasuresInfo: %v\n", info)
@@ -33,7 +35,9 @@ func loadTSX(fp string, pTsxIns *models.Tsx) {
}
byteArr, err := ioutil.ReadFile(fp)
ErrFatal(err)
if nil != err {
panic(err)
}
models.DeserializeToTsxIns(byteArr, pTsxIns)
for _, Pos := range pTsxIns.TrapPolyLineList {
fmt.Printf("%v\n", Pos)
@@ -43,10 +47,14 @@ func loadTSX(fp string, pTsxIns *models.Tsx) {
func getTMXInfo() {
relativePath = "../frontend/assets/resources/map/treasurehunter.tmx"
execPath, err := os.Executable()
ErrFatal(err)
if nil != err {
panic(err)
}
pwd, err := os.Getwd()
ErrFatal(err)
if nil != err {
panic(err)
}
fmt.Printf("execPath = %v, pwd = %s, returning...\n", execPath, pwd)
@@ -61,10 +69,14 @@ func getTSXInfo() {
relativePath = "../frontend/assets/resources/map/tile_1.tsx"
execPath, err := os.Executable()
ErrFatal(err)
if nil != err {
panic(err)
}
pwd, err := os.Getwd()
ErrFatal(err)
if nil != err {
panic(err)
}
fmt.Printf("execPath = %v, pwd = %s, returning...\n", execPath, pwd)
tsxIns := models.Tsx{}

View File

@@ -1,6 +1,9 @@
package ws
import (
. "battle_srv/common"
"battle_srv/models"
pb "battle_srv/protos"
"container/heap"
"fmt"
"github.com/gin-gonic/gin"
@@ -8,12 +11,12 @@ import (
"github.com/gorilla/websocket"
"go.uber.org/zap"
"net/http"
. "server/common"
"server/models"
pb "server/pb_output"
"strconv"
"sync/atomic"
"time"
. "dnmshared"
"runtime/debug"
)
const (
@@ -102,7 +105,7 @@ func Serve(c *gin.Context) {
}
defer func() {
if r := recover(); r != nil {
Logger.Warn("Recovered from: ", zap.Any("panic", r))
Logger.Error("Recovered from: ", zap.Any("panic", r))
}
}()
/**
@@ -244,8 +247,8 @@ func Serve(c *gin.Context) {
bciFrame := &pb.BattleColliderInfo{
BoundRoomId: pRoom.Id,
StageName: pRoom.StageName,
StrToVec2DListMap: models.ToPbVec2DListMap(pRoom.RawBattleStrToVec2DListMap),
StrToPolygon2DListMap: models.ToPbPolygon2DListMap(pRoom.RawBattleStrToPolygon2DListMap),
StrToVec2DListMap: pRoom.RawBattleStrToVec2DListMap,
StrToPolygon2DListMap: pRoom.RawBattleStrToPolygon2DListMap,
StageDiscreteW: pRoom.StageDiscreteW,
StageDiscreteH: pRoom.StageDiscreteH,
StageTileW: pRoom.StageTileW,
@@ -261,6 +264,11 @@ func Serve(c *gin.Context) {
InputFrameUpsyncDelayTolerance: pRoom.InputFrameUpsyncDelayTolerance,
MaxChasingRenderFramesPerUpdate: pRoom.MaxChasingRenderFramesPerUpdate,
PlayerBattleState: pThePlayer.BattleState, // For frontend to know whether it's rejoining
RollbackEstimatedDtMillis: pRoom.RollbackEstimatedDtMillis,
RollbackEstimatedDtNanos: pRoom.RollbackEstimatedDtNanos,
WorldToVirtualGridRatio: pRoom.WorldToVirtualGridRatio,
VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio,
}
resp := &pb.WsResp{
@@ -349,7 +357,7 @@ func Serve(c *gin.Context) {
receivingLoopAgainstPlayer := func() error {
defer func() {
if r := recover(); r != nil {
Logger.Warn("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
Logger.Error("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r), zap.Any("callstack", debug.Stack()))
}
Logger.Info("Goroutine `receivingLoopAgainstPlayer` is stopped for:", zap.Any("playerId", playerId), zap.Any("roomId", pRoom.Id))
}()

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

File diff suppressed because one or more lines are too long

BIN
charts/InputDelayIntro.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
charts/RollbackAndChase.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
charts/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

BIN
charts/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -0,0 +1,21 @@
PROJECTNAME=viscol.exe
ROOT_DIR=.
all: help
## Available proxies for downloading go modules are listed in "https://github.com/golang/go/wiki/Modules#how-do-i-use-vendoring-with-modules-is-vendoring-going-away".
GOPROXY=https://mirrors.aliyun.com/goproxy
build:
go build -o $(ROOT_DIR)/$(PROJECTNAME)
run: build
./$(PROJECTNAME)
.PHONY: help
help: Makefile
@echo
@echo " Choose a command run:"
@echo
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
@echo

View File

@@ -0,0 +1,52 @@
package main
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/solarlune/resolv"
"image/color"
)
var (
PolygonFillerImage = ebiten.NewImage(1, 1)
)
func DrawPolygon(screen *ebiten.Image, shape *resolv.ConvexPolygon, clr color.Color) {
PolygonFillerImage.Fill(clr)
indices := []uint16{}
vs := []ebiten.Vertex{}
coors := shape.Transformed()
centerX := float64(0)
centerY := float64(0)
n := uint16(len(coors))
for i, coor := range coors {
centerX += coor.X()
centerY += coor.Y()
vs = append(vs, ebiten.Vertex{
DstX: float32(coor.X()),
DstY: float32(coor.Y()),
SrcX: 0,
SrcY: 0,
ColorR: 1,
ColorG: 1,
ColorB: 1,
ColorA: 1,
})
indices = append(indices, uint16(i), uint16(i+1)%n, n)
}
centerX = centerX / float64(n)
centerY = centerY / float64(n)
vs = append(vs, ebiten.Vertex{
DstX: float32(centerX),
DstY: float32(centerY),
SrcX: 0,
SrcY: 0,
ColorR: 1,
ColorG: 1,
ColorB: 1,
ColorA: 1,
})
screen.DrawTriangles(vs, indices, PolygonFillerImage, nil)
}

Binary file not shown.

View File

@@ -0,0 +1,28 @@
module viscol
go 1.19
require (
dnmshared v0.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/hajimehoshi/ebiten/v2 v2.4.7
github.com/solarlune/resolv v0.5.1
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
)
require (
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad // indirect
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 // indirect
github.com/jezek/xgb v1.0.1 // indirect
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
replace dnmshared => ../dnmshared

102
collider_visualizer/go.sum Normal file
View File

@@ -0,0 +1,102 @@
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744 h1:A8UnJ/5OKzki4HBDwoRQz7I6sxKsokpMXcGh+fUxpfc=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJPCvzV9jUoVQG9GEUqIq5hjfYzXTqQ52Rh8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hajimehoshi/bitmapfont/v2 v2.2.1 h1:y7zcy02/UgO24IL3COqYtrRZzhRucNBtmCo/SNU648k=
github.com/hajimehoshi/bitmapfont/v2 v2.2.1/go.mod h1:wjrYAy8vKgj9JsFgnYAOK346/uvE22TlmqouzdnYIs0=
github.com/hajimehoshi/ebiten/v2 v2.4.7 h1:XuvB7R0Rbw/O7g6vNU8gqr5b9e7MNhhAONMSsyreLDI=
github.com/hajimehoshi/ebiten/v2 v2.4.7/go.mod h1:Ofk1EfQZZ8tL0TlEPF5wPrnN+8Oa/ywuQOYh+uYsqLQ=
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41 h1:s01qIIRG7vN/5ndLwkDktjx44ulFk6apvAjVBYR50Yo=
github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
github.com/jakecoffman/cp v1.2.1/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
github.com/jezek/xgb v1.0.1 h1:YUGhxps0aR7J2Xplbs23OHnV1mWaxFVcOl9b+1RQkt8=
github.com/jezek/xgb v1.0.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jfreymuth/oggvorbis v1.0.4/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0 h1:v8lWpj5957KtDMKu+xQtlu6G3ZoZR6Tn9bsfZCRG5Xw=
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0/go.mod h1:GAX7tMJqXx9fB1BrsTWPOXy6IBRX+J461BffVPAdpwo=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/solarlune/resolv v0.5.1 h1:Ul6PAs/zaxiMUOEYz1Z6VeUj5k3CDcWMvSh+kivybDY=
github.com/solarlune/resolv v0.5.1/go.mod h1:HjM2f/0NoVjVdZsi26GtugX5aFbA62COEFEXkOhveRw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 h1:3vUV5x5+3LfQbgk7paCM6INOaJG9xXQbn79xoNkwfIk=
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 h1:Sx/u41w+OwrInGdEckYmEuU5gHoGSL4QbDz3S9s6j4U=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

171
collider_visualizer/main.go Normal file
View File

@@ -0,0 +1,171 @@
package main
import (
_ "embed"
"fmt"
"image/color"
"go.uber.org/zap"
"github.com/golang/freetype/truetype"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/solarlune/resolv"
"golang.org/x/image/font"
"encoding/xml"
"io/ioutil"
"os"
"path/filepath"
. "dnmshared"
)
func parseStage(stageName string) (int32, int32, int32, int32, StrToVec2DListMap, StrToPolygon2DListMap, error) {
pwd, err := os.Getwd()
if nil != err {
Logger.Error("Failed to get current working dir:", zap.Any("pwd", pwd), zap.Any("err", err))
}
relativePathForAllStages := "../frontend/assets/resources/map"
relativePathForChosenStage := fmt.Sprintf("%s/%s", relativePathForAllStages, stageName)
pTmxMapIns := &TmxMap{}
absDirPathContainingDirectlyTmxFile := filepath.Join(pwd, relativePathForChosenStage)
absTmxFilePath := fmt.Sprintf("%s/map.tmx", absDirPathContainingDirectlyTmxFile)
if !filepath.IsAbs(absTmxFilePath) {
panic("Tmx filepath must be absolute!")
}
byteArr, err := ioutil.ReadFile(absTmxFilePath)
if nil != err {
panic(err)
}
err = xml.Unmarshal(byteArr, pTmxMapIns)
if nil != err {
panic(err)
}
// Obtain the content of `gidBoundariesMapInB2World`.
gidBoundariesMapInB2World := make(map[int]StrToPolygon2DListMap, 0)
for _, tileset := range pTmxMapIns.Tilesets {
relativeTsxFilePath := fmt.Sprintf("%s/%s", filepath.Join(pwd, relativePathForChosenStage), tileset.Source) // Note that "TmxTileset.Source" can be a string of "relative path".
absTsxFilePath, err := filepath.Abs(relativeTsxFilePath)
if nil != err {
panic(err)
}
if !filepath.IsAbs(absTsxFilePath) {
panic("Filepath must be absolute!")
}
byteArrOfTsxFile, err := ioutil.ReadFile(absTsxFilePath)
if nil != err {
panic(err)
}
DeserializeTsxToColliderDict(pTmxMapIns, byteArrOfTsxFile, int(tileset.FirstGid), gidBoundariesMapInB2World)
}
return ParseTmxLayersAndGroups(pTmxMapIns, gidBoundariesMapInB2World)
}
//go:embed excel.ttf
var excelFont []byte
type Game struct {
World WorldInterface
Width, Height int
Debug bool
ShowHelpText bool
Screen *ebiten.Image
FontFace font.Face
}
func NewGame() *Game {
stageName := "simple" // Use this for calibration
// stageName := "richsoil"
stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap, err := parseStage(stageName)
if nil != err {
panic(err)
}
PolygonFillerImage.Fill(color.RGBA{60, 60, 60, 255}) // Required to init color of the polygons!
spaceW := stageDiscreteW * stageTileW
spaceH := stageDiscreteH * stageTileH
ebiten.SetWindowResizable(true)
ebiten.SetWindowTitle("resolv test")
g := &Game{
Width: int(spaceW),
Height: int(spaceH),
ShowHelpText: true,
}
g.World = NewWorldColliderDisplay(g, stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap)
fontData, _ := truetype.Parse(excelFont)
g.FontFace = truetype.NewFace(fontData, &truetype.Options{Size: 10})
return g
}
func (g *Game) Update() error {
g.World.Update()
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
g.Screen = screen
screen.Fill(color.RGBA{20, 20, 40, 255})
g.World.Draw(screen)
}
func (g *Game) DrawText(screen *ebiten.Image, x, y int, textLines ...string) {
rectHeight := 10
for _, txt := range textLines {
w := float64(font.MeasureString(g.FontFace, txt).Round())
ebitenutil.DrawRect(screen, float64(x), float64(y-8), w, float64(rectHeight), color.RGBA{0, 0, 0, 192})
text.Draw(screen, txt, g.FontFace, x+1, y+1, color.RGBA{0, 0, 150, 255})
text.Draw(screen, txt, g.FontFace, x, y, color.RGBA{100, 150, 255, 255})
y += rectHeight
}
}
func (g *Game) DebugDraw(screen *ebiten.Image, space *resolv.Space) {
for y := 0; y < space.Height(); y++ {
for x := 0; x < space.Width(); x++ {
cell := space.Cell(x, y)
cw := float64(space.CellWidth)
ch := float64(space.CellHeight)
cx := float64(cell.X) * cw
cy := float64(cell.Y) * ch
drawColor := color.RGBA{20, 20, 20, 255}
if cell.Occupied() {
drawColor = color.RGBA{255, 255, 0, 255}
}
ebitenutil.DrawLine(screen, cx, cy, cx+cw, cy, drawColor)
ebitenutil.DrawLine(screen, cx+cw, cy, cx+cw, cy+ch, drawColor)
ebitenutil.DrawLine(screen, cx+cw, cy+ch, cx, cy+ch, drawColor)
ebitenutil.DrawLine(screen, cx, cy+ch, cx, cy, drawColor)
}
}
}
func (g *Game) Layout(w, h int) (int, int) {
return g.Width, g.Height
}
func main() {
ebiten.RunGame(NewGame())
}

View File

@@ -0,0 +1,122 @@
package main
import (
. "dnmshared"
. "dnmshared/sharedprotos"
"fmt"
"github.com/hajimehoshi/ebiten/v2"
"github.com/solarlune/resolv"
"go.uber.org/zap"
"image/color"
)
type WorldColliderDisplay struct {
Game *Game
Space *resolv.Space
}
func (world *WorldColliderDisplay) Init() {
}
func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTileW, stageTileH int32, playerPosMap StrToVec2DListMap, barrierMap StrToPolygon2DListMap) *WorldColliderDisplay {
playerPosList := *(playerPosMap["PlayerStartingPos"])
barrierList := *(barrierMap["Barrier"])
world := &WorldColliderDisplay{Game: game}
Logger.Info("Parsed variables", zap.Any("stageDiscreteW", stageDiscreteW), zap.Any("stageDiscreteH", stageDiscreteH), zap.Any("stageTileW", stageTileW), zap.Any("stageTileH", stageTileH))
spaceW := stageDiscreteW * stageTileW
spaceH := stageDiscreteH * stageTileH
spaceOffsetX := float64(spaceW) * 0.5
spaceOffsetY := float64(spaceH) * 0.5
virtualGridToWorldRatio := 0.1
playerDefaultSpeed := 20
minStep := (int(float64(playerDefaultSpeed)*virtualGridToWorldRatio) << 2)
playerColliderRadius := float64(24)
playerColliders := make([]*resolv.Object, len(playerPosList.Eles))
space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep)
for i, playerPos := range playerPosList.Eles {
playerCollider := GenerateRectCollider(playerPos.X, playerPos.Y, playerColliderRadius*2, playerColliderRadius*2, spaceOffsetX, spaceOffsetY, "Player") // [WARNING] Deliberately not using a circle because "resolv v0.5.1" doesn't yet align circle center with space cell center, regardless of the "specified within-object offset"
Logger.Info(fmt.Sprintf("Player Collider#%d: player world pos =(%.2f, %.2f), shape=%v", i, playerPos.X, playerPos.Y, ConvexPolygonStr(playerCollider.Shape.(*resolv.ConvexPolygon))))
playerColliders[i] = playerCollider
space.Add(playerCollider)
}
barrierLocalId := 0
for _, barrierUnaligned := range barrierList.Eles {
barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
Logger.Info(fmt.Sprintf("Added barrier: shape=%v", ConvexPolygonStr(barrierCollider.Shape.(*resolv.ConvexPolygon))))
space.Add(barrierCollider)
barrierLocalId++
}
world.Space = space
moveToCollide := true
if moveToCollide {
newVx, newVy := int32(-2959), int32(-2261)
effPushback := Vec2D{X: float64(0), Y: float64(0)}
toTestPlayerCollider := playerColliders[0]
toTestPlayerCollider.X, toTestPlayerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, playerColliderRadius, playerColliderRadius, spaceOffsetX, spaceOffsetY, virtualGridToWorldRatio)
Logger.Info(fmt.Sprintf("Checking collision for virtual (%d, %d), now playerShape=%v", newVx, newVy, ConvexPolygonStr(toTestPlayerCollider.Shape.(*resolv.ConvexPolygon))))
toTestPlayerCollider.Update()
if collision := toTestPlayerCollider.Check(0, 0); collision != nil {
playerShape := toTestPlayerCollider.Shape.(*resolv.ConvexPolygon)
for _, obj := range collision.Objects {
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape); overlapped {
Logger.Warn(fmt.Sprintf("Overlapped: a=%v, b=%v, pushbackX=%v, pushbackY=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY))
effPushback.X += pushbackX
effPushback.Y += pushbackY
} else {
Logger.Warn(fmt.Sprintf("Collided BUT not overlapped: a=%v, b=%v, overlapResult=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), overlapResult))
}
}
toTestPlayerCollider.X -= effPushback.X
toTestPlayerCollider.Y -= effPushback.Y
toTestPlayerCollider.Update()
Logger.Info(fmt.Sprintf("effPushback={%v, %v}", effPushback.X, effPushback.Y))
}
}
return world
}
func (world *WorldColliderDisplay) Update() {
}
func (world *WorldColliderDisplay) Draw(screen *ebiten.Image) {
for _, o := range world.Space.Objects() {
if o.HasTags("Player") {
drawColor := color.RGBA{0, 255, 0, 255}
DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)
} else {
drawColor := color.RGBA{60, 60, 60, 255}
DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)
}
}
world.Game.DebugDraw(screen, world.Space)
if world.Game.ShowHelpText {
world.Game.DrawText(screen, 16, 16,
"~ Collider Display test ~",
"F1: Toggle Debug View",
"F2: Show / Hide help text",
"R: Restart world",
fmt.Sprintf("%d FPS (frames per second)", int(ebiten.CurrentFPS())),
fmt.Sprintf("%d TPS (ticks per second)", int(ebiten.CurrentTPS())),
)
}
}

View File

@@ -0,0 +1,9 @@
package main
import "github.com/hajimehoshi/ebiten/v2"
type WorldInterface interface {
Init()
Update()
Draw(*ebiten.Image)
}

50
dnmshared/geometry.go Normal file
View File

@@ -0,0 +1,50 @@
package dnmshared
import (
. "dnmshared/sharedprotos"
"math"
)
func NormVec2D(dx, dy float64) Vec2D {
return Vec2D{X: dy, Y: -dx}
}
func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D {
// Transform again to put "anchor" at the top-left point of the bounding box for "resolv"
boundingBoxTL := &Vec2D{
X: math.MaxFloat64,
Y: math.MaxFloat64,
}
for _, p := range input.Points {
if p.X < boundingBoxTL.X {
boundingBoxTL.X = p.X
}
if p.Y < boundingBoxTL.Y {
boundingBoxTL.Y = p.Y
}
}
// Now "input.Anchor" should move to "input.Anchor+boundingBoxTL", thus "boundingBoxTL" is also the value of the negative diff for all "input.Points"
output := &Polygon2D{
Anchor: &Vec2D{
X: input.Anchor.X + boundingBoxTL.X,
Y: input.Anchor.Y + boundingBoxTL.Y,
},
Points: make([]*Vec2D, len(input.Points)),
}
for i, p := range input.Points {
output.Points[i] = &Vec2D{
X: p.X - boundingBoxTL.X,
Y: p.Y - boundingBoxTL.Y,
}
}
return output
}
func Distance(pt1 *Vec2D, pt2 *Vec2D) float64 {
dx := pt1.X - pt2.X
dy := pt1.Y - pt2.Y
return math.Sqrt(dx*dx + dy*dy)
}

3
dnmshared/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module dnmshared
go 1.19

View File

@@ -1,4 +1,4 @@
package common
package dnmshared
import (
"go.uber.org/zap"
@@ -19,5 +19,7 @@ func init() {
LoggerConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
var err error
Logger, err = LoggerConfig.Build()
ErrFatal(err)
if nil != err {
panic(err)
}
}

255
dnmshared/resolv_helper.go Normal file
View File

@@ -0,0 +1,255 @@
package dnmshared
import (
. "dnmshared/sharedprotos"
"fmt"
"github.com/kvartborg/vector"
"github.com/solarlune/resolv"
"math"
"strings"
)
func ConvexPolygonStr(body *resolv.ConvexPolygon) string {
var s []string = make([]string, len(body.Points))
for i, p := range body.Points {
s[i] = fmt.Sprintf("[%.2f, %.2f]", p[0]+body.X, p[1]+body.Y)
}
return fmt.Sprintf("{\n%s\n}", strings.Join(s, ",\n"))
}
func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
cx, cy := WorldToPolygonColliderAnchorPos(origX, origY, w*0.5, h*0.5, spaceOffsetX, spaceOffsetY)
collider := resolv.NewObject(cx, cy, w, h, tag)
shape := resolv.NewRectangle(0, 0, w, h)
collider.SetShape(shape)
return collider
}
func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
aligned := AlignPolygon2DToBoundingBox(unalignedSrc)
var w, h float64 = 0, 0
shape := resolv.NewConvexPolygon()
for i, pi := range aligned.Points {
for j, pj := range aligned.Points {
if i == j {
continue
}
if math.Abs(pj.X-pi.X) > w {
w = math.Abs(pj.X - pi.X)
}
if math.Abs(pj.Y-pi.Y) > h {
h = math.Abs(pj.Y - pi.Y)
}
}
}
for i := 0; i < len(aligned.Points); i++ {
p := aligned.Points[i]
shape.AddPoints(p.X, p.Y)
}
collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag)
collider.SetShape(shape)
return collider
}
func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64, *SatResult) {
origX, origY := playerShape.Position()
defer func() {
playerShape.SetPosition(origX, origY)
}()
playerShape.SetPosition(origX+oldDx, origY+oldDy)
overlapResult := &SatResult{
Overlap: 0,
OverlapX: 0,
OverlapY: 0,
AContainedInB: true,
BContainedInA: true,
Axis: vector.Vector{0, 0},
}
if overlapped := IsPolygonPairOverlapped(playerShape, barrierShape, overlapResult); overlapped {
pushbackX, pushbackY := overlapResult.Overlap*overlapResult.OverlapX, overlapResult.Overlap*overlapResult.OverlapY
return true, pushbackX, pushbackY, overlapResult
} else {
return false, 0, 0, overlapResult
}
}
type SatResult struct {
Overlap float64
OverlapX float64
OverlapY float64
AContainedInB bool
BContainedInA bool
Axis vector.Vector
}
func IsPolygonPairOverlapped(a, b *resolv.ConvexPolygon, result *SatResult) bool {
aCnt, bCnt := len(a.Points), len(b.Points)
// Single point case
if 1 == aCnt && 1 == bCnt {
if nil != result {
result.Overlap = 0
}
return a.Points[0].X() == b.Points[0].X() && a.Points[0].Y() == b.Points[0].Y()
}
if 1 < aCnt {
for _, axis := range a.SATAxes() {
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
return false
}
}
}
if 1 < bCnt {
for _, axis := range b.SATAxes() {
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
return false
}
}
}
return true
}
func isPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e vector.Vector, result *SatResult) bool {
/*
[WARNING] This function is deliberately made private, it shouldn't be used alone (i.e. not along the norms of a polygon), otherwise the pushbacks calculated would be meaningless.
Consider the following example
a: {
anchor: [1337.19 1696.74]
points: [[0 0] [24 0] [24 24] [0 24]]
},
b: {
anchor: [1277.72 1570.56]
points: [[642.57 319.16] [0 319.16] [5.73 0] [643.75 0.90]]
}
e = (-2.98, 1.49).Unit()
*/
var aStart, aEnd, bStart, bEnd float64 = math.MaxFloat64, -math.MaxFloat64, math.MaxFloat64, -math.MaxFloat64
for _, p := range a.Points {
dot := (p.X()+a.X)*e.X() + (p.Y()+a.Y)*e.Y()
if aStart > dot {
aStart = dot
}
if aEnd < dot {
aEnd = dot
}
}
for _, p := range b.Points {
dot := (p.X()+b.X)*e.X() + (p.Y()+b.Y)*e.Y()
if bStart > dot {
bStart = dot
}
if bEnd < dot {
bEnd = dot
}
}
if aStart > bEnd || aEnd < bStart {
// Separated by unit vector "e"
return true
}
if nil != result {
result.Axis = e
overlap := float64(0)
if aStart < bStart {
result.AContainedInB = false
if aEnd < bEnd {
overlap = aEnd - bStart
result.BContainedInA = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = -option2
}
}
} else {
result.BContainedInA = false
if aEnd > bEnd {
overlap = aStart - bEnd
result.AContainedInB = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = -option2
}
}
}
currentOverlap := result.Overlap
absoluteOverlap := overlap
if overlap < 0 {
absoluteOverlap = -overlap
}
if 0 == currentOverlap || currentOverlap > absoluteOverlap {
var sign float64 = 1
if overlap < 0 {
sign = -1
}
result.Overlap = absoluteOverlap
result.OverlapX = e.X() * sign
result.OverlapY = e.Y() * sign
}
}
// the specified unit vector "e" doesn't separate "a" and "b", overlap result is generated
return false
}
func WorldToVirtualGridPos(wx, wy, worldToVirtualGridRatio float64) (int32, int32) {
// [WARNING] Introduces loss of precision!
// In JavaScript floating numbers suffer from seemingly non-deterministic arithmetics, and even if certain libs solved this issue by approaches such as fixed-point-number, they might not be used in other libs -- e.g. the "collision libs" we're interested in -- thus couldn't kill all pains.
var virtualGridX int32 = int32(math.Round(wx * worldToVirtualGridRatio))
var virtualGridY int32 = int32(math.Round(wy * worldToVirtualGridRatio))
return virtualGridX, virtualGridY
}
func VirtualGridToWorldPos(vx, vy int32, virtualGridToWorldRatio float64) (float64, float64) {
// No loss of precision
var wx float64 = float64(vx) * virtualGridToWorldRatio
var wy float64 = float64(vy) * virtualGridToWorldRatio
return wx, wy
}
func WorldToPolygonColliderAnchorPos(wx, wy, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY float64) (float64, float64) {
return wx - halfBoundingW + collisionSpaceOffsetX, wy - halfBoundingH + collisionSpaceOffsetY
}
func PolygonColliderAnchorToWorldPos(cx, cy, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY float64) (float64, float64) {
return cx + halfBoundingW - collisionSpaceOffsetX, cy + halfBoundingH - collisionSpaceOffsetY
}
func PolygonColliderAnchorToVirtualGridPos(cx, cy, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY float64, worldToVirtualGridRatio float64) (int32, int32) {
wx, wy := PolygonColliderAnchorToWorldPos(cx, cy, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY)
return WorldToVirtualGridPos(wx, wy, worldToVirtualGridRatio)
}
func VirtualGridToPolygonColliderAnchorPos(vx, vy int32, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY float64, virtualGridToWorldRatio float64) (float64, float64) {
wx, wy := VirtualGridToWorldPos(vx, vy, virtualGridToWorldRatio)
return WorldToPolygonColliderAnchorPos(wx, wy, halfBoundingW, halfBoundingH, collisionSpaceOffsetX, collisionSpaceOffsetY)
}

View File

@@ -0,0 +1,427 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.21.4
// source: geometry.proto
package sharedprotos
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Direction struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Dx int32 `protobuf:"varint,1,opt,name=dx,proto3" json:"dx,omitempty"`
Dy int32 `protobuf:"varint,2,opt,name=dy,proto3" json:"dy,omitempty"`
}
func (x *Direction) Reset() {
*x = Direction{}
if protoimpl.UnsafeEnabled {
mi := &file_geometry_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Direction) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Direction) ProtoMessage() {}
func (x *Direction) ProtoReflect() protoreflect.Message {
mi := &file_geometry_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Direction.ProtoReflect.Descriptor instead.
func (*Direction) Descriptor() ([]byte, []int) {
return file_geometry_proto_rawDescGZIP(), []int{0}
}
func (x *Direction) GetDx() int32 {
if x != nil {
return x.Dx
}
return 0
}
func (x *Direction) GetDy() int32 {
if x != nil {
return x.Dy
}
return 0
}
type Vec2D struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
X float64 `protobuf:"fixed64,1,opt,name=x,proto3" json:"x,omitempty"`
Y float64 `protobuf:"fixed64,2,opt,name=y,proto3" json:"y,omitempty"`
}
func (x *Vec2D) Reset() {
*x = Vec2D{}
if protoimpl.UnsafeEnabled {
mi := &file_geometry_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Vec2D) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Vec2D) ProtoMessage() {}
func (x *Vec2D) ProtoReflect() protoreflect.Message {
mi := &file_geometry_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Vec2D.ProtoReflect.Descriptor instead.
func (*Vec2D) Descriptor() ([]byte, []int) {
return file_geometry_proto_rawDescGZIP(), []int{1}
}
func (x *Vec2D) GetX() float64 {
if x != nil {
return x.X
}
return 0
}
func (x *Vec2D) GetY() float64 {
if x != nil {
return x.Y
}
return 0
}
type Polygon2D struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Anchor *Vec2D `protobuf:"bytes,1,opt,name=anchor,proto3" json:"anchor,omitempty"`
Points []*Vec2D `protobuf:"bytes,2,rep,name=points,proto3" json:"points,omitempty"`
}
func (x *Polygon2D) Reset() {
*x = Polygon2D{}
if protoimpl.UnsafeEnabled {
mi := &file_geometry_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Polygon2D) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Polygon2D) ProtoMessage() {}
func (x *Polygon2D) ProtoReflect() protoreflect.Message {
mi := &file_geometry_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Polygon2D.ProtoReflect.Descriptor instead.
func (*Polygon2D) Descriptor() ([]byte, []int) {
return file_geometry_proto_rawDescGZIP(), []int{2}
}
func (x *Polygon2D) GetAnchor() *Vec2D {
if x != nil {
return x.Anchor
}
return nil
}
func (x *Polygon2D) GetPoints() []*Vec2D {
if x != nil {
return x.Points
}
return nil
}
type Vec2DList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Eles []*Vec2D `protobuf:"bytes,1,rep,name=eles,proto3" json:"eles,omitempty"`
}
func (x *Vec2DList) Reset() {
*x = Vec2DList{}
if protoimpl.UnsafeEnabled {
mi := &file_geometry_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Vec2DList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Vec2DList) ProtoMessage() {}
func (x *Vec2DList) ProtoReflect() protoreflect.Message {
mi := &file_geometry_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Vec2DList.ProtoReflect.Descriptor instead.
func (*Vec2DList) Descriptor() ([]byte, []int) {
return file_geometry_proto_rawDescGZIP(), []int{3}
}
func (x *Vec2DList) GetEles() []*Vec2D {
if x != nil {
return x.Eles
}
return nil
}
type Polygon2DList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Eles []*Polygon2D `protobuf:"bytes,1,rep,name=eles,proto3" json:"eles,omitempty"`
}
func (x *Polygon2DList) Reset() {
*x = Polygon2DList{}
if protoimpl.UnsafeEnabled {
mi := &file_geometry_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Polygon2DList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Polygon2DList) ProtoMessage() {}
func (x *Polygon2DList) ProtoReflect() protoreflect.Message {
mi := &file_geometry_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Polygon2DList.ProtoReflect.Descriptor instead.
func (*Polygon2DList) Descriptor() ([]byte, []int) {
return file_geometry_proto_rawDescGZIP(), []int{4}
}
func (x *Polygon2DList) GetEles() []*Polygon2D {
if x != nil {
return x.Eles
}
return nil
}
var File_geometry_proto protoreflect.FileDescriptor
var file_geometry_proto_rawDesc = []byte{
0x0a, 0x0e, 0x67, 0x65, 0x6f, 0x6d, 0x65, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x12, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x22, 0x2b,
0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x64,
0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x64, 0x78, 0x12, 0x0e, 0x0a, 0x02, 0x64,
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x64, 0x79, 0x22, 0x23, 0x0a, 0x05, 0x56,
0x65, 0x63, 0x32, 0x44, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52,
0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x01, 0x79,
0x22, 0x65, 0x0a, 0x09, 0x50, 0x6f, 0x6c, 0x79, 0x67, 0x6f, 0x6e, 0x32, 0x44, 0x12, 0x2b, 0x0a,
0x06, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e,
0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x56, 0x65, 0x63,
0x32, 0x44, 0x52, 0x06, 0x61, 0x6e, 0x63, 0x68, 0x6f, 0x72, 0x12, 0x2b, 0x0a, 0x06, 0x70, 0x6f,
0x69, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x68, 0x61,
0x72, 0x65, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x56, 0x65, 0x63, 0x32, 0x44, 0x52,
0x06, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x34, 0x0a, 0x09, 0x56, 0x65, 0x63, 0x32, 0x44,
0x4c, 0x69, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x04, 0x65, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x73, 0x2e, 0x56, 0x65, 0x63, 0x32, 0x44, 0x52, 0x04, 0x65, 0x6c, 0x65, 0x73, 0x22, 0x3c, 0x0a,
0x0d, 0x50, 0x6f, 0x6c, 0x79, 0x67, 0x6f, 0x6e, 0x32, 0x44, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x2b,
0x0a, 0x04, 0x65, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x73,
0x68, 0x61, 0x72, 0x65, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2e, 0x50, 0x6f, 0x6c, 0x79,
0x67, 0x6f, 0x6e, 0x32, 0x44, 0x52, 0x04, 0x65, 0x6c, 0x65, 0x73, 0x42, 0x18, 0x5a, 0x16, 0x64,
0x6e, 0x6d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2f, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_geometry_proto_rawDescOnce sync.Once
file_geometry_proto_rawDescData = file_geometry_proto_rawDesc
)
func file_geometry_proto_rawDescGZIP() []byte {
file_geometry_proto_rawDescOnce.Do(func() {
file_geometry_proto_rawDescData = protoimpl.X.CompressGZIP(file_geometry_proto_rawDescData)
})
return file_geometry_proto_rawDescData
}
var file_geometry_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_geometry_proto_goTypes = []interface{}{
(*Direction)(nil), // 0: sharedprotos.Direction
(*Vec2D)(nil), // 1: sharedprotos.Vec2D
(*Polygon2D)(nil), // 2: sharedprotos.Polygon2D
(*Vec2DList)(nil), // 3: sharedprotos.Vec2DList
(*Polygon2DList)(nil), // 4: sharedprotos.Polygon2DList
}
var file_geometry_proto_depIdxs = []int32{
1, // 0: sharedprotos.Polygon2D.anchor:type_name -> sharedprotos.Vec2D
1, // 1: sharedprotos.Polygon2D.points:type_name -> sharedprotos.Vec2D
1, // 2: sharedprotos.Vec2DList.eles:type_name -> sharedprotos.Vec2D
2, // 3: sharedprotos.Polygon2DList.eles:type_name -> sharedprotos.Polygon2D
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_geometry_proto_init() }
func file_geometry_proto_init() {
if File_geometry_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_geometry_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Direction); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_geometry_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Vec2D); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_geometry_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Polygon2D); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_geometry_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Vec2DList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_geometry_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Polygon2DList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_geometry_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_geometry_proto_goTypes,
DependencyIndexes: file_geometry_proto_depIdxs,
MessageInfos: file_geometry_proto_msgTypes,
}.Build()
File_geometry_proto = out.File
file_geometry_proto_rawDesc = nil
file_geometry_proto_goTypes = nil
file_geometry_proto_depIdxs = nil
}

View File

@@ -1,15 +1,15 @@
package models
package dnmshared
import (
"bytes"
"compress/zlib"
. "dnmshared/sharedprotos"
"encoding/base64"
"encoding/xml"
"errors"
"go.uber.org/zap"
"io/ioutil"
"math"
. "server/common"
"strconv"
"strings"
)
@@ -174,10 +174,8 @@ func (l *TmxLayer) decodeBase64() ([]uint32, error) {
return gids, nil
}
type Vec2DList []*Vec2D
type Polygon2DList []*Polygon2D
type StrToVec2DListMap map[string]*Vec2DList // Note that it's deliberately NOT using "map[string]Vec2DList", for the easy of passing return value to "models/room.go".
type StrToPolygon2DListMap map[string]*Polygon2DList // Note that it's deliberately NOT using "map[string]Polygon2DList", for the easy of passing return value to "models/room.go".
type StrToVec2DListMap map[string]*Vec2DList
type StrToPolygon2DListMap map[string]*Polygon2DList
func tmxPolylineToPolygon2D(pTmxMapIns *TmxMap, singleObjInTmxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline) (*Polygon2D, error) {
if nil == targetPolyline {
@@ -234,10 +232,8 @@ func tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns *TmxMap, singleObjInTsxFile *T
pointsCount := len(singleValueArray)
thePolygon2DFromPolyline := &Polygon2D{
Anchor: nil,
Points: make([]*Vec2D, pointsCount),
TileWidth: pTsxIns.TileWidth,
TileHeight: pTsxIns.TileHeight,
Anchor: nil,
Points: make([]*Vec2D, pointsCount),
}
/*
@@ -328,16 +324,17 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
if _, ok := theStrToPolygon2DListMap[key]; ok {
pThePolygon2DList = theStrToPolygon2DListMap[key]
} else {
thePolygon2DList := make(Polygon2DList, 0)
theStrToPolygon2DListMap[key] = &thePolygon2DList
pThePolygon2DList = theStrToPolygon2DListMap[key]
pThePolygon2DList = &Polygon2DList{
Eles: make([]*Polygon2D, 0),
}
theStrToPolygon2DListMap[key] = pThePolygon2DList
}
thePolygon2DFromPolyline, err := tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
if nil != err {
panic(err)
}
*pThePolygon2DList = append(*pThePolygon2DList, thePolygon2DFromPolyline)
pThePolygon2DList.Eles = append(pThePolygon2DList.Eles, thePolygon2DFromPolyline)
}
}
return nil
@@ -353,8 +350,10 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
var pTheVec2DListToCache *Vec2DList
_, ok := toRetStrToVec2DListMap[objGroup.Name]
if false == ok {
theVec2DListToCache := make(Vec2DList, 0)
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache
pTheVec2DListToCache = &Vec2DList{
Eles: make([]*Vec2D, 0),
}
toRetStrToVec2DListMap[objGroup.Name] = pTheVec2DListToCache
}
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects {
@@ -363,17 +362,18 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
Y: singleObjInTmxFile.Y,
}
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld)
pTheVec2DListToCache.Eles = append(pTheVec2DListToCache.Eles, &thePosInWorld)
}
case "Barrier":
// Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]".
var pThePolygon2DListToCache *Polygon2DList
_, ok := toRetStrToPolygon2DListMap[objGroup.Name]
if false == ok {
thePolygon2DListToCache := make(Polygon2DList, 0)
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
pThePolygon2DListToCache = &Polygon2DList{
Eles: make([]*Polygon2D, 0),
}
toRetStrToPolygon2DListMap[objGroup.Name] = pThePolygon2DListToCache
}
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects {
if nil == singleObjInTmxFile.Polyline {
@@ -387,7 +387,7 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
if nil != err {
panic(err)
}
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
pThePolygon2DListToCache.Eles = append(pThePolygon2DListToCache.Eles, thePolygon2DInWorld)
}
default:
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
{
"ver": "1.0.5",
"uuid": "bd514df4-095e-4088-9060-d99397a29a4f",
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.0",
"uuid": "51b3303a-7c55-4f8b-b6b9-b5efb6164d19",
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.0",
"uuid": "8347ba4a-e73c-4173-a9ba-f7711fae5a90",
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.0",
"uuid": "05720598-8487-406c-b2f3-2059240fde56",
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.2",
"uuid": "4e2296fe-8855-4fb4-a407-fea295ded82e",
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.0",
"uuid": "da664a14-58c3-4f57-8aca-1270f96bf3ee",
"subMetas": {}
}

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.0",
"uuid": "59b65107-0da7-47b3-a08d-7de824dd5a39",
"subMetas": {}
}

View File

@@ -1,335 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="isometric" renderorder="right-down" width="50" height="50" tilewidth="64" tileheight="64" infinite="0" nextlayerid="11" nextobjectid="213">
<tileset firstgid="1" source="Tile_W64_H64_S01.tsx"/>
<tileset firstgid="17" source="Tile_W300_H300_S01.tsx"/>
<layer id="1" name="GroundFloor" width="50" height="50">
<data encoding="base64" compression="zlib">
eJztz0ENACAMALGR4F8zNspyjwronZm7xPlcD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPy7bHBg/ldQwR
</data>
</layer>
<objectgroup id="2" name="PlayerStartingPos">
<object id="135" x="320" y="2884">
<point/>
</object>
<object id="137" x="2876" y="320">
<point/>
</object>
</objectgroup>
<layer id="3" name="FirsrtFloor" width="50" height="50">
<data encoding="base64" compression="zlib">
eJztWctuBCEMW7Vqt9v2/7+32gMSQnnYTmB6WB+ZwZmEkHHgdnuBxe8/5WLwcyHXuzH2ccAuw5Wty3j/bjyzxlC7M9iYrFxZfNbnqi/IOqAxqfow8AWOoXZmZL58k9xvzrg1L+JS9oO6LijfCR882yvWdUH5rG+KuBgfrFo42+7CJ/m+l5MWoloYjZ+CUsN2+OLVc0YDRPa9nNzhC7LPs//Xo8FuNs5wRvt2Rw7v8AXZ5zty2FvL3Xsf4e+qsVf60l1jq754MR2w8qFSY6MaqdSRJ7KYRnNWIL509iseZ4de93TyHZirwOOM1lXtC6JnSg+K2FPnVHSysr9P+4Dq8a5+Wp2jcHq5FPWgFXuZXu/sqxCuHX1cR+1BtN4A0y96/ycldiu8XOquxWhNOZ1LDFA+RCdn/4OOeFjw9nLl/5TVUPZ8YIX3bSd6z26w33zKl0xzM/xd2hvhnKFo7ox/h/ZmtOcpXzp1q6K5PXjfjPSAKJcF9jwXQdd9B8q1E8pZYUW3zlBqVYSOPRatm1XjKrUqQsUXties1qqs9kd8im5l7ovQnOzYr0wuWc+YOz7kvcq6ILkU9ZOqhmfvdj17M6rn3ayGV30YUM8qB6qxeIK9n96FaixmXOVDJ3bomRc4/AGscghn
</data>
</layer>
<objectgroup id="7" name="HighScoreTreasure">
<object id="172" gid="16" x="1736" y="708" width="64" height="64"/>
<object id="173" gid="16" x="2284" y="768" width="64" height="64"/>
<object id="170" gid="16" x="916" y="1608" width="64" height="64"/>
<object id="171" gid="16" x="1166" y="2674" width="64" height="64"/>
<object id="199" gid="16" x="2536.36" y="2533.7" width="64" height="64"/>
<object id="200" gid="16" x="2812.12" y="3045.82" width="64" height="64"/>
<object id="201" gid="16" x="3033.33" y="2824.61" width="64" height="64"/>
<object id="202" gid="16" x="2093.94" y="1467.03" width="64" height="64"/>
<object id="203" gid="16" x="654.545" y="664" width="64" height="64"/>
<object id="204" gid="16" x="172.727" y="376.121" width="64" height="64"/>
<object id="205" gid="16" x="360.606" y="176.121" width="64" height="64"/>
</objectgroup>
<objectgroup id="4" name="Barrier">
<properties>
<property name="type" value="barrier_and_shelter"/>
</properties>
<object id="1" x="234" y="730">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -74,78 178,330 250,258"/>
</object>
<object id="2" x="804" y="156">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -74,78 178,330 250,258"/>
</object>
<object id="3" x="230" y="410">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="250,-258 -74,78 178,330 504,0"/>
</object>
<object id="4" x="2470" y="2650">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="250,-258 -74,78 178,330 504,0"/>
</object>
<object id="5" x="1082.23" y="1537.08">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="548.339,-547.082 -94.2284,95.4898 230.657,408.918 867.772,-230"/>
</object>
<object id="6" x="1188" y="604">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -74,78 178,330 250,258"/>
</object>
<object id="7" x="889.741" y="770">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,22 -93.7407,98.1818 283.593,478 358.259,416.909"/>
</object>
<object id="8" x="691.741" y="1088">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-18,30 -93.7407,98.1818 169.593,356 238.259,288.909"/>
</object>
<object id="9" x="311.741" y="1916">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-22,30 -96.4077,103.515 867.593,1062 938.259,992.909"/>
</object>
<object id="10" x="629.741" y="1854">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-22,30 -93.7407,98.1818 361.593,548 430.259,480.909"/>
</object>
<object id="11" x="1015.74" y="1852">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 329.593,512 396.259,450.909"/>
</object>
<object id="12" x="1973.74" y="514">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 355.593,538 424.259,474.909"/>
</object>
<object id="13" x="2041.74" y="194">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 865.593,1048 934.259,992.909"/>
</object>
<object id="14" x="1975.74" y="896">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 317.593,508 382.259,440.909"/>
</object>
<object id="15" x="1917.74" y="1798">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 285.593,472 352.259,402.909"/>
</object>
<object id="16" x="1399.74" y="2564">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 228.259,278.909"/>
</object>
<object id="17" x="1595.74" y="2372">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="18" x="1791.74" y="2184">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="19" x="2301.74" y="1670">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="20" x="2489.74" y="1478">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="21" x="2683.74" y="1284">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="22" x="2109.74" y="2690">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="23" x="2809.74" y="1986">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-26,26 -93.7407,98.1818 161.593,348 226.259,280.909"/>
</object>
<object id="41" x="1069.7" y="415.152">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -66.6667,-72.7273 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="42" x="684.848" y="793.939">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -66.6667,-72.7273 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="43" x="360.606" y="1560.61">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="381.818,-375.758 309.091,-442.424 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="44" x="1445.45" y="475.758">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="381.818,-375.758 309.091,-442.424 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="45" x="354.545" y="2778.79">
<properties>
<property name="boundary_type" value="barrier"/>
<property name="debug_mark" value="1"/>
</properties>
<polyline points="123.031,-135.636 67.6364,-192.909 -323.636,198.303 -258.667,265.939"/>
</object>
<object id="46" x="481.818" y="2906.06">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="136.364,-130.303 63.6364,-190.909 -330.636,202.637 -266.667,260.606"/>
</object>
<object id="47" x="2915.15" y="224.242">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="136.364,-130.303 63.6364,-190.909 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="48" x="3048.48" y="351.515">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="136.364,-130.303 63.6364,-190.909 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="49" x="869.697" y="1754.55">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-3.0303,-3.0303 -66.6667,-66.6667 -330.303,204.97 -258.667,273.939"/>
</object>
<object id="50" x="2021.21" y="609.091">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-3.0303,-3.0303 -66.6667,-66.6667 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="51" x="2406.06" y="1309.09">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="118.182,-124.242 57.5758,-184.848 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="52" x="1454.55" y="2266.67">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="118.182,-124.242 57.5758,-184.848 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="53" x="1818.18" y="2648.48">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="200,-193.939 133.333,-260.606 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="54" x="2718.18" y="1754.55">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="200,-193.939 133.333,-260.606 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="55" x="2342.42" y="2596.97">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="60.6061,-72.7273 3.0303,-136.364 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="56" x="2793.94" y="2151.52">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="60.6061,-72.7273 3.0303,-136.364 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="57" x="69.697" y="57.5758">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -78.7879,-1.27898e-13 -66.6667,3121.21 1.13687e-13,3121.21"/>
</object>
<object id="59" x="3200" y="66.6667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -6.06061,-78.7879 -3215.15,-81.8182 -3221.21,9.09091"/>
</object>
<object id="60" x="3203.03" y="3221.21">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -6.06061,-78.7879 -3215.15,-81.8182 -3221.21,9.09091"/>
</object>
<object id="61" x="3209.09" y="27.2727">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -78.7879,-1.27898e-13 -66.6667,3121.21 1.13687e-13,3121.21"/>
</object>
</objectgroup>
<objectgroup id="5" name="LowScoreTreasure">
<object id="165" gid="13" x="1520" y="204" width="64" height="64" visible="0"/>
<object id="166" gid="13" x="1372" y="352" width="64" height="64" visible="0"/>
<object id="167" gid="13" x="1184" y="524" width="64" height="64" visible="0"/>
<object id="168" gid="13" x="544" y="1184" width="64" height="64" visible="0"/>
<object id="169" gid="13" x="294" y="1442" width="64" height="64" visible="0"/>
<object id="174" gid="16" x="2750" y="1234" width="64" height="64" visible="0"/>
<object id="175" gid="13" x="1562" y="1990" width="64" height="64" visible="0"/>
<object id="176" gid="13" x="1846" y="1718" width="64" height="64" visible="0"/>
<object id="177" gid="13" x="906" y="2438" width="64" height="64" visible="0"/>
<object id="178" gid="13" x="638" y="2186" width="64" height="64" visible="0"/>
<object id="179" gid="13" x="378" y="1878" width="64" height="64" visible="0"/>
<object id="180" gid="13" x="580" y="1672" width="64" height="64" visible="0"/>
<object id="181" gid="13" x="1518" y="634" width="64" height="64" visible="0"/>
<object id="182" gid="13" x="1818" y="410" width="64" height="64" visible="0"/>
<object id="183" gid="13" x="2308" y="360" width="64" height="64" visible="0"/>
<object id="184" gid="13" x="2524" y="576" width="64" height="64" visible="0"/>
<object id="186" gid="13" x="2726" y="790" width="64" height="64" visible="0"/>
<object id="187" gid="13" x="2928" y="972" width="64" height="64" visible="0"/>
<object id="192" gid="13" x="1484.85" y="3064" width="64" height="64" visible="0"/>
<object id="193" gid="13" x="1972.73" y="3048.85" width="64" height="64" visible="0"/>
<object id="194" gid="13" x="2006.06" y="2664" width="64" height="64" visible="0"/>
<object id="195" gid="13" x="312.121" y="2291.27" width="64" height="64" visible="0"/>
<object id="196" gid="13" x="503.03" y="2488.24" width="64" height="64" visible="0"/>
<object id="197" gid="13" x="606.061" y="3076.12" width="64" height="64" visible="0"/>
<object id="198" gid="13" x="833.333" y="2842.79" width="64" height="64" visible="0"/>
<object id="206" gid="13" x="2612.12" y="2064" width="64" height="64" visible="0"/>
<object id="207" gid="13" x="3030.3" y="1985.21" width="64" height="64" visible="0"/>
</objectgroup>
<objectgroup id="6" name="GuardTower">
<object id="188" gid="17" x="1429.33" y="1781.33" width="300" height="300" visible="0"/>
<object id="210" gid="17" x="1784.67" y="1414" width="300" height="300"/>
<object id="211" gid="17" x="2786" y="2783.33" width="300" height="300"/>
<object id="212" gid="17" x="532.667" y="543.333" width="300" height="300"/>
</objectgroup>
</map>

View File

@@ -1,5 +0,0 @@
{
"ver": "2.0.2",
"uuid": "7fd0b77d-0736-4e00-89eb-7111f07ed4f1",
"subMetas": {}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="isometric" renderorder="right-down" width="50" height="50" tilewidth="64" tileheight="64" infinite="0" nextlayerid="11" nextobjectid="213">
<map version="1.2" tiledversion="1.2.3" orientation="isometric" renderorder="right-down" width="50" height="50" tilewidth="64" tileheight="64" infinite="0" nextlayerid="11" nextobjectid="218">
<tileset firstgid="1" source="Tile_W64_H64_S01.tsx"/>
<tileset firstgid="17" source="Tile_W300_H300_S01.tsx"/>
<layer id="1" name="GroundFloor" width="50" height="50" locked="1">
@@ -33,7 +33,7 @@
<object id="204" gid="16" x="172.727" y="376.121" width="64" height="64"/>
<object id="205" gid="16" x="360.606" y="176.121" width="64" height="64"/>
</objectgroup>
<objectgroup id="4" name="Barrier" locked="1">
<objectgroup id="4" name="Barrier">
<properties>
<property name="type" value="barrier_and_shelter"/>
</properties>
@@ -271,29 +271,29 @@
</properties>
<polyline points="60.6061,-72.7273 3.0303,-136.364 -330.303,196.97 -266.667,260.606"/>
</object>
<object id="57" x="69.697" y="57.5758">
<object id="213" x="1854.55" y="-1351.52">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -78.7879,-1.27898e-13 -66.6667,3121.21 1.13687e-13,3121.21"/>
<polyline points="0,0 -3190.91,3215.15 -3115.15,3290.91 48.4848,54.5455"/>
</object>
<object id="59" x="3200" y="66.6667">
<object id="214" x="-572.727" y="1257.58">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -6.06061,-78.7879 -3215.15,-81.8182 -3221.21,9.09091"/>
<polyline points="0,0 2575.76,2624.24 2472.73,2727.27 -136.364,112.121"/>
</object>
<object id="60" x="3203.03" y="3221.21">
<object id="216" x="1190.91" y="-584.848">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -6.06061,-78.7879 -3215.15,-81.8182 -3221.21,9.09091"/>
<polyline points="0,0 2657.58,2627.27 2818.18,2466.67 172.727,-166.667"/>
</object>
<object id="61" x="3209.09" y="27.2727">
<object id="217" x="1887.88" y="3748.48">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -78.7879,-1.27898e-13 -66.6667,3121.21 1.13687e-13,3121.21"/>
<polyline points="0,0 221.212,227.273 2087.88,-1627.27 1842.42,-1872.73"/>
</object>
</objectgroup>
<objectgroup id="5" name="LowScoreTreasure">

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.1",
"uuid": "33d33d2f-5ca7-4677-9c82-4ced71aa0f8a",
"uuid": "6955a517-3fb6-4777-b8b0-74dbd76f18e4",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.1",
"uuid": "f9460fe9-26ad-4e4b-8a3c-c97bef705a71",
"uuid": "51c54820-d753-4be8-a855-5760eed8f7ef",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}

View File

@@ -1,6 +1,6 @@
{
"ver": "2.3.3",
"uuid": "b711d3f8-b9f4-41ab-a7e4-6562992fb65e",
"uuid": "136d09e9-c33c-45dc-abb7-e367d730c814",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
@@ -11,8 +11,8 @@
"subMetas": {
"Tile_W256_H128_S01": {
"ver": "1.0.4",
"uuid": "2d6c9481-6bd5-4c19-b468-529d027226c3",
"rawTextureUuid": "b711d3f8-b9f4-41ab-a7e4-6562992fb65e",
"uuid": "7acc48f5-d9c9-4438-8794-57a85590bd97",
"rawTextureUuid": "136d09e9-c33c-45dc-abb7-e367d730c814",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "df775ad6-885e-411b-8b2a-f4bcf70b4fbf",
"subMetas": {}
}

View File

@@ -1,6 +1,6 @@
{
"ver": "2.3.3",
"uuid": "757f31be-7ad1-42dc-bd6b-2b4c7652e457",
"uuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
@@ -11,8 +11,8 @@
"subMetas": {
"Tile_W256_H256_S01": {
"ver": "1.0.4",
"uuid": "0d9aafd3-d738-473c-95b8-bf577b78f03d",
"rawTextureUuid": "757f31be-7ad1-42dc-bd6b-2b4c7652e457",
"uuid": "4a23290b-bf5a-4849-ac19-6ebd4b7daa59",
"rawTextureUuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "81508f64-031d-4d00-9aa0-8e9841907d0a",
"subMetas": {}
}

View File

@@ -1,6 +1,6 @@
{
"ver": "2.3.3",
"uuid": "2385ca17-af24-4122-a271-785f3010d7f2",
"uuid": "74245e28-6cec-4960-ac41-5b482ad8fd13",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
@@ -11,8 +11,8 @@
"subMetas": {
"Tile_W256_H256_S02": {
"ver": "1.0.4",
"uuid": "34e3f356-e65d-4745-8f70-7706aa4083f8",
"rawTextureUuid": "2385ca17-af24-4122-a271-785f3010d7f2",
"uuid": "8fc46c1f-6fb4-4290-99f3-b773b92312b7",
"rawTextureUuid": "74245e28-6cec-4960-ac41-5b482ad8fd13",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "4e822a8b-f3be-4b83-b61d-9e04c56c5eba",
"subMetas": {}
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.2",
"uuid": "a9a02975-8df5-42a1-bc25-b0ff7c1c03ba",
"subMetas": {}
}

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,6 +1,6 @@
{
"ver": "2.3.3",
"uuid": "0a5e9b39-6a98-4b45-9ad5-6df3effec2ef",
"uuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
@@ -11,8 +11,8 @@
"subMetas": {
"Tile_W300_H300_S01": {
"ver": "1.0.4",
"uuid": "acb30663-c0cf-4727-a046-26e40610c49f",
"rawTextureUuid": "0a5e9b39-6a98-4b45-9ad5-6df3effec2ef",
"uuid": "66b49304-7b5b-442c-92a5-d2b368abf659",
"rawTextureUuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "8c8beea4-faa3-4270-aa27-dc5f2b7766c2",
"subMetas": {}
}

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -1,6 +1,6 @@
{
"ver": "2.3.3",
"uuid": "5e68ce04-f41f-4ab0-a55c-608542e5638a",
"uuid": "dd454e0f-4474-4801-8e86-ba134f6293a9",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
@@ -11,8 +11,8 @@
"subMetas": {
"Tile_W64_H64_S01": {
"ver": "1.0.4",
"uuid": "610a5bd5-c643-426e-a6e8-63acc5c516d9",
"rawTextureUuid": "5e68ce04-f41f-4ab0-a55c-608542e5638a",
"uuid": "e287ce95-457a-48b3-9c94-314bbcb69009",
"rawTextureUuid": "dd454e0f-4474-4801-8e86-ba134f6293a9",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "c63de1bc-6d0e-467e-848b-f5c021a1b950",
"subMetas": {}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="isometric" renderorder="right-down" width="50" height="50" tilewidth="64" tileheight="64" infinite="0" nextlayerid="11" nextobjectid="215">
<tileset firstgid="1" source="Tile_W64_H64_S01.tsx"/>
<tileset firstgid="17" source="Tile_W300_H300_S01.tsx"/>
<layer id="1" name="GroundFloor" width="50" height="50" locked="1">
<data encoding="base64" compression="zlib">
eJztz0ENACAMALGR4F8zNspyjwronZm7xPlcD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPSw9LD0sPy7bHBg/ldQwR
</data>
</layer>
<objectgroup id="2" name="PlayerStartingPos">
<object id="135" x="1513.33" y="1996">
<point/>
</object>
<object id="137" x="2270" y="1640">
<point/>
</object>
</objectgroup>
<layer id="3" name="FirsrtFloor" width="50" height="50">
<data encoding="base64" compression="zlib">
eJzt1jEKgDAQRNE0GtD739fGaQIhqJHdCf81aSz2oyspBQAAAADWcUYPMIEaaugU37TvwbGl9y05tYz2wWFfaMiBhhyNKzRs99n7lzo0SG1OcWqQtsWxQdSwD57L3CCjO4dDg7zd+Yye7nxmajlCp5jD6Y4OAAAA4H8XE6wBrA==
</data>
</layer>
<objectgroup id="4" name="Barrier">
<properties>
<property name="type" value="barrier_and_shelter"/>
</properties>
<object id="5" x="1082.23" y="1537.08">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="548.339,-547.082 -94.2284,95.4898 230.657,408.918 867.772,-230"/>
</object>
<object id="213" x="1044" y="1852">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="1101.33,-342 470.926,284.848 526.26,339.333 1152.93,-285.091"/>
</object>
<object id="214" x="988" y="1632">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-3,1 -70,72 514,640 588,572"/>
</object>
</objectgroup>
</map>

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.2",
"uuid": "9b8777b4-8725-46ea-96c0-d580e9f023d1",
"subMetas": {}
}

View File

@@ -0,0 +1,26 @@
syntax = "proto3";
option go_package = "dnmshared/sharedprotos"; // here "./" corresponds to the "--go_out" value in "protoc" command
package sharedprotos;
message Direction {
int32 dx = 1;
int32 dy = 2;
}
message Vec2D {
double x = 1;
double y = 2;
}
message Polygon2D {
Vec2D anchor = 1;
repeated Vec2D points = 2;
}
message Vec2DList {
repeated Vec2D eles = 1;
}
message Polygon2DList {
repeated Polygon2D eles = 1;
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "2ba698f8-1af7-4c47-9d43-b2730b62c692",
"subMetas": {}
}

View File

@@ -1,35 +1,13 @@
syntax = "proto3";
option go_package = "."; // "./" corresponds to the "--go_out" value in "protoc" command
option go_package = "battle_srv/protos"; // here "./" corresponds to the "--go_out" value in "protoc" command
package treasurehunterx;
message Direction {
int32 dx = 1;
int32 dy = 2;
}
message Vec2D {
double x = 1;
double y = 2;
}
message Polygon2D {
Vec2D Anchor = 1;
repeated Vec2D Points = 2;
}
message Vec2DList {
repeated Vec2D vec2DList = 1;
}
message Polygon2DList {
repeated Polygon2D polygon2DList = 1;
}
package protos;
import "geometry.proto"; // The import path here is only w.r.t. the proto file, not the Go package.
message BattleColliderInfo {
string stageName = 1;
map<string, Vec2DList> strToVec2DListMap = 2;
map<string, Polygon2DList> strToPolygon2DListMap = 3;
map<string, sharedprotos.Vec2DList> strToVec2DListMap = 2;
map<string, sharedprotos.Polygon2DList> strToPolygon2DListMap = 3;
int32 stageDiscreteW = 4;
int32 stageDiscreteH = 5;
int32 stageTileW = 6;
@@ -46,14 +24,19 @@ message BattleColliderInfo {
int32 inputFrameUpsyncDelayTolerance = 16;
int32 maxChasingRenderFramesPerUpdate = 17;
int32 playerBattleState = 18;
double rollbackEstimatedDtMillis = 19;
int64 rollbackEstimatedDtNanos = 20;
double worldToVirtualGridRatio = 21;
double virtualGridToWorldRatio = 22;
}
message Player {
message PlayerDownsync {
int32 id = 1;
double x = 2;
double y = 3;
Direction dir = 4;
double speed = 5;
int32 virtualGridX = 2;
int32 virtualGridY = 3;
sharedprotos.Direction dir = 4;
int32 speed = 5; // in terms of virtual grid units
int32 battleState = 6;
int32 lastMoveGmtMillis = 7;
int32 score = 10;
@@ -61,12 +44,13 @@ message Player {
int32 joinIndex = 12;
}
message PlayerMeta {
message PlayerDownsyncMeta {
int32 id = 1;
string name = 2;
string displayName = 3;
string avatar = 4;
int32 joinIndex = 5;
double colliderRadius = 6;
}
message InputFrameUpsync {
@@ -86,9 +70,9 @@ message HeartbeatUpsync {
message RoomDownsyncFrame {
int32 id = 1;
map<int32, Player> players = 2;
map<int32, PlayerDownsync> players = 2;
int64 countdownNanos = 3;
map<int32, PlayerMeta> playerMetas = 4;
map<int32, PlayerDownsyncMeta> playerMetas = 4;
}
message WsReq {

View File

@@ -100,7 +100,7 @@
"__id__": 1
},
"_children": [],
"_active": false,
"_active": true,
"_components": [
{
"__id__": 3
@@ -119,8 +119,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 93.36,
"height": 40
"width": 46.68,
"height": 27.72
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -132,7 +132,7 @@
"ctor": "Float64Array",
"array": [
-5,
101,
50,
0,
0,
0,
@@ -164,12 +164,16 @@
"__id__": 2
},
"_enabled": true,
"_materials": [],
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "(0, 0)",
"_N$string": "(0, 0)",
"_fontSize": 40,
"_lineHeight": 40,
"_fontSize": 20,
"_lineHeight": 22,
"_enableWrapText": true,
"_N$file": null,
"_isSystemFontUsed": true,
@@ -557,15 +561,13 @@
},
"_enabled": true,
"animComp": null,
"baseSpeed": 50,
"speed": 50,
"lastMovedAt": 0,
"eps": 0.1,
"magicLeanLowerBound": 0.414,
"magicLeanUpperBound": 2.414,
"arrowTipNode": {
"__id__": 8
},
"coordLabel": {
"__id__": 3
},
"_id": ""
},
{

View File

@@ -100,7 +100,7 @@
"__id__": 1
},
"_children": [],
"_active": false,
"_active": true,
"_components": [
{
"__id__": 3
@@ -119,8 +119,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 93.36,
"height": 40
"width": 46.68,
"height": 27.72
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -132,7 +132,7 @@
"ctor": "Float64Array",
"array": [
-5,
101,
50,
0,
0,
0,
@@ -164,12 +164,16 @@
"__id__": 2
},
"_enabled": true,
"_materials": [],
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_useOriginalSize": false,
"_string": "(0, 0)",
"_N$string": "(0, 0)",
"_fontSize": 40,
"_lineHeight": 40,
"_fontSize": 20,
"_lineHeight": 22,
"_enableWrapText": true,
"_N$file": null,
"_isSystemFontUsed": true,
@@ -557,15 +561,13 @@
},
"_enabled": true,
"animComp": null,
"baseSpeed": 50,
"speed": 50,
"lastMovedAt": 0,
"eps": 0.1,
"magicLeanLowerBound": 0.414,
"magicLeanUpperBound": 2.414,
"arrowTipNode": {
"__id__": 8
},
"coordLabel": {
"__id__": 3
},
"_id": ""
},
{

View File

@@ -304,7 +304,6 @@
"__id__": 5
},
"_enabled": true,
"useDiffFrameAlgo": true,
"canvasNode": {
"__id__": 2
},
@@ -317,22 +316,9 @@
"player2Prefab": {
"__uuid__": "1f479636-9eb8-4612-8f97-371964d6eae3"
},
"treasurePrefab": {
"__uuid__": "ec3f3234-9b84-43c2-a3cd-58b924cce8e5"
},
"trapPrefab": {
"__uuid__": "9f340a31-ddfa-46c2-94c7-d11615aedcb1"
},
"speedShoePrefab": null,
"polygonBoundaryBarrierPrefab": {
"__uuid__": "4154eec0-d644-482f-a889-c00ae6b69958"
},
"polygonBoundaryShelterPrefab": {
"__uuid__": "f820a6ec-e7a9-46cf-9b8a-331aa3e21487"
},
"polygonBoundaryShelterZReducerPrefab": {
"__uuid__": "a36d024b-a979-4d18-b089-19af313ffb82"
},
"keyboardInputControllerNode": {
"__id__": 8
},
@@ -351,9 +337,6 @@
"countdownLabel": {
"__id__": 30
},
"trapBulletPrefab": {
"__uuid__": "7673e0e4-bebd-4caa-8a10-a6e1e86f1b2f"
},
"resultPanelPrefab": {
"__uuid__": "c4cfe3bd-c59e-4d5b-95cb-c933b120e184"
},
@@ -369,13 +352,12 @@
"playersInfoPrefab": {
"__uuid__": "b4e519f4-e698-4403-9ff2-47b8dacb077e"
},
"guardTowerPrefab": {
"__uuid__": "31a63530-7811-45bc-a4ee-571faf917e35"
},
"forceBigEndianFloatingNumDecoding": false,
"backgroundMapTiledIns": {
"__id__": 4
},
"renderFrameIdLagTolerance": 4,
"teleportEps1D": 0.001,
"_id": "d12gkAmppNlIzqcRDELa91"
},
{
@@ -557,7 +539,7 @@
"array": [
0,
0,
217.54856073274254,
342.9460598986377,
0,
0,
0,
@@ -1579,6 +1561,7 @@
"mapNode": {
"__id__": 5
},
"speed": 5000,
"_id": "76ImpM7XtPSbiLHDXdsJa+"
},
{
@@ -1631,7 +1614,6 @@
"joyStickEps": 0.1,
"magicLeanLowerBound": 0.414,
"magicLeanUpperBound": 2.414,
"pollerFps": 24,
"linearScaleFacBase": 1,
"minScale": 1,
"maxScale": 2,

View File

@@ -440,7 +440,7 @@
"array": [
0,
0,
216.50635094610968,
237.35666382819272,
0,
0,
0,

View File

@@ -6,30 +6,10 @@ module.export = cc.Class({
type: cc.Animation,
default: null,
},
baseSpeed: {
type: cc.Float,
default: 50,
},
speed: {
type: cc.Float,
default: 50
},
lastMovedAt: {
type: cc.Float,
default: 0 // In "GMT milliseconds"
},
eps: {
default: 0.10,
type: cc.Float
},
magicLeanLowerBound: {
default: 0.414, // Tangent of (PI/8).
type: cc.Float
},
magicLeanUpperBound: {
default: 2.414, // Tangent of (3*PI/8).
type: cc.Float
},
}
},
// LIFE-CYCLE CALLBACKS:
@@ -44,14 +24,14 @@ module.export = cc.Class({
onLoad() {
const self = this;
self.clips = {
'01': 'Top',
'0-1': 'Bottom',
'02': 'Top',
'0-2': 'Bottom',
'-20': 'Left',
'20': 'Right',
'-21': 'TopLeft',
'21': 'TopRight',
'-2-1': 'BottomLeft',
'2-1': 'BottomRight'
'-11': 'TopLeft',
'11': 'TopRight',
'-1-1': 'BottomLeft',
'1-1': 'BottomRight'
};
const canvasNode = self.mapNode.parent;
self.mapIns = self.mapNode.getComponent("Map");
@@ -70,7 +50,7 @@ module.export = cc.Class({
this.activeDirection = newScheduledDirection;
this.activeDirection = newScheduledDirection;
const clipKey = newScheduledDirection.dx.toString() + newScheduledDirection.dy.toString();
const clips = (this.attacked ? this.attackedClips : this.clips);
const clips = (this.attacked ? this.attackedClips : this.clips);
let clip = clips[clipKey];
if (!clip) {
// Keep playing the current anim.
@@ -86,11 +66,9 @@ module.export = cc.Class({
}
},
update(dt) {
},
update(dt) {},
lateUpdate(dt) {
},
lateUpdate(dt) {},
_generateRandomDirection() {
return ALL_DISCRETE_DIRECTIONS_CLOCKWISE[Math.floor(Math.random() * ALL_DISCRETE_DIRECTIONS_CLOCKWISE.length)];
@@ -117,16 +95,16 @@ module.export = cc.Class({
updateSpeed(proposedSpeed) {
if (0 == proposedSpeed && 0 < this.speed) {
this.startFrozenDisplay();
}
this.startFrozenDisplay();
}
if (0 < proposedSpeed && 0 == this.speed) {
this.stopFrozenDisplay();
}
this.speed = proposedSpeed;
this.stopFrozenDisplay();
}
this.speed = proposedSpeed;
},
startFrozenDisplay() {
const self = this;
const self = this;
self.attacked = true;
},

View File

@@ -6,7 +6,10 @@ cc.Class({
type: cc.Node,
default: null
},
speed: {
type: cc.Float,
default: 500
},
},
onLoad () {
@@ -26,6 +29,9 @@ cc.Class({
if (!selfPlayerRichInfo) return;
const selfPlayerNode = selfPlayerRichInfo.node;
if (!selfPlayerNode) return;
self.mainCamera.node.setPosition(selfPlayerNode.position);
const pDiff = selfPlayerNode.position.sub(self.mainCamera.node.position);
pDiff.normalizeSelf();
const newCamPos = self.mainCamera.node.position.add(pDiff.mul(dt*self.speed));
self.mainCamera.node.setPosition(newCamPos);
}
});

View File

@@ -1,6 +1,8 @@
const i18n = require('LanguageData');
i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field
window.pb = require("./modules/room_downsync_frame_proto_bundle.forcemsg");
cc.Class({
extends: cc.Component,
@@ -67,19 +69,6 @@ cc.Class({
onLoad() {
//kobako: 腾讯统计代码
//WARN: 打包到微信小游戏的时候会导致出错
/*
(function() {
var mta = document.createElement("script");
mta.src = "//pingjs.qq.com/h5/stats.js?v2.0.4";
mta.setAttribute("name", "MTAH5");
mta.setAttribute("sid", "500674632");
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(mta, s);
})();
*/
window.atFirstLocationHref = window.location.href.split('#')[0];
const self = this;
self.getRetCodeList();
@@ -97,9 +86,7 @@ cc.Class({
self.smsLoginCaptchaLabel.active = true;
self.loginButton.active = true;
self.checkPhoneNumber = self.checkPhoneNumber.bind(self);
self.checkIntAuthTokenExpire = self.checkIntAuthTokenExpire.bind(self);
self.checkCaptcha = self.checkCaptcha.bind(self);
self.onLoginButtonClicked = self.onLoginButtonClicked.bind(self);
self.onSMSCaptchaGetButtonClicked = self.onSMSCaptchaGetButtonClicked.bind(self);
self.smsLoginCaptchaButton.on('click', self.onSMSCaptchaGetButtonClicked);
@@ -112,28 +99,16 @@ cc.Class({
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
cc.loader.loadRes("pbfiles/room_downsync_frame", function(err, textAsset /* cc.TextAsset */ ) {
if (err) {
cc.error(err.message || err);
return;
self.checkIntAuthTokenExpire().then(
(intAuthToken) => {
console.log("Successfully found `intAuthToken` in local cache");
self.useTokenLogin(intAuthToken);
},
() => {
console.warn("Failed to find `intAuthToken` in local cache");
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
// Otherwise, `window.RoomDownsyncFrame` is already assigned.
let protoRoot = new protobuf.Root;
window.protobuf.parse(textAsset.text, protoRoot);
window.RoomDownsyncFrame = protoRoot.lookupType("treasurehunterx.RoomDownsyncFrame");
window.BattleColliderInfo = protoRoot.lookupType("treasurehunterx.BattleColliderInfo");
window.WsReq = protoRoot.lookupType("treasurehunterx.WsReq");
window.WsResp = protoRoot.lookupType("treasurehunterx.WsResp");
self.checkIntAuthTokenExpire().then(
() => {
const intAuthToken = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken;
self.useTokenLogin(intAuthToken);
},
() => {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
);
});
);
},
getRetCodeList() {
@@ -221,11 +196,28 @@ cc.Class({
checkIntAuthTokenExpire() {
return new Promise((resolve, reject) => {
if (!cc.sys.localStorage.getItem('selfPlayer')) {
console.warn("Couldn't find selfPlayer key in local cache");
reject();
return;
}
const selfPlayer = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
(selfPlayer.intAuthToken && new Date().getTime() < selfPlayer.expiresAt) ? resolve() : reject();
if (null == selfPlayer) {
console.warn("Couldn't find selfPlayer object in local cache");
reject();
return;
}
if (null == selfPlayer.intAuthToken) {
console.warn("Couldn't find selfPlayer object with key `intAuthToken` in local cache");
reject();
return;
}
if (new Date().getTime() > selfPlayer.expiresAt) {
console.warn("Couldn't find unexpired selfPlayer `intAuthToken` in local cache");
reject();
return;
}
resolve(selfPlayer.intAuthToken);
})
},
@@ -278,13 +270,15 @@ cc.Class({
intAuthToken: _intAuthToken
},
success: function(resp) {
console.log("Login attempt `useTokenLogin` succeeded.");
self.onLoggedIn(resp);
},
error: function(xhr, status, errMsg) {
console.log("Login attempt `useTokenLogin` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`.");
console.warn("Login attempt `useTokenLogin` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`.");
window.clearBoundRoomIdInBothVolatileAndPersistentStorage()
},
timeout: function() {
console.warn("Login attempt `useTokenLogin` timed out, about to enable interactive controls.");
self.enableInteractiveControls(true);
},
});
@@ -335,7 +329,7 @@ cc.Class({
onLoggedIn(res) {
const self = this;
cc.log(`OnLoggedIn ${JSON.stringify(res)}.`)
console.log("OnLoggedIn ", JSON.stringify(res))
if (res.ret === self.retCodeDict.OK) {
self.enableInteractiveControls(false);
const date = Number(res.expiresAt);
@@ -360,6 +354,7 @@ cc.Class({
);
cc.director.loadScene('default_map');
} else {
console.log("OnLoggedIn failed, about to remove `selfPlayer` in local cache.")
cc.sys.localStorage.removeItem("selfPlayer");
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
self.enableInteractiveControls(true);

View File

@@ -111,25 +111,29 @@ cc.Class({
type: cc.Integer,
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
},
jigglingEps1D: {
type: cc.Float,
default: 1e-3
},
},
_inputFrameIdDebuggable(inputFrameId) {
return (0 == inputFrameId % 10);
},
dumpToRenderCache: function(roomDownsyncFrame) {
dumpToRenderCache: function(rdf) {
const self = this;
const minToKeepRenderFrameId = self.lastAllConfirmedRenderFrameId;
while (0 < self.recentRenderCache.cnt && self.recentRenderCache.stFrameId < minToKeepRenderFrameId) {
self.recentRenderCache.pop();
}
const ret = self.recentRenderCache.setByFrameId(roomDownsyncFrame, roomDownsyncFrame.id);
const ret = self.recentRenderCache.setByFrameId(rdf, rdf.id);
return ret;
},
dumpToInputCache: function(inputFrameDownsync) {
const self = this;
let minToKeepInputFrameId = self._convertToInputFrameId(self.lastAllConfirmedRenderFrameId, self.inputDelayFrames); // [WARNING] This could be different from "self.lastAllConfirmedInputFrameId". We'd like to keep the corresponding inputFrame for "self.lastAllConfirmedRenderFrameId" such that a rollback could place "self.chaserRenderFrameId = self.lastAllConfirmedRenderFrameId" for the worst case incorrect prediction.
let minToKeepInputFrameId = self._convertToInputFrameId(self.lastAllConfirmedRenderFrameId, self.inputDelayFrames); // [WARNING] This could be different from "self.lastAllConfirmedInputFrameId". We'd like to keep the corresponding delayedInputFrame for "self.lastAllConfirmedRenderFrameId" such that a rollback could place "self.chaserRenderFrameId = self.lastAllConfirmedRenderFrameId" for the worst case incorrect prediction.
if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) {
minToKeepInputFrameId = self.lastAllConfirmedInputFrameId;
}
@@ -170,9 +174,16 @@ cc.Class({
}
const joinIndex = self.selfPlayerInfo.joinIndex;
const discreteDir = self.ctrl.getDiscretizedDirection();
const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId);
const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]);
// If "forceConfirmation" is active on backend, we shouldn't override the already downsynced "inputFrameDownsync"s.
const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId);
if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) {
return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]];
}
const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice());
const discreteDir = self.ctrl.getDiscretizedDirection();
prefabbedInputList[(joinIndex - 1)] = discreteDir.encodedIdx;
const prefabbedInputFrameDownsync = {
inputFrameId: inputFrameId,
@@ -182,7 +193,6 @@ cc.Class({
self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]);
return [previousSelfInput, discreteDir.encodedIdx];
},
@@ -219,7 +229,7 @@ cc.Class({
inputFrameUpsyncBatch.push(inputFrameUpsync);
}
}
const reqData = window.WsReq.encode({
const reqData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
playerId: self.selfPlayerInfo.id,
act: window.UPSYNC_MSG_ACT_PLAYER_CMD,
@@ -249,8 +259,8 @@ cc.Class({
if (null != window.handleBattleColliderInfo) {
window.handleBattleColliderInfo = null;
}
if (null != window.handleClientSessionCloseOrError) {
window.handleClientSessionCloseOrError = null;
if (null != window.handleClientSessionError) {
window.handleClientSessionError = null;
}
},
@@ -320,12 +330,10 @@ cc.Class({
self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others".
self.recentInputCache = new RingBuffer(1024);
self.latestCollisionSys = new collisions.Collisions();
self.chaserCollisionSys = new collisions.Collisions();
self.collisionSys = new collisions.Collisions();
self.collisionBarrierIndexPrefix = (1 << 16); // For tracking the movements of barriers, though not yet actually used
self.latestCollisionSysMap = new Map();
self.chaserCollisionSysMap = new Map();
self.collisionSysMap = new Map();
self.transitToState(ALL_MAP_STATES.VISUAL);
@@ -344,9 +352,11 @@ cc.Class({
window.mapIns = self;
window.forceBigEndianFloatingNumDecoding = self.forceBigEndianFloatingNumDecoding;
self.showCriticalCoordinateLabels = false;
console.warn("+++++++ Map onLoad()");
window.handleClientSessionCloseOrError = function() {
console.warn('+++++++ Common handleClientSessionCloseOrError()');
window.handleClientSessionError = function() {
console.warn('+++++++ Common handleClientSessionError()');
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
console.log("Battled ended by settlement");
@@ -414,11 +424,15 @@ cc.Class({
self.inputScaleFrames = parsedBattleColliderInfo.inputScaleFrames;
self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance;
self.rollbackEstimatedDt = 1.0 / parsedBattleColliderInfo.serverFps;
self.rollbackEstimatedDtMillis = 1000.0 * self.rollbackEstimatedDt;
self.rollbackEstimatedDtToleranceMillis = self.rollbackEstimatedDtMillis / 1000.0;
self.battleDurationNanos = parsedBattleColliderInfo.battleDurationNanos;
self.rollbackEstimatedDt = parsedBattleColliderInfo.rollbackEstimatedDt;
self.rollbackEstimatedDtMillis = parsedBattleColliderInfo.rollbackEstimatedDtMillis;
self.rollbackEstimatedDtNanos = parsedBattleColliderInfo.rollbackEstimatedDtNanos;
self.maxChasingRenderFramesPerUpdate = parsedBattleColliderInfo.maxChasingRenderFramesPerUpdate;
self.worldToVirtualGridRatio = parsedBattleColliderInfo.worldToVirtualGridRatio;
self.virtualGridToWorldRatio = parsedBattleColliderInfo.virtualGridToWorldRatio;
const tiledMapIns = self.node.getComponent(cc.TiledMap);
const fullPathOfTmxFile = cc.js.formatStr("map/%s/map", parsedBattleColliderInfo.stageName);
@@ -461,16 +475,44 @@ cc.Class({
const x0 = boundaryObj[0].x,
y0 = boundaryObj[0].y;
let pts = [];
// TODO: Simplify this redundant coordinate conversion within "extractBoundaryObjects", but since this routine is only called once per battle, not urgent.
for (let i = 0; i < boundaryObj.length; ++i) {
pts.push([boundaryObj[i].x - x0, boundaryObj[i].y - y0]);
const dx = boundaryObj[i].x - x0;
const dy = boundaryObj[i].y - y0;
pts.push([dx, dy]);
/*
if (self.showCriticalCoordinateLabels) {
const barrierVertLabelNode = new cc.Node();
switch (i % 4) {
case 0:
barrierVertLabelNode.color = cc.Color.RED;
break;
case 1:
barrierVertLabelNode.color = cc.Color.GRAY;
break;
case 2:
barrierVertLabelNode.color = cc.Color.BLACK;
break;
default:
barrierVertLabelNode.color = cc.Color.MAGENTA;
break;
}
barrierVertLabelNode.setPosition(cc.v2(x0+0.95*dx, y0+0.5*dy));
const barrierVertLabel = barrierVertLabelNode.addComponent(cc.Label);
barrierVertLabel.fontSize = 20;
barrierVertLabel.lineHeight = 22;
barrierVertLabel.string = `(${boundaryObj[i].x.toFixed(1)}, ${boundaryObj[i].y.toFixed(1)})`;
safelyAddChild(self.node, barrierVertLabelNode);
setLocalZOrder(barrierVertLabelNode, 5);
barrierVertLabelNode.active = true;
}
const newBarrierLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
const newBarrierChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts);
*/
}
const newBarrier = self.collisionSys.createPolygon(x0, y0, pts);
// console.log("Created barrier: ", newBarrier);
++barrierIdCounter;
const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter);
self.latestCollisionSysMap.set(collisionBarrierIndex, newBarrierLatest);
self.chaserCollisionSysMap.set(collisionBarrierIndex, newBarrierChaser);
self.collisionSysMap.set(collisionBarrierIndex, newBarrier);
}
self.selfPlayerInfo = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
@@ -493,7 +535,7 @@ cc.Class({
self.backgroundMapTiledIns.node.setContentSize(newBackgroundMapSize.width * newBackgroundMapTileSize.width, newBackgroundMapSize.height * newBackgroundMapTileSize.height);
self.backgroundMapTiledIns.node.setPosition(cc.v2(0, 0));
const reqData = window.WsReq.encode({
const reqData = window.pb.protos.WsReq.encode({
msgId: Date.now(),
act: window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK,
}).finish();
@@ -567,6 +609,7 @@ cc.Class({
if (rdf.id < self.lastAllConfirmedRenderFrameId) {
return window.RING_BUFF_FAILED_TO_SET;
}
const dumpRenderCacheRet = self.dumpToRenderCache(rdf);
if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) {
console.error("Something is wrong while setting the RingBuffer by frameId!");
@@ -575,20 +618,18 @@ cc.Class({
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START < rdf.id && window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet) {
/*
Don't change
- lastAllConfirmedRenderFrameId, it's updated only in "rollbackAndChase > _createRoomDownsyncFrameLocally" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
- chaserRenderFrameId, it's updated only in "onInputFrameDownsyncBatch" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
- lastAllConfirmedRenderFrameId, it's updated only in "rollbackAndChase" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
- chaserRenderFrameId, it's updated only in "rollbackAndChase & onInputFrameDownsyncBatch" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
*/
return dumpRenderCacheRet;
}
// The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
console.log('On battle started or resynced! renderFrameId=', rdf.id);
self.renderFrameId = rdf.id;
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId".
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id;
// The logic below applies to ( || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) {
console.log('On battle started! renderFrameId=', rdf.id);
} else {
console.log('On battle resynced! renderFrameId=', rdf.id);
}
const players = rdf.players;
const playerMetas = rdf.playerMetas;
@@ -601,6 +642,12 @@ cc.Class({
playersInfoScriptIns.updateData(playerMeta);
}
self.renderFrameId = rdf.id;
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId".
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id;
if (null != rdf.countdownNanos) {
self.countdownNanos = rdf.countdownNanos;
}
@@ -630,7 +677,7 @@ cc.Class({
return true;
},
onInputFrameDownsyncBatch(batch, dumpRenderCacheRet /* second param is default to null */ ) {
onInputFrameDownsyncBatch(batch) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState
&& ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) {
@@ -644,49 +691,44 @@ cc.Class({
if (inputFrameDownsyncId < self.lastAllConfirmedInputFrameId) {
continue;
}
if (window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) {
// Deliberately left blank, in this case "chaserRenderFrameId" is already reset to proper value.
} else {
const inputFrameIdConsecutive = (inputFrameDownsyncId == self.lastAllConfirmedInputFrameId + 1);
const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId);
if (null == localInputFrame && false == inputFrameIdConsecutive) {
throw "localInputFrame not existing and is NOT CONSECUTIVELY EXTENDING recentInputCache: inputFrameDownsyncId=" + inputFrameDownsyncId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId + ", recentInputCache=" + self._stringifyRecentInputCache(false);
} else if (null == firstPredictedYetIncorrectInputFrameId && null != localInputFrame && !self.equalInputLists(localInputFrame.inputList, inputFrameDownsync.inputList)) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId);
if (null != localInputFrame
&&
null == firstPredictedYetIncorrectInputFrameId
&&
!self.equalInputLists(localInputFrame.inputList, inputFrameDownsync.inputList)
) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1;
self.dumpToInputCache(inputFrameDownsync);
}
if (null != firstPredictedYetIncorrectInputFrameId) {
const inputFrameId1 = firstPredictedYetIncorrectInputFrameId;
const renderFrameId1 = self._convertToFirstUsedRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId"
if (renderFrameId1 < self.renderFrameId) {
/*
A typical case is as follows.
--------------------------------------------------------
[self.lastAllConfirmedRenderFrameId] : 22
if (null == firstPredictedYetIncorrectInputFrameId) return;
const inputFrameId1 = firstPredictedYetIncorrectInputFrameId;
const renderFrameId1 = self._convertToFirstUsedRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId"
if (renderFrameId1 >= self.renderFrameId) return; // No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId1" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range.
<renderFrameId1> : 36
if (renderFrameId1 >= self.chaserRenderFrameId) return;
/*
A typical case is as follows.
--------------------------------------------------------
[self.lastAllConfirmedRenderFrameId] : 22
<renderFrameId1> : 36
<self.chaserRenderFrameId> : 62
<self.chaserRenderFrameId> : 62
[self.renderFrameId] : 64
--------------------------------------------------------
*/
if (renderFrameId1 < self.chaserRenderFrameId) {
// The actual rollback-and-chase would later be executed in update(dt).
console.warn("Mismatched input detected, resetting chaserRenderFrameId: inputFrameId1:", inputFrameId1, ", renderFrameId1:", renderFrameId1, ", chaserRenderFrameId before reset: ", self.chaserRenderFrameId);
self.chaserRenderFrameId = renderFrameId1;
} else {
// Deliberately left blank, chasing is ongoing.
}
} else {
// No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId2" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range.
}
}
[self.renderFrameId] : 64
--------------------------------------------------------
*/
// The actual rollback-and-chase would later be executed in update(dt).
console.warn(`Mismatched input detected, resetting chaserRenderFrameId: ${self.chaserRenderFrameId}->${renderFrameId1} by firstPredictedYetIncorrectInputFrameId: ${inputFrameId1}`);
self.chaserRenderFrameId = renderFrameId1;
},
onPlayerAdded(rdf) {
@@ -702,7 +744,7 @@ cc.Class({
logBattleStats() {
const self = this;
let s = [];
s.push("Battle stats: renderFrameId=" + self.renderFrameId + ", lastAllConfirmedRenderFrameId=" + self.lastAllConfirmedRenderFrameId + ", lastUpsyncInputFrameId=" + self.lastUpsyncInputFrameId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId);
s.push(`Battle stats: renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}`);
for (let i = self.recentInputCache.stFrameId; i < self.recentInputCache.edFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.getByFrameId(i);
@@ -734,18 +776,22 @@ cc.Class({
self.playersInfoNode.getComponent("PlayersInfo").clearInfo();
},
spawnPlayerNode(joinIndex, x, y) {
spawnPlayerNode(joinIndex, vx, vy, playerRichInfo) {
const self = this;
const newPlayerNode = 1 == joinIndex ? cc.instantiate(self.player1Prefab) : cc.instantiate(self.player2Prefab); // hardcoded for now, car color determined solely by joinIndex
newPlayerNode.setPosition(cc.v2(x, y));
newPlayerNode.getComponent("SelfPlayer").mapNode = self.node;
const currentSelfColliderCircle = newPlayerNode.getComponent(cc.CircleCollider);
const wpos = self.virtualGridToWorldPos(vx, vy);
const newPlayerColliderLatest = self.latestCollisionSys.createCircle(x, y, currentSelfColliderCircle.radius);
const newPlayerColliderChaser = self.chaserCollisionSys.createCircle(x, y, currentSelfColliderCircle.radius);
newPlayerNode.setPosition(cc.v2(wpos[0], wpos[1]));
newPlayerNode.getComponent("SelfPlayer").mapNode = self.node;
const cpos = self.virtualGridToPlayerColliderPos(vx, vy, playerRichInfo);
const d = playerRichInfo.colliderRadius * 2,
x0 = cpos[0],
y0 = cpos[1];
let pts = [[0, 0], [d, 0], [d, d], [0, d]];
const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts);
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
self.latestCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderLatest);
self.chaserCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderChaser);
self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider);
safelyAddChild(self.node, newPlayerNode);
setLocalZOrder(newPlayerNode, 5);
@@ -753,8 +799,8 @@ cc.Class({
newPlayerNode.active = true;
const playerScriptIns = newPlayerNode.getComponent("SelfPlayer");
playerScriptIns.scheduleNewDirection({
dx: 0,
dy: 0
dx: playerRichInfo.dir.dx,
dy: playerRichInfo.dir.dy
}, true);
return [newPlayerNode, playerScriptIns];
@@ -792,12 +838,17 @@ cc.Class({
if (nextChaserRenderFrameId > self.renderFrameId) {
nextChaserRenderFrameId = self.renderFrameId;
}
self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap);
self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.collisionSys, self.collisionSysMap, true);
let t2 = performance.now();
// Inside "self.rollbackAndChase", the "self.latestCollisionSys" is ALWAYS ROLLED BACK to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding inputFrameDownsync, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.latestCollisionSys, self.latestCollisionSysMap);
// Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.collisionSys, self.collisionSysMap, false);
/*
const nonTrivialChaseEnded = (prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self.renderFrameId);
if (nonTrivialChaseEnded) {
console.debug("Non-trivial chase ended, prevChaserRenderFrameId=" + prevChaserRenderFrameId + ", nextChaserRenderFrameId=" + nextChaserRenderFrameId);
}
*/
self.applyRoomDownsyncFrameDynamics(rdf);
let t3 = performance.now();
} catch (err) {
@@ -805,7 +856,7 @@ cc.Class({
} finally {
// Update countdown
if (null != self.countdownNanos) {
self.countdownNanos -= (performance.now() - self.lastRenderFrameIdTriggeredAt) * 1000000;
self.countdownNanos = self.battleDurationNanos - self.renderFrameId * self.rollbackEstimatedDtNanos;
if (self.countdownNanos <= 0) {
self.onBattleStopped(self.playerRichInfoDict);
return;
@@ -894,15 +945,28 @@ cc.Class({
setLocalZOrder(toShowNode, 10);
},
hideFindingPlayersGUI() {
hideFindingPlayersGUI(rdf) {
const self = this;
if (null == self.findingPlayerNode.parent) return;
self.findingPlayerNode.parent.removeChild(self.findingPlayerNode);
if (null != rdf) {
self._initPlayerRichInfoDict(rdf.players, rdf.playerMetas);
}
},
onBattleReadyToStart(playerMetas) {
console.log("Calling `onBattleReadyToStart` with:", playerMetas);
onBattleReadyToStart(rdf) {
const self = this;
const players = rdf.players;
const playerMetas = rdf.playerMetas;
self._initPlayerRichInfoDict(players, playerMetas);
// Show the top status indicators for IN_BATTLE
const playersInfoScriptIns = self.playersInfoNode.getComponent("PlayersInfo");
for (let i in playerMetas) {
const playerMeta = playerMetas[i];
playersInfoScriptIns.updateData(playerMeta);
}
console.log("Calling `onBattleReadyToStart` with:", playerMetas);
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.hideExitButton();
findingPlayerScriptIns.updatePlayersInfo(playerMetas);
@@ -916,67 +980,22 @@ cc.Class({
}, 1500);
},
_createRoomDownsyncFrameLocally(renderFrameId, collisionSys, collisionSysMap) {
const self = this;
const prevRenderFrameId = renderFrameId - 1;
const inputFrameAppliedOnPrevRenderFrame = (
0 > prevRenderFrameId
?
null
:
self.getCachedInputFrameDownsyncWithPrediction(self._convertToInputFrameId(prevRenderFrameId, self.inputDelayFrames))
);
// TODO: Find a better way to assign speeds instead of using "speedRefRenderFrameId".
const speedRefRenderFrameId = prevRenderFrameId;
const speedRefRenderFrame = (
0 > speedRefRenderFrameId
?
null
:
self.recentRenderCache.getByFrameId(speedRefRenderFrameId)
);
const rdf = {
id: renderFrameId,
refFrameId: renderFrameId,
players: {}
};
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
const joinIndex = playerRichInfo.joinIndex;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
rdf.players[playerRichInfo.id] = {
id: playerRichInfo.id,
x: playerCollider.x,
y: playerCollider.y,
dir: self.ctrl.decodeDirection(null == inputFrameAppliedOnPrevRenderFrame ? 0 : inputFrameAppliedOnPrevRenderFrame.inputList[joinIndex - 1]),
speed: (null == speedRefRenderFrame ? playerRichInfo.speed : speedRefRenderFrame.players[playerRichInfo.id].speed),
joinIndex: joinIndex
};
});
if (
null != inputFrameAppliedOnPrevRenderFrame && self._allConfirmed(inputFrameAppliedOnPrevRenderFrame.confirmedList)
&&
self.lastAllConfirmedRenderFrameId >= prevRenderFrameId
&&
rdf.id > self.lastAllConfirmedRenderFrameId
) {
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id; // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId"
}
self.dumpToRenderCache(rdf);
return rdf;
},
applyRoomDownsyncFrameDynamics(rdf) {
const self = this;
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
const immediatePlayerInfo = rdf.players[playerId];
playerRichInfo.node.setPosition(immediatePlayerInfo.x, immediatePlayerInfo.y);
playerRichInfo.scriptIns.scheduleNewDirection(immediatePlayerInfo.dir, true);
playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed);
const wpos = self.virtualGridToWorldPos(immediatePlayerInfo.virtualGridX, immediatePlayerInfo.virtualGridY);
const dx = (wpos[0] - playerRichInfo.node.x);
const dy = (wpos[1] - playerRichInfo.node.y);
const justJiggling = (self.jigglingEps1D >= Math.abs(dx) && self.jigglingEps1D >= Math.abs(dy));
if (!justJiggling) {
playerRichInfo.node.setPosition(wpos[0], wpos[1]);
playerRichInfo.virtualGridX = immediatePlayerInfo.virtualGridX;
playerRichInfo.virtualGridY = immediatePlayerInfo.virtualGridY;
playerRichInfo.scriptIns.scheduleNewDirection(immediatePlayerInfo.dir, false);
playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed);
}
});
},
@@ -994,71 +1013,140 @@ cc.Class({
return inputFrameDownsync;
},
rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap) {
// TODO: Write unit-test for this function to compare with its backend counter part
applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap) {
const self = this;
let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame"
if (null == latestRdf) {
console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
const nextRenderFramePlayers = {}
for (let playerId in currRenderFrame.players) {
const currPlayerDownsync = currRenderFrame.players[playerId];
nextRenderFramePlayers[playerId] = {
id: playerId,
virtualGridX: currPlayerDownsync.virtualGridX,
virtualGridY: currPlayerDownsync.virtualGridY,
dir: {
dx: currPlayerDownsync.dir.dx,
dy: currPlayerDownsync.dir.dy,
},
speed: currPlayerDownsync.speed,
battleState: currPlayerDownsync.battleState,
score: currPlayerDownsync.score,
removed: currPlayerDownsync.removed,
joinIndex: currPlayerDownsync.joinIndex,
};
}
if (renderFrameIdSt >= renderFrameIdEd) {
return latestRdf;
}
/*
Reset "position" of players in "collisionSys" according to "renderFrameIdSt". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics.
*/
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
const joinIndex = playerRichInfo.joinIndex;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = latestRdf.players[playerId];
playerCollider.x = player.x;
playerCollider.y = player.y;
});
const toRet = {
id: currRenderFrame.id + 1,
players: nextRenderFramePlayers,
};
/*
This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd".
*/
for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) {
const renderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"
const j = self._convertToInputFrameId(i, self.inputDelayFrames);
const inputFrameDownsync = self.getCachedInputFrameDownsyncWithPrediction(j);
if (null == inputFrameDownsync) {
console.error("Failed to get cached inputFrameDownsync for renderFrameId=", i, ", inputFrameId=", j, "lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
if (null != delayedInputFrame) {
const inputList = delayedInputFrame.inputList;
const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1;
effPushbacks[joinIndex - 1] = [0.0, 0.0];
const playerId = self.playerRichInfoArr[j].id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = currRenderFrame.players[playerId];
const encodedInput = inputList[joinIndex - 1];
const decodedInput = self.ctrl.decodeDirection(encodedInput);
// console.log(`Got non-zero inputs for playerId=${playerId}, decodedInput=${JSON.stringify(decodedInput)} @currRenderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.id}`);
/*
Reset "position" of players in "collisionSys" according to "virtual grid position". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics.
*/
const newVx = player.virtualGridX + (decodedInput.dx + player.speed * decodedInput.dx);
const newVy = player.virtualGridY + (decodedInput.dy + player.speed * decodedInput.dy);
const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]);
playerCollider.x = newCpos[0];
playerCollider.y = newCpos[1];
// Update directions and thus would eventually update moving animation accordingly
nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx;
nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy;
}
const inputList = inputFrameDownsync.inputList;
// [WARNING] Traverse in the order of joinIndices to guarantee determinism.
collisionSys.update();
const result = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle?
for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1;
const playerId = self.playerRichInfoArr[j].id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = renderFrame.players[playerId];
const encodedInput = inputList[joinIndex - 1];
const decodedInput = self.ctrl.decodeDirection(encodedInput);
const baseChange = player.speed * self.rollbackEstimatedDt * decodedInput.speedFactor;
playerCollider.x += baseChange * decodedInput.dx;
playerCollider.y += baseChange * decodedInput.dy;
}
collisionSys.update();
const result = collisionSys.createResult(); // Can I reuse a "self.latestCollisionSysResult" object throughout the whole battle?
for (let i in self.playerRichInfoArr) {
const joinIndex = parseInt(i) + 1;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const potentials = playerCollider.potentials();
for (const barrier of potentials) {
for (const potential of potentials) {
// Test if the player collides with the wall
if (!playerCollider.collides(barrier, result)) continue;
if (!playerCollider.collides(potential, result)) continue;
// Push the player out of the wall
playerCollider.x -= result.overlap * result.overlap_x;
playerCollider.y -= result.overlap * result.overlap_y;
effPushbacks[joinIndex - 1][0] += result.overlap * result.overlap_x;
effPushbacks[joinIndex - 1][1] += result.overlap * result.overlap_y;
}
}
latestRdf = self._createRoomDownsyncFrameLocally(i + 1, collisionSys, collisionSysMap);
for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1;
const playerId = self.playerRichInfoArr[j].id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j]);
nextRenderFramePlayers[playerId].virtualGridX = newVpos[0];
nextRenderFramePlayers[playerId].virtualGridY = newVpos[1];
}
}
return toRet;
},
rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap, isChasing) {
/*
This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd" if not interruptted.
*/
const self = this;
let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame"
if (null == latestRdf) {
console.error(`Couldn't find renderFrameId=${renderFrameIdSt}, to rollback, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`);
return latestRdf;
}
if (renderFrameIdSt >= renderFrameIdEd) {
return latestRdf;
}
for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) {
const currRenderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"!
if (null == currRenderFrame) {
console.warn(`Couldn't find renderFrame for i=${i} to rollback, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, might've been interruptted by onRoomDownsyncFrame`);
return latestRdf;
}
const j = self._convertToInputFrameId(i, self.inputDelayFrames);
const delayedInputFrame = self.getCachedInputFrameDownsyncWithPrediction(j);
if (null == delayedInputFrame) {
console.warn(`Failed to get cached delayedInputFrame for i=${i}, j=${j}, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}`);
return latestRdf;
}
latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap);
if (
self._allConfirmed(delayedInputFrame.confirmedList)
&&
latestRdf.id > self.lastAllConfirmedRenderFrameId
) {
// We got a more up-to-date "all-confirmed-render-frame".
self.lastAllConfirmedRenderFrameId = latestRdf.id;
if (latestRdf.id > self.chaserRenderFrameId) {
// it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId", regardeless of the "isChasing" param
self.chaserRenderFrameId = latestRdf.id;
}
}
if (true == isChasing) {
// Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
self.chaserRenderFrameId = latestRdf.id;
}
self.dumpToRenderCache(latestRdf);
}
return latestRdf;
@@ -1071,8 +1159,10 @@ cc.Class({
if (self.playerRichInfoDict.has(playerId)) continue; // Skip already put keys
const immediatePlayerInfo = players[playerId];
const immediatePlayerMeta = playerMetas[playerId];
const nodeAndScriptIns = self.spawnPlayerNode(immediatePlayerInfo.joinIndex, immediatePlayerInfo.x, immediatePlayerInfo.y);
self.playerRichInfoDict.set(playerId, immediatePlayerInfo);
Object.assign(self.playerRichInfoDict.get(playerId), immediatePlayerMeta);
const nodeAndScriptIns = self.spawnPlayerNode(immediatePlayerInfo.joinIndex, immediatePlayerInfo.virtualGridX, immediatePlayerInfo.virtualGridY, self.playerRichInfoDict.get(playerId));
Object.assign(self.playerRichInfoDict.get(playerId), {
node: nodeAndScriptIns[0],
@@ -1100,7 +1190,7 @@ cc.Class({
return s.join('\n');
}
return "[stInputFrameId=" + self.recentInputCache.stFrameId + ", edInputFrameId=" + self.recentInputCache.edFrameId + ")";
return `[stInputFrameId=${self.recentInputCache.stFrameId}, edInputFrameId=${self.recentInputCache.edFrameId})`;
},
_stringifyRecentRenderCache(usefullOutput) {
@@ -1113,7 +1203,43 @@ cc.Class({
return s.join('\n');
}
return "[stRenderFrameId=" + self.recentRenderCache.stFrameId + ", edRenderFrameId=" + self.recentRenderCache.edFrameId + ")";
return `[stRenderFrameId=${self.recentRenderCache.stFrameId}, edRenderFrameId=${self.recentRenderCache.edFrameId})`;
},
worldToVirtualGridPos(x, y) {
// [WARNING] Introduces loss of precision!
const self = this;
// In JavaScript floating numbers suffer from seemingly non-deterministic arithmetics, and even if certain libs solved this issue by approaches such as fixed-point-number, they might not be used in other libs -- e.g. the "collision libs" we're interested in -- thus couldn't kill all pains.
let virtualGridX = Math.round(x * self.worldToVirtualGridRatio);
let virtualGridY = Math.round(y * self.worldToVirtualGridRatio);
return [virtualGridX, virtualGridY];
},
virtualGridToWorldPos(vx, vy) {
// No loss of precision
const self = this;
let wx = parseFloat(vx) * self.virtualGridToWorldRatio;
let wy = parseFloat(vy) * self.virtualGridToWorldRatio;
return [wx, wy];
},
playerWorldToCollisionPos(wx, wy, playerRichInfo) {
return [wx - playerRichInfo.colliderRadius, wy - playerRichInfo.colliderRadius];
},
playerColliderAnchorToWorldPos(cx, cy, playerRichInfo) {
return [cx + playerRichInfo.colliderRadius, cy + playerRichInfo.colliderRadius];
},
playerColliderAnchorToVirtualGridPos(cx, cy, playerRichInfo) {
const self = this;
const wpos = self.playerColliderAnchorToWorldPos(cx, cy, playerRichInfo);
return self.worldToVirtualGridPos(wpos[0], wpos[1])
},
virtualGridToPlayerColliderPos(vx, vy, playerRichInfo) {
const self = this;
const wpos = self.virtualGridToWorldPos(vx, vy);
return self.playerWorldToCollisionPos(wpos[0], wpos[1], playerRichInfo)
},
});

View File

@@ -1,4 +1,4 @@
const BasePlayer = require("./BasePlayer");
const BasePlayer = require("./BasePlayer");
cc.Class({
extends: BasePlayer,
@@ -7,6 +7,10 @@ cc.Class({
arrowTipNode: {
type: cc.Node,
default: null
},
coordLabel: {
type: cc.Label,
default: null
}
},
start() {
@@ -26,6 +30,10 @@ cc.Class({
'2-1': 'attackedRight'
};
this.arrowTipNode.active = false;
if (!this.mapIns.showCriticalCoordinateLabels) {
this.coordLabel.node.active = false;
}
},
showArrowTipNode() {
@@ -34,7 +42,7 @@ cc.Class({
return;
}
self.arrowTipNode.active = true;
window.setTimeout(function(){
window.setTimeout(function() {
if (null == self.arrowTipNode) {
return;
}
@@ -44,6 +52,9 @@ cc.Class({
update(dt) {
BasePlayer.prototype.update.call(this, dt);
if (this.mapIns.showCriticalCoordinateLabels) {
this.coordLabel.string = `(${this.node.x.toFixed(2)}, ${this.node.y.toFixed(2)})`;
}
},
});

View File

@@ -372,7 +372,7 @@ TileCollisionManager.prototype.extractBoundaryObjects = function (withTiledMapNo
for (let tsxFilenameIdx = 0; tsxFilenameIdx < tsxFileNames.length; ++tsxFilenameIdx) {
const tsxOrientation = tileSets[tsxFilenameIdx].orientation;
if (cc.TiledMap.Orientation.ORTHO == tsxOrientation) {
cc.error("Error at tileset %s: We proceed with ONLY tilesets in ORTHO orientation for all map orientations by now.", tsxFileNames[tsxFilenameIdx]);
cc.error("Error at tileset %s: We don't proceed with tilesets in ORTHO orientation by now.", tsxFileNames[tsxFilenameIdx]);
continue;
};

View File

@@ -1,24 +1,16 @@
window.DIRECTION_DECODER = [
[0, 0, null],
[0, +1, null],
[0, -1, null],
[+2, 0, null],
[-2, 0, null],
[+2, +1, null],
[-2, -1, null],
[+2, -1, null],
[-2, +1, null],
[+2, 0, null],
[-2, 0, null],
[0, +1, null],
[0, -1, null],
// The 3rd value matches low-precision constants in backend.
[0, 0],
[0, +2],
[0, -2],
[+2, 0],
[-2, 0],
[+1, +1],
[-1, -1],
[+1, -1],
[-1, +1],
];
for (let k in window.DIRECTION_DECODER) {
const length = Math.sqrt(window.DIRECTION_DECODER[k][0]*window.DIRECTION_DECODER[k][0] + window.DIRECTION_DECODER[k][1]*window.DIRECTION_DECODER[k][1]);
window.DIRECTION_DECODER[k][2] = (0 == length ? 0 : (1.0/length));
}
cc.Class({
extends: cc.Component,
properties: {
@@ -44,11 +36,11 @@ cc.Class({
type: cc.Float
},
magicLeanLowerBound: {
default: 0.414, // Tangent of (PI/8).
default: 0.1,
type: cc.Float
},
magicLeanUpperBound: {
default: 2.414, // Tangent of (3*PI/8).
default: 0.9,
type: cc.Float
},
// For joystick ends.
@@ -121,8 +113,8 @@ cc.Class({
_initTouchEvent() {
const self = this;
const translationListenerNode = (self.translationListenerNode ? self.translationListenerNode : self.mapNode);
const zoomingListenerNode = (self.zoomingListenerNode ? self.zoomingListenerNode : self.mapNode);
const translationListenerNode = (self.translationListenerNode ? self.translationListenerNode : self.mapNode);
const zoomingListenerNode = (self.zoomingListenerNode ? self.zoomingListenerNode : self.mapNode);
translationListenerNode.on(cc.Node.EventType.TOUCH_START, function(event) {
self._touchStartEvent(event);
@@ -136,7 +128,7 @@ cc.Class({
translationListenerNode.on(cc.Node.EventType.TOUCH_CANCEL, function(event) {
self._touchEndEvent(event);
});
translationListenerNode.inTouchPoints = new Map();
translationListenerNode.inTouchPoints = new Map();
zoomingListenerNode.on(cc.Node.EventType.TOUCH_START, function(event) {
self._touchStartEvent(event);
@@ -150,7 +142,7 @@ cc.Class({
zoomingListenerNode.on(cc.Node.EventType.TOUCH_CANCEL, function(event) {
self._touchEndEvent(event);
});
zoomingListenerNode.inTouchPoints = new Map();
zoomingListenerNode.inTouchPoints = new Map();
},
_isMapOverMoved(mapTargetPos) {
@@ -159,7 +151,7 @@ cc.Class({
},
_touchStartEvent(event) {
const theListenerNode = event.target;
const theListenerNode = event.target;
for (let touch of event._touches) {
theListenerNode.inTouchPoints.set(touch._id, touch);
}
@@ -169,12 +161,12 @@ cc.Class({
if (ALL_MAP_STATES.VISUAL != this.mapScriptIns.state) {
return;
}
const theListenerNode = event.target;
const theListenerNode = event.target;
const linearScaleFacBase = this.linearScaleFacBase; // Not used yet.
if (1 != theListenerNode.inTouchPoints.size) {
return;
}
if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) {
if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) {
return;
}
const diffVec = event.currentTouch._point.sub(event.currentTouch._startPoint);
@@ -193,9 +185,9 @@ cc.Class({
if (ALL_MAP_STATES.VISUAL != this.mapScriptIns.state) {
return;
}
const theListenerNode = event.target;
const theListenerNode = event.target;
if (2 != theListenerNode.inTouchPoints.size) {
return;
return;
}
if (2 == event._touches.length) {
const firstTouch = event._touches[0];
@@ -223,13 +215,13 @@ cc.Class({
}
this.mainCamera.zoomRatio = targetScale;
for (let child of this.mainCameraNode.children) {
child.setScale(1/targetScale);
child.setScale(1 / targetScale);
}
}
},
_touchEndEvent(event) {
const theListenerNode = event.target;
const theListenerNode = event.target;
do {
if (!theListenerNode.inTouchPoints.has(event.currentTouch._id)) {
break;
@@ -245,7 +237,7 @@ cc.Class({
break;
}
// TODO: Handle single-finger-click event.
// TODO: Handle single-finger-click event.
} while (false);
this.cachedStickHeadPosition = cc.v2(0.0, 0.0);
for (let touch of event._touches) {
@@ -270,19 +262,11 @@ cc.Class({
encodedIdx: 0
};
if (Math.abs(continuousDx) < eps && Math.abs(continuousDy) < eps) {
return ret;
return ret;
}
if (Math.abs(continuousDx) < eps) {
ret.dx = 0;
if (0 < continuousDy) {
ret.dy = +1; // up
ret.encodedIdx = 1;
} else {
ret.dy = -1; // down
ret.encodedIdx = 2;
}
} else if (Math.abs(continuousDy) < eps) {
const criticalRatio = continuousDy / continuousDx;
if (Math.abs(criticalRatio) < this.magicLeanLowerBound) {
ret.dy = 0;
if (0 < continuousDx) {
ret.dx = +2; // right
@@ -291,66 +275,55 @@ cc.Class({
ret.dx = -2; // left
ret.encodedIdx = 4;
}
} else if (Math.abs(criticalRatio) > this.magicLeanUpperBound) {
ret.dx = 0;
if (0 < continuousDy) {
ret.dy = +2; // up
ret.encodedIdx = 1;
} else {
ret.dy = -2; // down
ret.encodedIdx = 2;
}
} else {
const criticalRatio = continuousDy / continuousDx;
if (criticalRatio > this.magicLeanLowerBound && criticalRatio < this.magicLeanUpperBound) {
if (0 < continuousDx) {
ret.dx = +2;
if (0 < continuousDx) {
if (0 < continuousDy) {
ret.dx = +1;
ret.dy = +1;
ret.encodedIdx = 5;
} else {
ret.dx = -2;
ret.dx = +1;
ret.dy = -1;
ret.encodedIdx = 7;
}
} else {
// 0 >= continuousDx
if (0 < continuousDy) {
ret.dx = -1;
ret.dy = +1;
ret.encodedIdx = 8;
} else {
ret.dx = -1;
ret.dy = -1;
ret.encodedIdx = 6;
}
} else if (criticalRatio > -this.magicLeanUpperBound && criticalRatio < -this.magicLeanLowerBound) {
if (0 < continuousDx) {
ret.dx = +2;
ret.dy = -1;
ret.encodedIdx = 7;
} else {
ret.dx = -2;
ret.dy = +1;
ret.encodedIdx = 8;
}
} else {
if (Math.abs(criticalRatio) < 1) {
ret.dy = 0;
if (0 < continuousDx) {
ret.dx = +2;
ret.encodedIdx = 9;
} else {
ret.dx = -2;
ret.encodedIdx = 10;
}
} else {
ret.dx = 0;
if (0 < continuousDy) {
ret.dy = +1;
ret.encodedIdx = 11;
} else {
ret.dy = -1;
ret.encodedIdx = 12;
}
}
}
}
return ret;
},
decodeDirection(encodedDirection) {
const mapped = window.DIRECTION_DECODER[encodedDirection];
if (null == mapped) {
console.error("Unexpected encodedDirection = ", encodedDirection);
console.error("Unexpected encodedDirection = ", encodedDirection);
}
return {
dx: mapped[0],
dy: mapped[1],
speedFactor: mapped[2],
}
dy: mapped[1],
};
},
getDiscretizedDirection() {
return this.discretizeDirection(this.cachedStickHeadPosition.x, this.cachedStickHeadPosition.y, this.joyStickEps);
return this.discretizeDirection(this.cachedStickHeadPosition.x, this.cachedStickHeadPosition.y, this.joyStickEps);
}
});

View File

@@ -104,18 +104,6 @@ window.getExpectedRoomIdSync = function() {
return null;
};
window.unsetClientSessionCloseOrErrorFlag = function() {
cc.sys.localStorage.removeItem("ClientSessionCloseOrErrorFlag");
return;
}
window.setClientSessionCloseOrErrorFlag = function() {
const oldVal = cc.sys.localStorage.getItem("ClientSessionCloseOrErrorFlag");
if (true == oldVal) return false;
cc.sys.localStorage.setItem("ClientSessionCloseOrErrorFlag", true);
return true;
}
window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
if (window.clientSession && window.clientSession.readyState == WebSocket.OPEN) {
if (null != onopenCb) {
@@ -124,7 +112,9 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
return;
}
const intAuthToken = cc.sys.localStorage.getItem("selfPlayer") ? JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken : "";
const selfPlayerStr = cc.sys.localStorage.getItem("selfPlayer");
const selfPlayer = null == selfPlayerStr ? null : JSON.parse(selfPlayerStr);
const intAuthToken = null == selfPlayer ? "" : selfPlayer.intAuthToken;
let urlToConnect = backendAddress.PROTOCOL.replace('http', 'ws') + '://' + backendAddress.HOST + ":" + backendAddress.PORT + backendAddress.WS_PATH_PREFIX + "?intAuthToken=" + intAuthToken;
@@ -144,19 +134,19 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
const clientSession = new WebSocket(urlToConnect);
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
clientSession.onopen = function(event) {
console.log("The WS clientSession is opened.");
clientSession.onopen = function(evt) {
console.log("The WS clientSession is opened. clientSession.id=", clientSession.id);
window.clientSession = clientSession;
if (null == onopenCb) return;
onopenCb();
};
clientSession.onmessage = function(event) {
if (null == event || null == event.data) {
clientSession.onmessage = function(evt) {
if (null == evt || null == evt.data) {
return;
}
try {
const resp = window.WsResp.decode(new Uint8Array(event.data));
const resp = window.pb.protos.WsResp.decode(new Uint8Array(evt.data));
switch (resp.act) {
case window.DOWNSYNC_MSG_ACT_HB_REQ:
window.handleHbRequirements(resp); // 获取boundRoomId并存储到localStorage
@@ -166,10 +156,10 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
break;
case window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED:
// Deliberately left blank for now
mapIns.hideFindingPlayersGUI();
mapIns.hideFindingPlayersGUI(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START:
mapIns.onBattleReadyToStart(resp.rdf.playerMetas);
mapIns.onBattleReadyToStart(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_START:
mapIns.onRoomDownsyncFrame(resp.rdf);
@@ -182,72 +172,61 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
break;
case window.DOWNSYNC_MSG_ACT_FORCED_RESYNC:
if (null == resp.inputFrameDownsyncBatch || 0 >= resp.inputFrameDownsyncBatch.length) {
console.error("Got empty inputFrameDownsyncBatch upon resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp, null, 2));
console.error(`Got empty inputFrameDownsyncBatch upon resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, the incoming resp=
${JSON.stringify(resp, null, 2)}`);
return;
}
// Unless upon ws session lost and reconnected, it's maintained true that "inputFrameDownsyncBatch[0].inputFrameId == frontend.lastAllConfirmedInputFrameId+1", and in this case we should try to keep frontend moving only by "frontend.recentInputCache" to avoid jiggling of synced positions
const inputFrameIdConsecutive = (resp.inputFrameDownsyncBatch[0].inputFrameId == mapIns.lastAllConfirmedInputFrameId + 1);
const renderFrameIdConsecutive = (resp.rdf.id <= mapIns.renderFrameId + mapIns.renderFrameIdLagTolerance);
if (inputFrameIdConsecutive && renderFrameIdConsecutive) {
console.log("Got consecutive resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp));
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
} else {
console.warn("Got forced resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp, null, 2));
// The following order of execution is important
const dumpRenderCacheRet = mapIns.onRoomDownsyncFrame(resp.rdf);
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch, dumpRenderCacheRet);
}
console.warn(`Got resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, inputFrameIdConsecutive=${inputFrameIdConsecutive}, renderFrameIdConsecutive=${renderFrameIdConsecutive}`);
// The following order of execution is important
mapIns.onRoomDownsyncFrame(resp.rdf);
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
break;
default:
break;
}
} catch (e) {
console.error("Unexpected error when parsing data of:", event.data, e);
console.error("Unexpected error when parsing data of:", evt.data, e);
}
};
clientSession.onerror = function(event) {
if (!window.setClientSessionCloseOrErrorFlag()) {
return;
clientSession.onerror = function(evt) {
console.error("Error caught on the WS clientSession: ", evt);
if (window.handleClientSessionError) {
window.handleClientSessionError();
}
console.error("Error caught on the WS clientSession: ", event);
if (window.clientSessionPingInterval) {
clearInterval(window.clientSessionPingInterval);
}
if (window.handleClientSessionCloseOrError) {
window.handleClientSessionCloseOrError();
}
window.unsetClientSessionCloseOrErrorFlag();
};
clientSession.onclose = function(event) {
if (!window.setClientSessionCloseOrErrorFlag()) {
return;
}
console.warn("The WS clientSession is closed: ", event);
if (window.clientSessionPingInterval) {
clearInterval(window.clientSessionPingInterval);
}
if (false == event.wasClean) {
// Chrome doesn't allow the use of "CustomCloseCode"s (yet) and will callback with a "WebsocketStdCloseCode 1006" and "false == event.wasClean" here. See https://tools.ietf.org/html/rfc6455#section-7.4 for more information.
if (window.handleClientSessionCloseOrError) {
window.handleClientSessionCloseOrError();
clientSession.onclose = function(evt) {
// [WARNING] The callback "onclose" might be called AFTER the webpage is refreshed with "1001 == evt.code".
console.warn("The WS clientSession is closed: ", evt, clientSession);
if (false == evt.wasClean) {
/*
Chrome doesn't allow the use of "CustomCloseCode"s (yet) and will callback with a "WebsocketStdCloseCode 1006" and "false == evt.wasClean" here. See https://tools.ietf.org/html/rfc6455#section-7.4 for more information.
*/
if (window.handleClientSessionError) {
window.handleClientSessionError();
}
} else {
switch (event.code) {
switch (evt.code) {
case constants.RET_CODE.PLAYER_NOT_ADDABLE_TO_ROOM:
case constants.RET_CODE.PLAYER_NOT_READDABLE_TO_ROOM:
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
break;
case constants.RET_CODE.UNKNOWN_ERROR:
case constants.RET_CODE.MYSQL_ERROR:
case constants.RET_CODE.PLAYER_NOT_FOUND:
case constants.RET_CODE.PLAYER_CHEATING:
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
case 1006: // Peer(i.e. the backend) gone unexpectedly
if (window.handleClientSessionError) {
window.handleClientSessionError();
}
break;
default:
break;
}
if (window.handleClientSessionCloseOrError) {
window.handleClientSessionCloseOrError();
}
}
window.unsetClientSessionCloseOrErrorFlag();
};
};
@@ -258,17 +237,17 @@ window.clearLocalStorageAndBackToLoginScene = function(shouldRetainBoundRoomIdIn
window.mapIns.musicEffectManagerScriptIns.stopAllMusic();
}
/**
* Here I deliberately removed the callback in the "common `handleClientSessionCloseOrError` callback"
* Here I deliberately removed the callback in the "common `handleClientSessionError` callback"
* within which another invocation to `clearLocalStorageAndBackToLoginScene` will be made.
*
* It'll be re-assigned to the common one upon reentrance of `Map.onLoad`.
*
* -- YFLu 2019-04-06
*/
window.handleClientSessionCloseOrError = () => {
console.warn("+++++++ Special handleClientSessionCloseOrError() assigned within `clearLocalStorageAndBackToLoginScene`");
window.handleClientSessionError = () => {
console.warn("+++++++ Special handleClientSessionError() assigned within `clearLocalStorageAndBackToLoginScene`");
// TBD.
window.handleClientSessionCloseOrError = null; // To ensure that it's called at most once.
window.handleClientSessionError = null; // To ensure that it's called at most once.
};
window.closeWSConnection();
window.clearSelfPlayer();

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"ver": "1.0.5",
"uuid": "f9cd97f6-3533-4a27-afc8-cf302da003b2",
"uuid": "1ef4a156-5c54-45b9-85ac-86734985e13a",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,

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