Compare commits

...

55 Commits
v0.4 ... v0.6.3

Author SHA1 Message Date
genxium
d21f59cafa Minor fix. 2022-11-20 21:07:45 +08:00
Wing
335fef66ef Merge pull request #7 from genxium/dungeon_characters
Updated anim of characters, and minor updates to inputs on wire to support atk button.
2022-11-20 20:18:44 +08:00
genxium
52480ab29f Updated TouchEventsManager to support input from Keyboard as well as an additional atk btn. 2022-11-20 20:13:08 +08:00
genxium
971f6461ab Updated charts. 2022-11-20 12:27:29 +08:00
genxium
061aa449c9 Fixes for online map class to use updated character animations. 2022-11-19 22:59:12 +08:00
genxium
78dd9ecd85 Initial commit for offline map, might break the online version. 2022-11-19 20:58:07 +08:00
yflu
d4226137b6 Initial draft of an offline map for testing new characters. 2022-11-17 23:39:32 +08:00
yflu
e432026fec Fixed proto_gen_shortcut script for OSX. 2022-11-17 23:13:53 +08:00
Wing
3e7718ed04 Merge pull request #6 from genxium/dungeon_battle
Added support of ORTHO orientation tmx.
2022-11-17 16:51:11 +08:00
genxium
b78dd54431 Regenerated new resource ccc meta files. 2022-11-17 15:10:17 +08:00
genxium
22fb72afbc Fixed frontend tmx parsing for ortho map. 2022-11-17 15:01:35 +08:00
genxium
7b9172c27b Fixed backend tmx parsing for ortho map. 2022-11-16 21:32:25 +08:00
genxium
7e12853a73 Added fineart resources. 2022-11-16 20:58:12 +08:00
genxium
95dcc2ef17 Updated README. 2022-11-13 22:23:54 +08:00
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
312 changed files with 13154 additions and 16409 deletions

View File

@@ -1,11 +1,24 @@
# Preface # Preface
This project is a demo for a websocket-based input synchronization method inspired by [GGPO](https://www.ggpo.net/). This project is a demo for a websocket-based rollback netcode inspired by [GGPO](https://github.com/pond3r/ggpo/blob/master/doc/README.md).
![screenshot-1](./screenshot-1.png)
Please checkout [this demo video](https://pan.baidu.com/s/1aM6e8IWaJszFCYAsRjt19g?pwd=z02c) to see whether the source codes are doing what you expect for synchronization. ![gif_demo](./charts/along_wall_interaction_with_reconnection.gif)
The video mainly shows the following feature (yet I'm not surprised if they're not obvious): when a player didn't have its input arrived at the backend in time (e.g. due to local lag, network delay or reconnection), backend forces confirmation of a prediction of its own and sends the confirmed input together w/ a reference render frame to that player. Please also checkout [this demo video](https://pan.baidu.com/s/1YkfuHjNLzlFVnKiEj6wrDQ?pwd=tkr5) to see how this demo carries out a full 60fps synchronization with the help of _batched input upsync/downsync_ for satisfying network I/O performance.
The video mainly shows the following features.
- The backend receives inputs from frontend peers and 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 is togglable 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**.
_(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)
# 1. Building & running # 1. Building & running
@@ -19,7 +32,7 @@ The video mainly shows the following feature (yet I'm not surprised if they're n
- [protobuf CLI](https://developers.google.com/protocol-buffers/docs/downloads) (optional, only for development) - [protobuf CLI](https://developers.google.com/protocol-buffers/docs/downloads) (optional, only for development)
### Frontend ### 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 incompatible) - [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) - [protojs](https://www.npmjs.com/package/protojs) (optional, only for development)
## 1.2 Provisioning ## 1.2 Provisioning
@@ -64,7 +77,7 @@ The easy way is to try out 2 players with test accounts on a same machine.
- Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene. - Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene.
- Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start. - Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start.
- Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync. - Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync.
![screenshot-2](./screenshot-2.png) ![screenshot-2](./charts/screenshot-2.png)
## 2 Troubleshooting ## 2 Troubleshooting

View File

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

View File

@@ -1,6 +1,8 @@
package utils package utils
import ( import (
. "battle_srv/common"
. "battle_srv/configs"
"bytes" "bytes"
"crypto/sha1" "crypto/sha1"
. "dnmshared" . "dnmshared"
@@ -11,8 +13,6 @@ import (
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
. "server/common"
. "server/configs"
"sort" "sort"
"time" "time"
) )
@@ -250,10 +250,6 @@ func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
return 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 return
} }
@@ -276,9 +272,6 @@ func (w *wechat) getAccessTokenFromServer() (accessToken string, err error) {
return 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 accessToken = r.AccessToken
return return
} }

View File

@@ -1,15 +1,15 @@
package env_tools package env_tools
import ( import (
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/models"
"battle_srv/storage"
. "dnmshared" . "dnmshared"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"go.uber.org/zap" "go.uber.org/zap"
. "server/common"
"server/common/utils"
"server/models"
"server/storage"
) )
func LoadPreConf() { func LoadPreConf() {

View File

@@ -1,11 +1,11 @@
package env_tools package env_tools
import ( import (
. "battle_srv/common"
"battle_srv/common/utils"
"battle_srv/models"
"battle_srv/storage"
. "dnmshared" . "dnmshared"
. "server/common"
"server/common/utils"
"server/models"
"server/storage"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"

View File

@@ -1,4 +1,4 @@
module server module battle_srv
go 1.19 go 1.19

View File

@@ -1,19 +1,19 @@
package main package main
import ( 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" "context"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"server/api"
"server/api/v1"
. "server/common"
"server/env_tools"
"server/models"
"server/storage"
"server/ws"
"syscall" "syscall"
"time" "time"

View File

@@ -1,7 +1,7 @@
package models package models
import ( import (
. "dnmshared" . "dnmshared/sharedprotos"
) )
type Barrier struct { type Barrier struct {

View File

@@ -1,77 +1,22 @@
package models package models
import ( import (
. "dnmshared" . "battle_srv/protos"
pb "server/pb_output" . "dnmshared/sharedprotos"
) )
func toPbVec2D(modelInstance *Vec2D) *pb.Vec2D { func toPbPlayers(modelInstances map[int32]*Player) map[int32]*PlayerDownsync {
toRet := &pb.Vec2D{ toRet := make(map[int32]*PlayerDownsync, 0)
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)
if nil == modelInstances { if nil == modelInstances {
return toRet return toRet
} }
for k, last := range modelInstances { for k, last := range modelInstances {
toRet[k] = &pb.Player{ toRet[k] = &PlayerDownsync{
Id: last.Id, Id: last.Id,
X: last.X, VirtualGridX: last.VirtualGridX,
Y: last.Y, VirtualGridY: last.VirtualGridY,
Dir: &pb.Direction{ Dir: &Direction{
Dx: last.Dir.Dx, Dx: last.Dir.Dx,
Dy: last.Dir.Dy, Dy: last.Dir.Dy,
}, },

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,26 @@
package models package models
import ( import (
. "battle_srv/common"
"battle_srv/common/utils"
. "battle_srv/protos"
. "dnmshared" . "dnmshared"
. "dnmshared/sharedprotos"
"encoding/xml"
"fmt" "fmt"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/solarlune/resolv" "github.com/solarlune/resolv"
"go.uber.org/zap" "go.uber.org/zap"
"io/ioutil"
"math" "math"
"math/rand" "math/rand"
. "server/common" "os"
"server/common/utils" "path/filepath"
pb "server/pb_output"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"encoding/xml"
"io/ioutil"
"os"
"path/filepath"
) )
const ( const (
@@ -61,36 +61,17 @@ const (
MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2 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{ var DIRECTION_DECODER = [][]int32{
{0, 0}, {0, 0},
{0, +1}, {0, +2},
{0, -1}, {0, -2},
{+2, 0}, {+2, 0},
{-2, 0}, {-2, 0},
{+2, +1}, {+1, +1},
{-2, -1}, {-1, -1},
{+2, -1}, {+1, -1},
{-2, +1}, {-1, +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,
} }
type RoomBattleState struct { type RoomBattleState struct {
@@ -129,11 +110,13 @@ func calRoomScore(inRoomPlayerCount int32, roomPlayerCnt int, currentRoomBattleS
} }
type Room struct { type Room struct {
Id int32 Id int32
Capacity int Capacity int
Players map[int32]*Player collisionSpaceOffsetX float64
PlayersArr []*Player // ordered by joinIndex collisionSpaceOffsetY float64
CollisionSysMap map[int32]*resolv.Object Players map[int32]*Player
PlayersArr []*Player // ordered by joinIndex
CollisionSysMap map[int32]*resolv.Object
/** /**
* The following `PlayerDownsyncSessionDict` is NOT individually put * The following `PlayerDownsyncSessionDict` is NOT individually put
* under `type Player struct` for a reason. * under `type Player struct` for a reason.
@@ -177,11 +160,15 @@ 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" 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) InputScaleFrames uint32 // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
JoinIndexBooleanArr []bool JoinIndexBooleanArr []bool
RollbackEstimatedDt float64
RollbackEstimatedDtMillis float64 RollbackEstimatedDtMillis float64
RollbackEstimatedDtNanos int64 RollbackEstimatedDtNanos int64
LastRenderFrameIdTriggeredAt int64 LastRenderFrameIdTriggeredAt int64
WorldToVirtualGridRatio float64
VirtualGridToWorldRatio float64
PlayerDefaultSpeed int32
StageName string StageName string
StageDiscreteW int32 StageDiscreteW int32
StageDiscreteH int32 StageDiscreteH int32
@@ -189,13 +176,9 @@ type Room struct {
StageTileH int32 StageTileH int32
RawBattleStrToVec2DListMap StrToVec2DListMap RawBattleStrToVec2DListMap StrToVec2DListMap
RawBattleStrToPolygon2DListMap StrToPolygon2DListMap RawBattleStrToPolygon2DListMap StrToPolygon2DListMap
BackendDynamicsEnabled bool
} }
const (
PLAYER_DEFAULT_SPEED = float64(200) // Hardcoded
ADD_SPEED = float64(100) // Hardcoded
)
func (pR *Room) updateScore() { func (pR *Room) updateScore() {
pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State) pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State)
} }
@@ -217,9 +200,8 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
pPlayerFromDbInit.AckingInputFrameId = -1 pPlayerFromDbInit.AckingInputFrameId = -1
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit.FrozenAtGmtMillis = -1 // Hardcoded temporarily. pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded
pPlayerFromDbInit.Speed = PLAYER_DEFAULT_SPEED // Hardcoded temporarily. pPlayerFromDbInit.ColliderRadius = float64(24) // Hardcoded
pPlayerFromDbInit.AddSpeedAtGmtMillis = -1 // Hardcoded temporarily.
pR.Players[playerId] = pPlayerFromDbInit pR.Players[playerId] = pPlayerFromDbInit
pR.PlayerDownsyncSessionDict[playerId] = session pR.PlayerDownsyncSessionDict[playerId] = session
@@ -251,6 +233,8 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1 pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK 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)) Logger.Warn("ReAddPlayerIfPossible finished.", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("joinIndex", pEffectiveInRoomPlayerInstance.JoinIndex), zap.Any("playerBattleState", pEffectiveInRoomPlayerInstance.BattleState), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount), zap.Any("AckingFrameId", pEffectiveInRoomPlayerInstance.AckingFrameId), zap.Any("AckingInputFrameId", pEffectiveInRoomPlayerInstance.AckingInputFrameId), zap.Any("LastSentInputFrameId", pEffectiveInRoomPlayerInstance.LastSentInputFrameId))
return true return true
@@ -268,7 +252,7 @@ func (pR *Room) ChooseStage() error {
} }
rand.Seed(time.Now().Unix()) rand.Seed(time.Now().Unix())
stageNameList := []string{ /*"simple" ,*/ "richsoil"} stageNameList := []string{"dungeon" /*"dungeon", "simple", "richsoil" */}
chosenStageIndex := rand.Int() % len(stageNameList) // Hardcoded temporarily. -- YFLu chosenStageIndex := rand.Int() % len(stageNameList) // Hardcoded temporarily. -- YFLu
pR.StageName = stageNameList[chosenStageIndex] pR.StageName = stageNameList[chosenStageIndex]
@@ -328,8 +312,8 @@ func (pR *Room) ChooseStage() error {
barrierPolygon2DList := *(toRetStrToPolygon2DListMap["Barrier"]) barrierPolygon2DList := *(toRetStrToPolygon2DListMap["Barrier"])
var barrierLocalIdInBattle int32 = 0 var barrierLocalIdInBattle int32 = 0
for _, polygon2DUnaligned := range barrierPolygon2DList { for _, polygon2DUnaligned := range barrierPolygon2DList.Eles {
polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned) polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned)
/* /*
// For debug-printing only. // 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)) Logger.Info("ChooseStage printing polygon2D for barrierPolygon2DList", zap.Any("barrierLocalIdInBattle", barrierLocalIdInBattle), zap.Any("polygon2D.Anchor", polygon2D.Anchor), zap.Any("polygon2D.Points", polygon2D.Points))
@@ -361,13 +345,6 @@ func (pR *Room) ConvertToLastUsedRenderFrameId(inputFrameId int32, inputDelayFra
return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames + (1 << pR.InputScaleFrames) - 1) return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames + (1 << pR.InputScaleFrames) - 1)
} }
func (pR *Room) EncodeUpsyncCmd(upsyncCmd *pb.InputFrameUpsync) uint64 {
var ret uint64 = 0
// There're 13 possible directions, occupying the first 4 bits, no need to shift
ret += uint64(upsyncCmd.EncodedDir)
return ret
}
func (pR *Room) RenderFrameBufferString() string { func (pR *Room) RenderFrameBufferString() string {
return fmt.Sprintf("{renderFrameId: %d, stRenderFrameId: %d, edRenderFrameId: %d, lastAllConfirmedRenderFrameId: %d}", pR.RenderFrameId, pR.RenderFrameBuffer.StFrameId, pR.RenderFrameBuffer.EdFrameId, pR.CurDynamicsRenderFrameId) return fmt.Sprintf("{renderFrameId: %d, stRenderFrameId: %d, edRenderFrameId: %d, lastAllConfirmedRenderFrameId: %d}", pR.RenderFrameId, pR.RenderFrameBuffer.StFrameId, pR.RenderFrameBuffer.EdFrameId, pR.CurDynamicsRenderFrameId)
} }
@@ -385,7 +362,7 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
if nil == tmp { if nil == tmp {
break 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)) s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList))
} }
@@ -407,7 +384,7 @@ func (pR *Room) StartBattle() {
// Initialize the "collisionSys" as well as "RenderFrameBuffer" // Initialize the "collisionSys" as well as "RenderFrameBuffer"
pR.CurDynamicsRenderFrameId = 0 pR.CurDynamicsRenderFrameId = 0
kickoffFrame := &pb.RoomDownsyncFrame{ kickoffFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId, Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players), Players: toPbPlayers(pR.Players),
CountdownNanos: pR.BattleDurationNanos, CountdownNanos: pR.BattleDurationNanos,
@@ -415,7 +392,11 @@ func (pR *Room) StartBattle() {
pR.RenderFrameBuffer.Put(kickoffFrame) pR.RenderFrameBuffer.Put(kickoffFrame)
// Refresh "Colliders" // 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. * Will be triggered from a goroutine which executes the critical `Room.AddPlayerIfPossible`, thus the `battleMainLoop` should be detached.
@@ -459,17 +440,11 @@ func (pR *Room) StartBattle() {
pR.prefabInputFrameDownsync(noDelayInputFrameId) pR.prefabInputFrameDownsync(noDelayInputFrameId)
} }
// Force setting all-confirmed of buffered inputFrames periodically pR.markConfirmationIfApplicable()
unconfirmedMask := pR.forceConfirmationIfApplicable() unconfirmedMask := uint64(0)
if pR.BackendDynamicsEnabled {
dynamicsDuration := int64(0) // Force setting all-confirmed of buffered inputFrames periodically
if 0 <= pR.LastAllConfirmedInputFrameId { unconfirmedMask = pR.forceConfirmationIfApplicable()
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
} }
upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId)) upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId))
@@ -479,31 +454,45 @@ func (pR *Room) StartBattle() {
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more. 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 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 { if refRenderFrameId > pR.RenderFrameId {
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 { for playerId, player := range pR.Players {
if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped { 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". // [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 continue
} }
if 0 == pR.RenderFrameId { 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) pR.sendSafely(kickoffFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId)
} else { } else {
// [WARNING] Websocket is TCP-based, thus no need to re-send a previously sent inputFrame to a same player! // [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 candidateToSendInputFrameId := pR.Players[playerId].LastSentInputFrameId + 1
if candidateToSendInputFrameId < pR.InputsBuffer.StFrameId { 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. // [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 candidateToSendInputFrameId = pR.InputsBuffer.StFrameId
} }
@@ -519,7 +508,7 @@ func (pR *Room) StartBattle() {
if nil == tmp { 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))) 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) { 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)) 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))
} }
@@ -529,22 +518,26 @@ func (pR *Room) StartBattle() {
if 0 >= len(toSendInputFrames) { if 0 >= len(toSendInputFrames) {
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"! // [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 { 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 continue
} }
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1) /*
var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr) Resync helps
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || 0 < (unconfirmedMask&joinMask) { 1. when player with a slower frontend clock lags significantly behind and thus wouldn't get its inputUpsync recognized due to faster "forceConfirmation"
// [WARNING] Even upon "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED", it could be true that "0 == (unconfirmedMask & joinMask)"! 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) tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId)
if nil == tmp { 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())) 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) pR.sendSafely(refRenderFrame, toSendInputFrames, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId)
} else { } else {
pR.sendSafely(nil, toSendInputFrames, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId) pR.sendSafely(nil, toSendInputFrames, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId)
@@ -553,24 +546,40 @@ func (pR *Room) StartBattle() {
} }
} }
// Evict no longer required "RenderFrameBuffer" if pR.BackendDynamicsEnabled {
for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) { // Evict no longer required "RenderFrameBuffer"
_ = pR.RenderFrameBuffer.Pop() for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) {
_ = pR.RenderFrameBuffer.Pop()
}
} }
toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames) 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) { 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) { 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 // 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++ pR.RenderFrameId++
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation) elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
if elapsedInCalculation > nanosPerFrame { 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)) time.Sleep(time.Duration(nanosPerFrame - elapsedInCalculation))
} }
@@ -586,7 +595,7 @@ func (pR *Room) toDiscreteInputsBufferIndex(inputFrameId int32, joinIndex int32)
return (inputFrameId << 2) + joinIndex // allowing joinIndex upto 15 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 { if swapped := atomic.CompareAndSwapInt32(&pR.State, RoomBattleStateIns.IN_BATTLE, RoomBattleStateIns.IN_BATTLE); !swapped {
return return
} }
@@ -624,7 +633,7 @@ 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 inputFrameId := inputFrameDownsync.InputFrameId
if -1 == pR.LastAllConfirmedInputFrameIdWithChange || false == pR.equalInputLists(inputFrameDownsync.InputList, pR.LastAllConfirmedInputList) { if -1 == pR.LastAllConfirmedInputFrameIdWithChange || false == pR.equalInputLists(inputFrameDownsync.InputList, pR.LastAllConfirmedInputList) {
if -1 == playerId { if -1 == playerId {
@@ -666,7 +675,7 @@ func (pR *Room) StopBattleForSettlement() {
Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id)) Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id))
pR.RenderFrameId++ pR.RenderFrameId++
for playerId, _ := range pR.Players { for playerId, _ := range pR.Players {
assembledFrame := pb.RoomDownsyncFrame{ assembledFrame := RoomDownsyncFrame{
Id: pR.RenderFrameId, Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players), Players: toPbPlayers(pR.Players),
CountdownNanos: -1, // TODO: Replace this magic constant! CountdownNanos: -1, // TODO: Replace this magic constant!
@@ -692,18 +701,19 @@ func (pR *Room) onBattlePrepare(cb BattleStartCbType) {
pR.State = RoomBattleStateIns.PREPARE pR.State = RoomBattleStateIns.PREPARE
Logger.Info("Battle state transitted to RoomBattleStateIns.PREPARE for:", zap.Any("roomId", pR.Id)) 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 { for _, player := range pR.Players {
playerMetas[player.Id] = &pb.PlayerMeta{ playerMetas[player.Id] = &PlayerDownsyncMeta{
Id: player.Id, Id: player.Id,
Name: player.Name, Name: player.Name,
DisplayName: player.DisplayName, DisplayName: player.DisplayName,
Avatar: player.Avatar, Avatar: player.Avatar,
JoinIndex: player.JoinIndex, ColliderRadius: player.ColliderRadius, // hardcoded for now
JoinIndex: player.JoinIndex,
} }
} }
battleReadyToStartFrame := &pb.RoomDownsyncFrame{ battleReadyToStartFrame := &RoomDownsyncFrame{
Id: DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START, Id: DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START,
Players: toPbPlayers(pR.Players), Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas, PlayerMetas: playerMetas,
@@ -773,6 +783,9 @@ func (pR *Room) Dismiss() {
func (pR *Room) OnDismissed() { func (pR *Room) OnDismissed() {
// Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference. // Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference.
pR.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.Players = make(map[int32]*Player)
pR.PlayersArr = make([]*Player, pR.Capacity) pR.PlayersArr = make([]*Player, pR.Capacity)
pR.CollisionSysMap = make(map[int32]*resolv.Object) pR.CollisionSysMap = make(map[int32]*resolv.Object)
@@ -794,13 +807,14 @@ func (pR *Room) OnDismissed() {
pR.NstDelayFrames = 8 pR.NstDelayFrames = 8
pR.InputScaleFrames = uint32(2) pR.InputScaleFrames = uint32(2)
pR.ServerFps = 60 pR.ServerFps = 60
pR.RollbackEstimatedDt = 0.016667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript
pR.RollbackEstimatedDtMillis = 16.667 // Use fixed-and-low-precision to mitigate the inconsistent floating-point-number issue between Golang and JavaScript pR.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.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for preventing FAST FRAME
pR.BattleDurationFrames = 30 * pR.ServerFps pR.BattleDurationFrames = 30 * pR.ServerFps
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1) pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
pR.InputFrameUpsyncDelayTolerance = 2 pR.InputFrameUpsyncDelayTolerance = 2
pR.MaxChasingRenderFramesPerUpdate = 10 pR.MaxChasingRenderFramesPerUpdate = 5
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
pR.ChooseStage() pR.ChooseStage()
pR.EffectivePlayerCount = 0 pR.EffectivePlayerCount = 0
@@ -911,16 +925,15 @@ func (pR *Room) onPlayerAdded(playerId int32) {
// Lazily assign the initial position of "Player" for "RoomDownsyncFrame". // Lazily assign the initial position of "Player" for "RoomDownsyncFrame".
playerPosList := *(pR.RawBattleStrToVec2DListMap["PlayerStartingPos"]) 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)) 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 { 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)) 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].VirtualGridX, pR.Players[playerId].VirtualGridY = WorldToVirtualGridPos(playerPos.X, playerPos.Y, pR.WorldToVirtualGridRatio)
pR.Players[playerId].Y = playerPos.Y
break break
} }
@@ -948,14 +961,15 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
return false return false
} }
playerMetas := make(map[int32]*pb.PlayerMeta, 0) playerMetas := make(map[int32]*PlayerDownsyncMeta, 0)
for _, eachPlayer := range pR.Players { for _, eachPlayer := range pR.Players {
playerMetas[eachPlayer.Id] = &pb.PlayerMeta{ playerMetas[eachPlayer.Id] = &PlayerDownsyncMeta{
Id: eachPlayer.Id, Id: eachPlayer.Id,
Name: eachPlayer.Name, Name: eachPlayer.Name,
DisplayName: eachPlayer.DisplayName, DisplayName: eachPlayer.DisplayName,
Avatar: eachPlayer.Avatar, Avatar: eachPlayer.Avatar,
JoinIndex: eachPlayer.JoinIndex, JoinIndex: eachPlayer.JoinIndex,
ColliderRadius: eachPlayer.ColliderRadius,
} }
} }
@@ -971,14 +985,14 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
*/ */
switch targetPlayer.BattleState { switch targetPlayer.BattleState {
case PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK: case PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK:
playerAckedFrame := &pb.RoomDownsyncFrame{ playerAckedFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId, Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players), Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas, PlayerMetas: playerMetas,
} }
pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, eachPlayer.Id) pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, eachPlayer.Id)
case PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK: case PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
playerAckedFrame := &pb.RoomDownsyncFrame{ playerAckedFrame := &RoomDownsyncFrame{
Id: pR.RenderFrameId, Id: pR.RenderFrameId,
Players: toPbPlayers(pR.Players), Players: toPbPlayers(pR.Players),
PlayerMetas: playerMetas, PlayerMetas: playerMetas,
@@ -1009,14 +1023,14 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
return true 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() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
pR.PlayerSignalToCloseDict[playerId](Constants.RetCode.UnknownError, fmt.Sprintf("%v", r)) pR.PlayerSignalToCloseDict[playerId](Constants.RetCode.UnknownError, fmt.Sprintf("%v", r))
} }
}() }()
pResp := &pb.WsResp{ pResp := &WsResp{
Ret: int32(Constants.RetCode.Ok), Ret: int32(Constants.RetCode.Ok),
Act: act, Act: act,
Rdf: roomDownsyncFrame, Rdf: roomDownsyncFrame,
@@ -1037,28 +1051,28 @@ func (pR *Room) shouldPrefabInputFrameDownsync(renderFrameId int32) bool {
return ((renderFrameId & ((1 << pR.InputScaleFrames) - 1)) == 0) 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, 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, - EITHER it's due to local lag and bad network,
- OR there's no change w.r.t. to its prev cmd. - 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 { if 0 == inputFrameId && 0 == pR.InputsBuffer.Cnt {
currInputFrameDownsync = &pb.InputFrameDownsync{ currInputFrameDownsync = &InputFrameDownsync{
InputFrameId: 0, InputFrameId: 0,
InputList: make([]uint64, pR.Capacity), InputList: make([]uint64, pR.Capacity),
ConfirmedList: uint64(0), ConfirmedList: uint64(0),
} }
} else { } else {
tmp := pR.InputsBuffer.GetByFrameId(inputFrameId - 1) tmp := pR.InputsBuffer.GetByFrameId(inputFrameId - 1) // There's no need for the backend to find the "lastAllConfirmed inputs" for prefabbing, either "BackendDynamicsEnabled" is true or false
if nil == tmp { if nil == tmp {
panic(fmt.Sprintf("Error prefabbing inputFrameDownsync: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false))) 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 currInputList := prevInputFrameDownsync.InputList // Would be a clone of the values
currInputFrameDownsync = &pb.InputFrameDownsync{ currInputFrameDownsync = &InputFrameDownsync{
InputFrameId: inputFrameId, InputFrameId: inputFrameId,
InputList: currInputList, InputList: currInputList,
ConfirmedList: uint64(0), ConfirmedList: uint64(0),
@@ -1069,6 +1083,42 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDowns
return currInputFrameDownsync 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] = inputFrameUpsync.Encoded
inputFrameDownsync.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr)
}
if allConfirmedMask == inputFrameDownsync.ConfirmedList {
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
} else {
break
}
}
}
func (pR *Room) forceConfirmationIfApplicable() uint64 { 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 // 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 renderFrameId1 := (pR.RenderFrameId - pR.NstDelayFrames) // the renderFrameId which should've been rendered on frontend
@@ -1091,24 +1141,12 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
if nil == tmp { 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))) 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) totPlayerCnt := uint32(pR.Capacity)
allConfirmedMask := uint64((1 << totPlayerCnt) - 1) allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
// Force confirmation of "inputFrame2" // Force confirmation of "inputFrame2"
inputFrame2 := tmp.(*InputFrameDownsync)
oldConfirmedList := inputFrame2.ConfirmedList oldConfirmedList := inputFrame2.ConfirmedList
unconfirmedMask := (oldConfirmedList ^ allConfirmedMask) unconfirmedMask := (oldConfirmedList ^ allConfirmedMask)
inputFrame2.ConfirmedList = allConfirmedMask inputFrame2.ConfirmedList = allConfirmedMask
@@ -1117,7 +1155,7 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
return unconfirmedMask return unconfirmedMask
} }
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32) { func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32, spaceOffsetX, spaceOffsetY float64) {
if fromRenderFrameId >= toRenderFrameId { if fromRenderFrameId >= toRenderFrameId {
return return
} }
@@ -1127,7 +1165,13 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
allConfirmedMask := uint64((1 << totPlayerCnt) - 1) allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
for collisionSysRenderFrameId := fromRenderFrameId; collisionSysRenderFrameId < toRenderFrameId; collisionSysRenderFrameId++ { 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) delayedInputFrameId := pR.ConvertToInputFrameId(collisionSysRenderFrameId, pR.InputDelayFrames)
var delayedInputFrame *InputFrameDownsync = nil
if 0 <= delayedInputFrameId { if 0 <= delayedInputFrameId {
if delayedInputFrameId > pR.LastAllConfirmedInputFrameId { 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))) 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)))
@@ -1136,76 +1180,135 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
if nil == tmp { 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))) 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. // [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) atomic.StoreUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask)
}
inputList := delayedInputFrame.InputList nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap)
// Ordered by joinIndex to guarantee determinism // Update in the latest player pointers
for _, player := range pR.PlayersArr { for playerId, playerDownsync := range nextRenderFrame.Players {
joinIndex := player.JoinIndex pR.Players[playerId].VirtualGridX = playerDownsync.VirtualGridX
encodedInput := inputList[joinIndex-1] pR.Players[playerId].VirtualGridY = playerDownsync.VirtualGridY
decodedInput := DIRECTION_DECODER[encodedInput] pR.Players[playerId].Dir.Dx = playerDownsync.Dir.Dx
decodedInputSpeedFactor := DIRECTION_DECODER_INVERSE_LENGTH[encodedInput] pR.Players[playerId].Dir.Dy = playerDownsync.Dir.Dy
if 0.0 == decodedInputSpeedFactor { }
continue pR.RenderFrameBuffer.Put(nextRenderFrame)
} pR.CurDynamicsRenderFrameId++
baseChange := player.Speed * pR.RollbackEstimatedDt * decodedInputSpeedFactor }
dx := baseChange * float64(decodedInput[0]) }
dy := baseChange * float64(decodedInput[1])
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex // TODO: Write unit-test for this function to compare with its frontend counter part
playerCollider := pR.CollisionSysMap[collisionPlayerIndex] func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame {
if collision := playerCollider.Check(dx, dy, "Barrier"); collision != nil { nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity)
changeWithCollision := collision.ContactWithObject(collision.Objects[0]) // Make a copy first
Logger.Info(fmt.Sprintf("Collided: roomId=%v, playerId=%v, orig dx=%v, orig dy=%v, proposed new dx =%v, proposed new dy=%v", pR.Id, player.Id, dx, dy, changeWithCollision.X(), changeWithCollision.Y())) for playerId, currPlayerDownsync := range currRenderFrame.Players {
// FIXME: Use a mechanism equivalent to that of the frontend! nextRenderFramePlayers[playerId] = &PlayerDownsync{
// dx = changeWithCollision.X() Id: playerId,
// dy = changeWithCollision.Y() VirtualGridX: currPlayerDownsync.VirtualGridX,
dx = 0 VirtualGridY: currPlayerDownsync.VirtualGridY,
dy = 0 Dir: &Direction{
} Dx: currPlayerDownsync.Dir.Dx,
playerCollider.X += dx Dy: currPlayerDownsync.Dir.Dy,
playerCollider.Y += dy },
// Update in "collision space" Speed: currPlayerDownsync.Speed,
playerCollider.Update() BattleState: currPlayerDownsync.BattleState,
Score: currPlayerDownsync.Score,
Removed: currPlayerDownsync.Removed,
JoinIndex: currPlayerDownsync.JoinIndex,
}
}
player.Dir.Dx = decodedInput[0] toRet := &RoomDownsyncFrame{
player.Dir.Dy = decodedInput[1] Id: currRenderFrame.Id + 1,
player.X += dx Players: nextRenderFramePlayers,
player.Y += dy 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]
decodedInput := pR.decodeInput(inputList[joinIndex-1])
proposedVirtualGridDx, proposedVirtualGridDy := (decodedInput.Dx + decodedInput.Dx*currPlayerDownsync.Speed), (decodedInput.Dy + decodedInput.Dy*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 != decodedInput.Dx || 0 != decodedInput.Dy {
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.Dx
nextRenderFramePlayers[playerId].Dir.Dy = decodedInput.Dy
} }
} }
newRenderFrame := pb.RoomDownsyncFrame{ // handle pushbacks upon collision after all movements treated as simultaneous
Id: collisionSysRenderFrameId + 1, for _, player := range pR.Players {
Players: toPbPlayers(pR.Players), joinIndex := player.JoinIndex
CountdownNanos: (pR.BattleDurationNanos - int64(collisionSysRenderFrameId)*pR.RollbackEstimatedDtNanos), 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) decodeInput(encodedInput uint64) *InputFrameDecoded {
encodedDirection := (encodedInput & 0xf)
btnALevel := int32((encodedInput >> 4) & 1)
return &InputFrameDecoded{
Dx: DIRECTION_DECODER[encodedDirection][0],
Dy: DIRECTION_DECODER[encodedDirection][1],
BtnALevel: btnALevel,
}
} }
func (pR *Room) inputFrameIdDebuggable(inputFrameId int32) bool { func (pR *Room) inputFrameIdDebuggable(inputFrameId int32) bool {
return 0 == (inputFrameId % 10) return 0 == (inputFrameId % 10)
} }
func (pR *Room) refreshColliders() { func (pR *Room) refreshColliders(spaceW, spaceH int32) {
playerColliderRadius := float64(12) // hardcoded
// Kindly note that by now, we've already got all the shapes in the tmx file into "pR.(Players | Barriers)" from "ParseTmxLayersAndGroups" // Kindly note that by now, we've already got all the shapes in the tmx file into "pR.(Players | Barriers)" from "ParseTmxLayersAndGroups"
spaceW := pR.StageDiscreteW * pR.StageTileW
spaceH := pR.StageDiscreteH * pR.StageTileH
spaceOffsetX := float64(spaceW) * 0.5 minStep := (int(float64(pR.PlayerDefaultSpeed)*pR.VirtualGridToWorldRatio) << 2) // the approx minimum distance a player can move per frame in world coordinate
spaceOffsetY := float64(spaceH) * 0.5 space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep) // allocate a new collision space everytime after a battle is settled
minStep := int(3) // the approx minimum distance a player can move per frame
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 { for _, player := range pR.Players {
playerCollider := resolv.NewObject(player.X+spaceOffsetX, player.Y+spaceOffsetY, playerColliderRadius*2, playerColliderRadius*2) wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio)
playerColliderShape := resolv.NewCircle(0, 0, playerColliderRadius*2) playerCollider := GenerateRectCollider(wx, wy, player.ColliderRadius*2, player.ColliderRadius*2, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player")
playerCollider.SetShape(playerColliderShape)
space.Add(playerCollider) space.Add(playerCollider)
// Keep track of the collider in "pR.CollisionSysMap" // Keep track of the collider in "pR.CollisionSysMap"
joinIndex := player.JoinIndex joinIndex := player.JoinIndex
@@ -1215,31 +1318,8 @@ func (pR *Room) refreshColliders() {
} }
for _, barrier := range pR.Barriers { for _, barrier := range pR.Barriers {
boundaryUnaligned := barrier.Boundary
var w float64 = 0 barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier")
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, p.Y)
}
barrierCollider := resolv.NewObject(barrier.Boundary.Anchor.X+spaceOffsetX, barrier.Boundary.Anchor.Y+spaceOffsetY, w, h, "Barrier")
barrierCollider.SetShape(barrierColliderShape)
space.Add(barrierCollider) space.Add(barrierCollider)
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
package storage package storage
import ( import (
. "server/common" . "battle_srv/common"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"

View File

@@ -1,8 +1,8 @@
package storage package storage
import ( import (
. "battle_srv/common"
"fmt" "fmt"
. "server/common"
"github.com/go-redis/redis" "github.com/go-redis/redis"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

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

4
charts/README.md Normal file
View File

@@ -0,0 +1,4 @@
# GIF creation cmd reference
```
ffmpeg -ss 12 -t 13 -i input.mp4 -vf "fps=10,scale=480:-1" -loop 0 output.gif
```

BIN
charts/RollbackAndChase.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 684 KiB

View File

@@ -3,11 +3,11 @@ module viscol
go 1.19 go 1.19
require ( require (
dnmshared v0.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/hajimehoshi/ebiten/v2 v2.4.7 github.com/hajimehoshi/ebiten/v2 v2.4.7
github.com/solarlune/resolv v0.5.1 github.com/solarlune/resolv v0.5.1
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
dnmshared v0.0.0
) )
require ( require (
@@ -22,6 +22,7 @@ require (
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
google.golang.org/protobuf v1.28.1 // indirect
) )
replace dnmshared => ../dnmshared replace dnmshared => ../dnmshared

View File

@@ -7,6 +7,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad h1:kX51IjbsJP
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220806181222-55e207c401ad/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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 h1:y7zcy02/UgO24IL3COqYtrRZzhRucNBtmCo/SNU648k=
github.com/hajimehoshi/bitmapfont/v2 v2.2.1/go.mod h1:wjrYAy8vKgj9JsFgnYAOK346/uvE22TlmqouzdnYIs0= 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 h1:XuvB7R0Rbw/O7g6vNU8gqr5b9e7MNhhAONMSsyreLDI=
@@ -93,4 +95,8 @@ golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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-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-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= 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=

View File

@@ -85,8 +85,9 @@ type Game struct {
func NewGame() *Game { func NewGame() *Game {
// stageName := "simple" // Use this for calibration // stageName := "simple" // Use this for calibration in isometric orientation
stageName := "richsoil" // stageName := "richsoil"
stageName := "dungeon"
stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap, err := parseStage(stageName) stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap, err := parseStage(stageName)
if nil != err { if nil != err {
panic(err) panic(err)
@@ -160,7 +161,6 @@ func (g *Game) DebugDraw(screen *ebiten.Image, space *resolv.Space) {
ebitenutil.DrawLine(screen, cx, cy+ch, cx, cy, drawColor) ebitenutil.DrawLine(screen, cx, cy+ch, cx, cy, drawColor)
} }
} }
} }
func (g *Game) Layout(w, h int) (int, int) { func (g *Game) Layout(w, h int) (int, int) {

View File

@@ -2,14 +2,12 @@ package main
import ( import (
. "dnmshared" . "dnmshared"
. "dnmshared/sharedprotos"
"fmt" "fmt"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/solarlune/resolv" "github.com/solarlune/resolv"
"go.uber.org/zap" "go.uber.org/zap"
"image/color" "image/color"
"math"
) )
type WorldColliderDisplay struct { type WorldColliderDisplay struct {
@@ -22,7 +20,7 @@ func (world *WorldColliderDisplay) Init() {
func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTileW, stageTileH int32, playerPosMap StrToVec2DListMap, barrierMap StrToPolygon2DListMap) *WorldColliderDisplay { func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTileW, stageTileH int32, playerPosMap StrToVec2DListMap, barrierMap StrToPolygon2DListMap) *WorldColliderDisplay {
playerList := *(playerPosMap["PlayerStartingPos"]) playerPosList := *(playerPosMap["PlayerStartingPos"])
barrierList := *(barrierMap["Barrier"]) barrierList := *(barrierMap["Barrier"])
world := &WorldColliderDisplay{Game: game} world := &WorldColliderDisplay{Game: game}
@@ -35,52 +33,58 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi
spaceOffsetX := float64(spaceW) * 0.5 spaceOffsetX := float64(spaceW) * 0.5
spaceOffsetY := float64(spaceH) * 0.5 spaceOffsetY := float64(spaceH) * 0.5
// TODO: Move collider y-axis transformation to a "dnmshared" virtualGridToWorldRatio := 0.1
playerColliderRadius := float64(12) // hardcoded playerDefaultSpeed := 20
space := resolv.NewSpace(int(spaceW), int(spaceH), 16, 16) minStep := (int(float64(playerDefaultSpeed)*virtualGridToWorldRatio) << 2)
for _, player := range playerList { playerColliderRadius := float64(16)
playerCollider := resolv.NewObject(player.X+spaceOffsetX, player.Y+spaceOffsetY, playerColliderRadius*2, playerColliderRadius*2, "Player") playerColliders := make([]*resolv.Object, len(playerPosList.Eles))
playerColliderShape := resolv.NewCircle(0, 0, playerColliderRadius*2) space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep)
playerCollider.SetShape(playerColliderShape) 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) space.Add(playerCollider)
} }
barrierLocalId := 0 barrierLocalId := 0
for _, barrierUnaligned := range barrierList { for _, barrierUnaligned := range barrierList.Eles {
barrier := AlignPolygon2DToBoundingBox(barrierUnaligned) barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
Logger.Info(fmt.Sprintf("Added barrier: shape=%v", ConvexPolygonStr(barrierCollider.Shape.(*resolv.ConvexPolygon))))
var w float64 = 0
var h float64 = 0
for i, pi := range barrier.Points {
for j, pj := range barrier.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 i := 0; i < len(barrier.Points); i++ {
p := barrier.Points[i]
barrierColliderShape.AddPoints(p.X, p.Y)
}
barrierCollider := resolv.NewObject(barrier.Anchor.X+spaceOffsetX, barrier.Anchor.Y+spaceOffsetY, w, h, "Barrier")
barrierCollider.SetShape(barrierColliderShape)
space.Add(barrierCollider) space.Add(barrierCollider)
barrierLocalId++ barrierLocalId++
} }
world.Space = space world.Space = space
moveToCollide := false
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 return world
} }
@@ -92,9 +96,8 @@ func (world *WorldColliderDisplay) Draw(screen *ebiten.Image) {
for _, o := range world.Space.Objects() { for _, o := range world.Space.Objects() {
if o.HasTags("Player") { if o.HasTags("Player") {
circle := o.Shape.(*resolv.Circle)
drawColor := color.RGBA{0, 255, 0, 255} drawColor := color.RGBA{0, 255, 0, 255}
ebitenutil.DrawCircle(screen, circle.X, circle.Y, circle.Radius, drawColor) DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)
} else { } else {
drawColor := color.RGBA{60, 60, 60, 255} drawColor := color.RGBA{60, 60, 60, 255}
DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor) DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)

View File

@@ -1,43 +1,46 @@
package dnmshared package dnmshared
import ( import (
. "dnmshared/sharedprotos"
"math" "math"
) )
// Use type `float64` for json unmarshalling of numbers. func NormVec2D(dx, dy float64) Vec2D {
type Direction struct { return Vec2D{X: dy, Y: -dx}
Dx int32 `json:"dx,omitempty"`
Dy int32 `json:"dy,omitempty"`
} }
type Vec2D struct { func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D {
X float64 `json:"x,omitempty"` // Transform again to put "anchor" at the top-left point of the bounding box for "resolv"
Y float64 `json:"y,omitempty"` 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
}
}
type Polygon2D struct { // Now "input.Anchor" should move to "input.Anchor+boundingBoxTL", thus "boundingBoxTL" is also the value of the negative diff for all "input.Points"
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. output := &Polygon2D{
Points []*Vec2D `json:"-"` 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 {
When used to represent a "polyline directly drawn in a `Tmx file`", we can initialize both "Anchor" and "Points" simultaneously. output.Points[i] = &Vec2D{
X: p.X - boundingBoxTL.X,
Y: p.Y - boundingBoxTL.Y,
}
}
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`". return output
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 Distance(pt1 *Vec2D, pt2 *Vec2D) float64 { func Distance(pt1 *Vec2D, pt2 *Vec2D) float64 {

View File

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

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

@@ -3,6 +3,7 @@ package dnmshared
import ( import (
"bytes" "bytes"
"compress/zlib" "compress/zlib"
. "dnmshared/sharedprotos"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"errors" "errors"
@@ -173,8 +174,6 @@ func (l *TmxLayer) decodeBase64() ([]uint32, error) {
return gids, nil return gids, nil
} }
type Vec2DList []*Vec2D
type Polygon2DList []*Polygon2D
type StrToVec2DListMap map[string]*Vec2DList type StrToVec2DListMap map[string]*Vec2DList
type StrToPolygon2DListMap map[string]*Polygon2DList type StrToPolygon2DListMap map[string]*Polygon2DList
@@ -233,10 +232,8 @@ func tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns *TmxMap, singleObjInTsxFile *T
pointsCount := len(singleValueArray) pointsCount := len(singleValueArray)
thePolygon2DFromPolyline := &Polygon2D{ thePolygon2DFromPolyline := &Polygon2D{
Anchor: nil, Anchor: nil,
Points: make([]*Vec2D, pointsCount), Points: make([]*Vec2D, pointsCount),
TileWidth: pTsxIns.TileWidth,
TileHeight: pTsxIns.TileHeight,
} }
/* /*
@@ -327,16 +324,17 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
if _, ok := theStrToPolygon2DListMap[key]; ok { if _, ok := theStrToPolygon2DListMap[key]; ok {
pThePolygon2DList = theStrToPolygon2DListMap[key] pThePolygon2DList = theStrToPolygon2DListMap[key]
} else { } else {
thePolygon2DList := make(Polygon2DList, 0) pThePolygon2DList = &Polygon2DList{
theStrToPolygon2DListMap[key] = &thePolygon2DList Eles: make([]*Polygon2D, 0),
pThePolygon2DList = theStrToPolygon2DListMap[key] }
theStrToPolygon2DListMap[key] = pThePolygon2DList
} }
thePolygon2DFromPolyline, err := tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns) thePolygon2DFromPolyline, err := tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
if nil != err { if nil != err {
panic(err) panic(err)
} }
*pThePolygon2DList = append(*pThePolygon2DList, thePolygon2DFromPolyline) pThePolygon2DList.Eles = append(pThePolygon2DList.Eles, thePolygon2DFromPolyline)
} }
} }
return nil return nil
@@ -352,8 +350,10 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
var pTheVec2DListToCache *Vec2DList var pTheVec2DListToCache *Vec2DList
_, ok := toRetStrToVec2DListMap[objGroup.Name] _, ok := toRetStrToVec2DListMap[objGroup.Name]
if false == ok { if false == ok {
theVec2DListToCache := make(Vec2DList, 0) pTheVec2DListToCache = &Vec2DList{
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache Eles: make([]*Vec2D, 0),
}
toRetStrToVec2DListMap[objGroup.Name] = pTheVec2DListToCache
} }
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name] pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects { for _, singleObjInTmxFile := range objGroup.Objects {
@@ -362,17 +362,18 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
Y: singleObjInTmxFile.Y, Y: singleObjInTmxFile.Y,
} }
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos) thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld) pTheVec2DListToCache.Eles = append(pTheVec2DListToCache.Eles, &thePosInWorld)
} }
case "Barrier": case "Barrier":
// Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]". // Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]".
var pThePolygon2DListToCache *Polygon2DList var pThePolygon2DListToCache *Polygon2DList
_, ok := toRetStrToPolygon2DListMap[objGroup.Name] _, ok := toRetStrToPolygon2DListMap[objGroup.Name]
if false == ok { if false == ok {
thePolygon2DListToCache := make(Polygon2DList, 0) pThePolygon2DListToCache = &Polygon2DList{
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache Eles: make([]*Polygon2D, 0),
}
toRetStrToPolygon2DListMap[objGroup.Name] = pThePolygon2DListToCache
} }
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects { for _, singleObjInTmxFile := range objGroup.Objects {
if nil == singleObjInTmxFile.Polyline { if nil == singleObjInTmxFile.Polyline {
@@ -386,7 +387,7 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
if nil != err { if nil != err {
panic(err) panic(err)
} }
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld) pThePolygon2DListToCache.Eles = append(pThePolygon2DListToCache.Eles, thePolygon2DInWorld)
} }
default: default:
} }
@@ -405,6 +406,12 @@ type TileRectilinearSize struct {
} }
func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerVec *Vec2D) Vec2D { func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerVec *Vec2D) Vec2D {
if "orthogonal" == pTmxMapIns.Orientation {
return Vec2D{
X: continuousObjLayerVec.X,
Y: -continuousObjLayerVec.Y,
}
}
var tileRectilinearSize TileRectilinearSize var tileRectilinearSize TileRectilinearSize
tileRectilinearSize.Width = float64(pTmxMapIns.TileWidth) tileRectilinearSize.Width = float64(pTmxMapIns.TileWidth)
tileRectilinearSize.Height = float64(pTmxMapIns.TileHeight) tileRectilinearSize.Height = float64(pTmxMapIns.TileHeight)
@@ -427,55 +434,24 @@ func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuous
} }
func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continuousObjLayerOffset *Vec2D) Vec2D { func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continuousObjLayerOffset *Vec2D) Vec2D {
layerOffset := Vec2D{ var layerOffset Vec2D
X: 0, if "orthogonal" == pTmxMapIns.Orientation {
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5, layerOffset = Vec2D{
X: -float64(pTmxMapIns.Width*pTmxMapIns.TileWidth) * 0.5,
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5,
}
} else {
// "isometric" == pTmxMapIns.Orientation
layerOffset = Vec2D{
X: 0,
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5,
}
} }
calibratedVec := continuousObjLayerOffset convertedVec := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerOffset)
convertedVec := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(calibratedVec)
toRet := Vec2D{ return Vec2D{
X: layerOffset.X + convertedVec.X, X: layerOffset.X + convertedVec.X,
Y: layerOffset.Y + convertedVec.Y, Y: layerOffset.Y + convertedVec.Y,
} }
return toRet
} }
func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D {
// Transform again to put "anchor" at the top-left point of the bounding box for "resolv"
float64Max := float64(99999999999999.9)
boundingBoxTL := &Vec2D{
X: float64Max,
Y: float64Max,
}
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)),
TileWidth: input.TileWidth,
TileHeight: input.TileHeight,
}
for i, p := range input.Points {
output.Points[i] = &Vec2D{
X: p.X-boundingBoxTL.X,
Y: p.Y-boundingBoxTL.Y,
}
}
return output
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{"width":128,"imagePath":"SoldierElf_tex.png","height":128,"name":"SoldierElf","SubTexture":[{"frameHeight":45,"y":1,"frameX":0,"width":34,"frameY":0,"height":44,"name":"cape","frameWidth":34,"x":70},{"width":10,"y":107,"height":14,"name":"shouder_l","x":74},{"width":11,"y":107,"height":14,"name":"forearm_l","x":61},{"width":15,"y":93,"height":16,"name":"hand_l","x":1},{"width":30,"y":61,"height":30,"name":"weapon_hand_l","x":1},{"width":8,"y":101,"height":11,"name":"thigh_l","x":86},{"width":12,"y":93,"height":17,"name":"calf_l","x":18},{"width":20,"y":113,"height":8,"name":"foot_l","x":39},{"width":28,"y":61,"height":31,"name":"pelvis","x":33},{"width":8,"y":88,"height":11,"name":"thigh_r","x":77},{"width":12,"y":88,"height":17,"name":"calf_r","x":63},{"width":20,"y":113,"height":8,"name":"foot_r","x":17},{"width":13,"y":94,"height":12,"name":"shouder_r","x":45},{"width":67,"y":1,"height":58,"name":"chest","x":1},{"width":11,"y":94,"height":17,"name":"forearm_r","x":32},{"width":14,"y":111,"height":13,"name":"hand_r","x":1},{"frameHeight":39,"y":47,"frameX":-2,"width":34,"frameY":0,"height":39,"name":"we_bl_4_f_1","frameWidth":36,"x":70}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1 @@
{"imagePath":"Soldier_02_tex.png","width":128,"name":"Soldier_02","SubTexture":[{"x":53,"y":44,"width":23,"name":"biu","height":22},{"x":76,"y":68,"width":9,"name":"rightArm","height":14},{"y":35,"frameY":0,"height":32,"frameWidth":29,"frameX":-1,"frameHeight":32,"width":27,"name":"yinmoqe00","x":89},{"x":53,"y":1,"width":34,"name":"body","height":41},{"x":78,"y":44,"width":9,"name":"rightShoulder","height":13},{"y":50,"frameY":0,"height":18,"frameWidth":19,"frameX":0,"frameHeight":18,"width":19,"name":"rightFrontArm","x":23},{"x":23,"y":70,"width":14,"name":"rightHand","height":14},{"y":68,"frameY":0,"height":12,"frameWidth":12,"frameX":0,"frameHeight":12,"width":12,"name":"leftArm","x":62},{"x":1,"y":73,"width":13,"name":"leftShoulder","height":12},{"x":1,"y":50,"width":20,"name":"leftFrontArm","height":21},{"x":89,"y":1,"width":33,"name":"head","height":32},{"x":1,"y":1,"width":50,"name":"head2","height":47},{"x":44,"y":68,"width":16,"name":"leftHand","height":14},{"y":59,"frameY":-2,"height":4,"frameWidth":8,"frameX":-1,"frameHeight":8,"width":4,"name":"huomiao01","x":78}],"height":128}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

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,6 +1,6 @@
{ {
"ver": "1.0.1", "ver": "1.0.1",
"uuid": "51c54820-d753-4be8-a855-5760eed8f7ef", "uuid": "2202f4f4-b792-4dea-8302-633315aded66",
"isSubpackage": false, "isSubpackage": false,
"subpackageName": "", "subpackageName": "",
"subMetas": {} "subMetas": {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"width":128,"SubTexture":[{"frameWidth":34,"y":1,"frameHeight":45,"width":34,"frameX":0,"height":44,"name":"cape","frameY":0,"x":70},{"width":10,"y":107,"height":14,"name":"shouder_l","x":74},{"width":11,"y":107,"height":14,"name":"forearm_l","x":61},{"width":15,"y":93,"height":16,"name":"hand_l","x":1},{"width":30,"y":61,"height":30,"name":"weapon_hand_l","x":1},{"width":8,"y":88,"height":11,"name":"thigh_l","x":77},{"width":12,"y":93,"height":17,"name":"calf_l","x":18},{"width":20,"y":113,"height":8,"name":"foot_l","x":39},{"width":28,"y":61,"height":31,"name":"pelvis","x":33},{"width":8,"y":101,"height":11,"name":"thigh_r","x":86},{"width":12,"y":88,"height":17,"name":"calf_r","x":63},{"width":20,"y":113,"height":8,"name":"foot_r","x":17},{"width":13,"y":94,"height":12,"name":"shouder_r","x":45},{"width":67,"y":1,"height":58,"name":"chest","x":1},{"width":11,"y":94,"height":17,"name":"forearm_r","x":32},{"width":14,"y":111,"height":13,"name":"hand_r","x":1},{"frameWidth":36,"y":47,"frameHeight":39,"width":34,"frameX":-2,"height":39,"name":"we_bl_4_f_1","frameY":0,"x":70}],"height":128,"name":"SoldierElf","imagePath":"SoldierElf_tex.png"}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.0",
"uuid": "24d7bb8f-577c-4e5d-b730-56613ca8685d",
"atlasJson": "{\"width\":128,\"SubTexture\":[{\"frameWidth\":34,\"y\":1,\"frameHeight\":45,\"width\":34,\"frameX\":0,\"height\":44,\"name\":\"cape\",\"frameY\":0,\"x\":70},{\"width\":10,\"y\":107,\"height\":14,\"name\":\"shouder_l\",\"x\":74},{\"width\":11,\"y\":107,\"height\":14,\"name\":\"forearm_l\",\"x\":61},{\"width\":15,\"y\":93,\"height\":16,\"name\":\"hand_l\",\"x\":1},{\"width\":30,\"y\":61,\"height\":30,\"name\":\"weapon_hand_l\",\"x\":1},{\"width\":8,\"y\":88,\"height\":11,\"name\":\"thigh_l\",\"x\":77},{\"width\":12,\"y\":93,\"height\":17,\"name\":\"calf_l\",\"x\":18},{\"width\":20,\"y\":113,\"height\":8,\"name\":\"foot_l\",\"x\":39},{\"width\":28,\"y\":61,\"height\":31,\"name\":\"pelvis\",\"x\":33},{\"width\":8,\"y\":101,\"height\":11,\"name\":\"thigh_r\",\"x\":86},{\"width\":12,\"y\":88,\"height\":17,\"name\":\"calf_r\",\"x\":63},{\"width\":20,\"y\":113,\"height\":8,\"name\":\"foot_r\",\"x\":17},{\"width\":13,\"y\":94,\"height\":12,\"name\":\"shouder_r\",\"x\":45},{\"width\":67,\"y\":1,\"height\":58,\"name\":\"chest\",\"x\":1},{\"width\":11,\"y\":94,\"height\":17,\"name\":\"forearm_r\",\"x\":32},{\"width\":14,\"y\":111,\"height\":13,\"name\":\"hand_r\",\"x\":1},{\"frameWidth\":36,\"y\":47,\"frameHeight\":39,\"width\":34,\"frameX\":-2,\"height\":39,\"name\":\"we_bl_4_f_1\",\"frameY\":0,\"x\":70}],\"height\":128,\"name\":\"SoldierElf\",\"imagePath\":\"SoldierElf_tex.png\"}",
"texture": "050fb016-1a1f-4341-8367-283bfeddc4a8",
"subMetas": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,6 +1,6 @@
{ {
"ver": "2.3.3", "ver": "2.3.3",
"uuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd", "uuid": "050fb016-1a1f-4341-8367-283bfeddc4a8",
"type": "sprite", "type": "sprite",
"wrapMode": "clamp", "wrapMode": "clamp",
"filterMode": "bilinear", "filterMode": "bilinear",
@@ -9,21 +9,21 @@
"packable": true, "packable": true,
"platformSettings": {}, "platformSettings": {},
"subMetas": { "subMetas": {
"Tile_W300_H300_S01": { "SoldierElf_tex": {
"ver": "1.0.4", "ver": "1.0.4",
"uuid": "66b49304-7b5b-442c-92a5-d2b368abf659", "uuid": "c62e1779-f92b-40d3-bf4f-7ab747e33d6e",
"rawTextureUuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd", "rawTextureUuid": "050fb016-1a1f-4341-8367-283bfeddc4a8",
"trimType": "auto", "trimType": "auto",
"trimThreshold": 1, "trimThreshold": 1,
"rotated": false, "rotated": false,
"offsetX": 4, "offsetX": -11.5,
"offsetY": -24.5, "offsetY": 1.5,
"trimX": 97, "trimX": 1,
"trimY": 85, "trimY": 1,
"width": 114, "width": 103,
"height": 179, "height": 123,
"rawWidth": 300, "rawWidth": 128,
"rawHeight": 300, "rawHeight": 128,
"borderTop": 0, "borderTop": 0,
"borderBottom": 0, "borderBottom": 0,
"borderLeft": 0, "borderLeft": 0,

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "f1176719-d1d6-4af5-89c6-ddff16ab85fd",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"width":128,"SubTexture":[{"frameWidth":23,"y":44,"frameHeight":22,"width":22,"frameX":-1,"height":22,"name":"biu","frameY":0,"x":53},{"width":9,"y":68,"height":14,"name":"rightArm","x":76},{"frameWidth":29,"y":35,"frameHeight":32,"width":26,"frameX":-1,"height":32,"name":"yinmoqe00","frameY":0,"x":89},{"width":34,"y":1,"height":41,"name":"body","x":53},{"width":9,"y":44,"height":13,"name":"rightShoulder","x":77},{"width":19,"y":50,"height":18,"name":"rightFrontArm","x":23},{"width":14,"y":70,"height":14,"name":"rightHand","x":23},{"width":12,"y":68,"height":12,"name":"leftArm","x":62},{"width":13,"y":73,"height":12,"name":"leftShoulder","x":1},{"width":20,"y":50,"height":21,"name":"leftFrontArm","x":1},{"width":33,"y":1,"height":32,"name":"head","x":89},{"width":50,"y":1,"height":47,"name":"head2","x":1},{"width":16,"y":68,"height":14,"name":"leftHand","x":44},{"frameWidth":8,"y":59,"frameHeight":8,"width":4,"frameX":-1,"height":4,"name":"huomiao01","frameY":-2,"x":77}],"height":128,"name":"SoldierFireGhost","imagePath":"SoldierFireGhost_tex.png"}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.0",
"uuid": "4a9187d5-a9ad-4464-a03c-d2f3cc277051",
"atlasJson": "{\"width\":128,\"SubTexture\":[{\"frameWidth\":23,\"y\":44,\"frameHeight\":22,\"width\":22,\"frameX\":-1,\"height\":22,\"name\":\"biu\",\"frameY\":0,\"x\":53},{\"width\":9,\"y\":68,\"height\":14,\"name\":\"rightArm\",\"x\":76},{\"frameWidth\":29,\"y\":35,\"frameHeight\":32,\"width\":26,\"frameX\":-1,\"height\":32,\"name\":\"yinmoqe00\",\"frameY\":0,\"x\":89},{\"width\":34,\"y\":1,\"height\":41,\"name\":\"body\",\"x\":53},{\"width\":9,\"y\":44,\"height\":13,\"name\":\"rightShoulder\",\"x\":77},{\"width\":19,\"y\":50,\"height\":18,\"name\":\"rightFrontArm\",\"x\":23},{\"width\":14,\"y\":70,\"height\":14,\"name\":\"rightHand\",\"x\":23},{\"width\":12,\"y\":68,\"height\":12,\"name\":\"leftArm\",\"x\":62},{\"width\":13,\"y\":73,\"height\":12,\"name\":\"leftShoulder\",\"x\":1},{\"width\":20,\"y\":50,\"height\":21,\"name\":\"leftFrontArm\",\"x\":1},{\"width\":33,\"y\":1,\"height\":32,\"name\":\"head\",\"x\":89},{\"width\":50,\"y\":1,\"height\":47,\"name\":\"head2\",\"x\":1},{\"width\":16,\"y\":68,\"height\":14,\"name\":\"leftHand\",\"x\":44},{\"frameWidth\":8,\"y\":59,\"frameHeight\":8,\"width\":4,\"frameX\":-1,\"height\":4,\"name\":\"huomiao01\",\"frameY\":-2,\"x\":77}],\"height\":128,\"name\":\"SoldierFireGhost\",\"imagePath\":\"SoldierFireGhost_tex.png\"}",
"texture": "700d963b-2192-4219-a066-8be5b3db7453",
"subMetas": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "700d963b-2192-4219-a066-8be5b3db7453",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"SoldierFireGhost_tex": {
"ver": "1.0.4",
"uuid": "8ef8a6b3-0bac-4cf1-bba0-ab090f4d9e52",
"rawTextureUuid": "700d963b-2192-4219-a066-8be5b3db7453",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -2.5,
"offsetY": 21,
"trimX": 1,
"trimY": 1,
"width": 121,
"height": 84,
"rawWidth": 128,
"rawHeight": 128,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "5650b341-a420-4d79-a969-39a461e13378",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="64" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="12">
<tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/>
<layer id="1" name="Ground" width="64" height="64">
<data encoding="base64" compression="zlib">
eJzt0jEOwjAUREGLkiLmALRU3In7HwMKkFCUCCwiNrKnmCbVvh8fSikHAAAA/qruYEOr84Zd+vM9re0v+vXr1z9S/7f0l3KZ9dXZ91r67QeAXxyf0jv0Z/tvO9iS7F+T3ucG+Xuk96RvkN4CMJLaIL0VttLy7r19euHdA+zX6YP0vlR3z/3ax2tfarw+TAO0L/VPb3ruXvv3o7z7lv70zuQN0vtgz+6DDSsZ
</data>
</layer>
<objectgroup id="1" name="PlayerStartingPos">
<object id="135" x="512" y="512">
<point/>
</object>
<object id="137" x="640" y="640">
<point/>
</object>
</objectgroup>
<objectgroup id="2" name="Barrier">
<properties>
<property name="type" value="barrier_and_shelter"/>
</properties>
<object id="1" x="400" y="224.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,141.5 17,141.5 17,-1"/>
</object>
<object id="2" x="559.5" y="207.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 1,158.5 17,158.5 17,0"/>
</object>
<object id="3" x="448.5" y="353">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,14.5 -49,14.5 -49,-1.5"/>
</object>
<object id="4" x="577.5" y="351.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -98,1 -98,16 -1,15.5"/>
</object>
<object id="5" x="449.333" y="654.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -178.667,0.666667 -178.667,17.3333 -0.666667,17.3333"/>
</object>
<object id="6" x="432.5" y="703.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -191.667,0.666667 -191.667,17.3333 -0.715174,17.3333"/>
</object>
<object id="7" x="400" y="751">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -191.667,0.666667 -191.667,17.3333 -0.715174,17.3333"/>
</object>
<object id="8" x="-9.33333" y="-13.3333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object>
<object id="9" x="-9.33333" y="1014.67">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object>
<object id="10" x="-14" y="-40">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 4,1110 24,1110 24,-8"/>
</object>
<object id="11" x="1014" y="-42">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 4,1110 24,1110 24,-8"/>
</object>
</objectgroup>
</map>

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.2",
"uuid": "1b802c87-1978-4c6a-bd0b-1f6b8526b3ad",
"subMetas": {}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.2" tiledversion="1.2.3" name="tiles0" tilewidth="16" tileheight="16" tilecount="64" columns="16">
<image source="watabou_pixel_dungeon_orig_files/tiles0.png" width="256" height="64"/>
</tileset>

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "d4cf0e72-7454-4310-b975-beb5e81a63ae",
"subMetas": {}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.2" tiledversion="1.2.3" name="tiles1" tilewidth="16" tileheight="16" tilecount="64" columns="16">
<image source="watabou_pixel_dungeon_orig_files/tiles1.png" width="256" height="64"/>
</tileset>

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "0bcabaac-a406-4b3d-9285-814e00c5b09d",
"subMetas": {}
}

View File

@@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "2a231040-0e69-42b4-a35b-9690e976e71a",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

View File

@@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "acb40b5d-372b-4502-a6bf-f756908f8221",
"subMetas": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,6 +1,6 @@
{ {
"ver": "2.3.3", "ver": "2.3.3",
"uuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a", "uuid": "94f42afa-43e1-4936-b5ac-7bcfe252f9e1",
"type": "sprite", "type": "sprite",
"wrapMode": "clamp", "wrapMode": "clamp",
"filterMode": "bilinear", "filterMode": "bilinear",
@@ -9,21 +9,21 @@
"packable": true, "packable": true,
"platformSettings": {}, "platformSettings": {},
"subMetas": { "subMetas": {
"Tile_W256_H256_S01": { "amulet": {
"ver": "1.0.4", "ver": "1.0.4",
"uuid": "4a23290b-bf5a-4849-ac19-6ebd4b7daa59", "uuid": "52580025-ae7a-4c48-878f-f685bc90e737",
"rawTextureUuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a", "rawTextureUuid": "94f42afa-43e1-4936-b5ac-7bcfe252f9e1",
"trimType": "auto", "trimType": "auto",
"trimThreshold": 1, "trimThreshold": 1,
"rotated": false, "rotated": false,
"offsetX": 0, "offsetX": 0,
"offsetY": 0, "offsetY": 0,
"trimX": 0, "trimX": 0,
"trimY": 64, "trimY": 0,
"width": 1280, "width": 32,
"height": 896, "height": 32,
"rawWidth": 1280, "rawWidth": 32,
"rawHeight": 1024, "rawHeight": 32,
"borderTop": 0, "borderTop": 0,
"borderBottom": 0, "borderBottom": 0,
"borderLeft": 0, "borderLeft": 0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 B

View File

@@ -1,6 +1,6 @@
{ {
"ver": "2.3.3", "ver": "2.3.3",
"uuid": "136d09e9-c33c-45dc-abb7-e367d730c814", "uuid": "a9781031-1dba-4214-9167-080fc1920f92",
"type": "sprite", "type": "sprite",
"wrapMode": "clamp", "wrapMode": "clamp",
"filterMode": "bilinear", "filterMode": "bilinear",
@@ -9,21 +9,21 @@
"packable": true, "packable": true,
"platformSettings": {}, "platformSettings": {},
"subMetas": { "subMetas": {
"Tile_W256_H128_S01": { "arcs1": {
"ver": "1.0.4", "ver": "1.0.4",
"uuid": "7acc48f5-d9c9-4438-8794-57a85590bd97", "uuid": "5430edd4-3e0b-4509-a575-d7c7f8db4e31",
"rawTextureUuid": "136d09e9-c33c-45dc-abb7-e367d730c814", "rawTextureUuid": "a9781031-1dba-4214-9167-080fc1920f92",
"trimType": "auto", "trimType": "auto",
"trimThreshold": 1, "trimThreshold": 1,
"rotated": false, "rotated": false,
"offsetX": 0, "offsetX": 0,
"offsetY": 831, "offsetY": 0,
"trimX": 0, "trimX": 0,
"trimY": 0, "trimY": 0,
"width": 2048, "width": 32,
"height": 386, "height": 32,
"rawWidth": 2048, "rawWidth": 32,
"rawHeight": 2048, "rawHeight": 32,
"borderTop": 0, "borderTop": 0,
"borderBottom": 0, "borderBottom": 0,
"borderLeft": 0, "borderLeft": 0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

View File

@@ -1,6 +1,6 @@
{ {
"ver": "2.3.3", "ver": "2.3.3",
"uuid": "74245e28-6cec-4960-ac41-5b482ad8fd13", "uuid": "940f2fcc-df03-46ed-ad32-d92880efe501",
"type": "sprite", "type": "sprite",
"wrapMode": "clamp", "wrapMode": "clamp",
"filterMode": "bilinear", "filterMode": "bilinear",
@@ -9,21 +9,21 @@
"packable": true, "packable": true,
"platformSettings": {}, "platformSettings": {},
"subMetas": { "subMetas": {
"Tile_W256_H256_S02": { "arcs2": {
"ver": "1.0.4", "ver": "1.0.4",
"uuid": "8fc46c1f-6fb4-4290-99f3-b773b92312b7", "uuid": "11915948-c7bf-41db-b6eb-74b364df7688",
"rawTextureUuid": "74245e28-6cec-4960-ac41-5b482ad8fd13", "rawTextureUuid": "940f2fcc-df03-46ed-ad32-d92880efe501",
"trimType": "auto", "trimType": "auto",
"trimThreshold": 1, "trimThreshold": 1,
"rotated": false, "rotated": false,
"offsetX": 19.5, "offsetX": 0,
"offsetY": 3.5, "offsetY": 0,
"trimX": 89, "trimX": 0,
"trimY": 0, "trimY": 0,
"width": 1409, "width": 64,
"height": 251, "height": 64,
"rawWidth": 1548, "rawWidth": 64,
"rawHeight": 258, "rawHeight": 64,
"borderTop": 0, "borderTop": 0,
"borderBottom": 0, "borderBottom": 0,
"borderLeft": 0, "borderLeft": 0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "ac193c85-fb5d-4d66-b911-81e2254e0009",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"avatars": {
"ver": "1.0.4",
"uuid": "b472e0d1-c866-4a4d-b8f9-4daca275b8db",
"rawTextureUuid": "ac193c85-fb5d-4d66-b911-81e2254e0009",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -16.5,
"offsetY": 2,
"trimX": 2,
"trimY": 0,
"width": 91,
"height": 28,
"rawWidth": 128,
"rawHeight": 32,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "f204f568-cf6e-462e-834e-ef6961dae26e",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"badges": {
"ver": "1.0.4",
"uuid": "fec2a102-8c77-4528-8130-488184e8bf38",
"rawTextureUuid": "f204f568-cf6e-462e-834e-ef6961dae26e",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 128,
"height": 128,
"rawWidth": 128,
"rawHeight": 128,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "90cc6214-a196-4c59-885e-86c4449d426b",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"banners": {
"ver": "1.0.4",
"uuid": "ce394cd8-e574-4170-8c9c-24072e7d6428",
"rawTextureUuid": "90cc6214-a196-4c59-885e-86c4449d426b",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 2.5,
"offsetY": 16.5,
"trimX": 5,
"trimY": 4,
"width": 123,
"height": 215,
"rawWidth": 128,
"rawHeight": 256,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "e0a44b92-2564-401c-8276-b47e279c0039",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"bat": {
"ver": "1.0.4",
"uuid": "5ad7b260-8239-4eb0-a436-7bc1c3743433",
"rawTextureUuid": "e0a44b92-2564-401c-8276-b47e279c0039",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -12,
"offsetY": 0.5,
"trimX": 0,
"trimY": 0,
"width": 104,
"height": 15,
"rawWidth": 128,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "a83b9bc0-78f0-41ed-acc2-6f88020d4828",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"bee": {
"ver": "1.0.4",
"uuid": "a5d1dc1d-5f46-430c-96d3-3e2d2584be13",
"rawTextureUuid": "a83b9bc0-78f0-41ed-acc2-6f88020d4828",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -40.5,
"offsetY": 0,
"trimX": 2,
"trimY": 0,
"width": 171,
"height": 16,
"rawWidth": 256,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "4ba0be3a-32d0-43f1-9640-5fb0f65ea16a",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"blacksmith": {
"ver": "1.0.4",
"uuid": "cc238a21-5d38-496a-86c0-3c223cb0977a",
"rawTextureUuid": "4ba0be3a-32d0-43f1-9640-5fb0f65ea16a",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -6.5,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 51,
"height": 16,
"rawWidth": 64,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "2ed4fd6c-7c83-49e8-baba-01145a109cc8",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"brute": {
"ver": "1.0.4",
"uuid": "eb04bd2a-243f-4ca8-8e58-3bc6cc5e61c1",
"rawTextureUuid": "2ed4fd6c-7c83-49e8-baba-01145a109cc8",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -61.5,
"offsetY": 0,
"trimX": 1,
"trimY": 0,
"width": 131,
"height": 32,
"rawWidth": 256,
"rawHeight": 32,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "f38d420a-e1d6-4b6f-8863-e35c52cf4f8b",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"buffs": {
"ver": "1.0.4",
"uuid": "5a9620a1-2da1-4870-94e9-096e5e323a43",
"rawTextureUuid": "f38d420a-e1d6-4b6f-8863-e35c52cf4f8b",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 128,
"height": 16,
"rawWidth": 128,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "4ce8b218-cd3d-40d8-9102-ba1e4cf415eb",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"burning_fist": {
"ver": "1.0.4",
"uuid": "c62848ac-4eec-46f9-b684-b5365d507212",
"rawTextureUuid": "4ce8b218-cd3d-40d8-9102-ba1e4cf415eb",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -42.5,
"offsetY": 7.5,
"trimX": 3,
"trimY": 0,
"width": 165,
"height": 17,
"rawWidth": 256,
"rawHeight": 32,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "21e86a32-9ad1-4d02-877b-37e65b874697",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"chrome": {
"ver": "1.0.4",
"uuid": "ba373151-5747-4c0d-8457-5543fefd50d2",
"rawTextureUuid": "21e86a32-9ad1-4d02-877b-37e65b874697",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -21,
"offsetY": 0.5,
"trimX": 1,
"trimY": 0,
"width": 84,
"height": 63,
"rawWidth": 128,
"rawHeight": 64,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "cde66edb-eafb-4e46-b5a9-0b21851f4974",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"crab": {
"ver": "1.0.4",
"uuid": "fc11e3df-a336-4ee6-b9f7-1dbabb1c7b1b",
"rawTextureUuid": "cde66edb-eafb-4e46-b5a9-0b21851f4974",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -15.5,
"offsetY": 0,
"trimX": 1,
"trimY": 0,
"width": 223,
"height": 16,
"rawWidth": 256,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "b146d7f8-393b-42d1-bb53-f0f4ad379dfc",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"dashboard": {
"ver": "1.0.4",
"uuid": "4d4eb3aa-5d9e-4f78-9c0c-dcb99f479c2a",
"rawTextureUuid": "b146d7f8-393b-42d1-bb53-f0f4ad379dfc",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -0.5,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 127,
"height": 32,
"rawWidth": 128,
"rawHeight": 32,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "6abab25f-0d0a-4994-8001-bc37e7632abe",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"demon": {
"ver": "1.0.4",
"uuid": "31fcc6ba-8e07-47f7-a933-0f45384dc511",
"rawTextureUuid": "6abab25f-0d0a-4994-8001-bc37e7632abe",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -2,
"offsetY": 1,
"trimX": 1,
"trimY": 0,
"width": 58,
"height": 14,
"rawWidth": 64,
"rawHeight": 16,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

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