Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3e7718ed04 | ||
|
b78dd54431 | ||
|
22fb72afbc | ||
|
7b9172c27b | ||
|
7e12853a73 | ||
|
95dcc2ef17 | ||
|
63164569b1 | ||
|
8a4efd023b | ||
|
b031fc1c61 | ||
|
4369729d9c | ||
|
2d080ad134 | ||
|
bd9beec5e5 | ||
|
89a54211e1 | ||
|
a4ebde3e07 | ||
|
41967b11f7 | ||
|
98daeff408 | ||
|
320e98361e | ||
|
15a062af10 | ||
|
3f4e49656a | ||
|
f97ce22cef | ||
|
901b189c5a | ||
|
e5ed8124e8 | ||
|
885443c2b1 | ||
|
aa795fcee5 | ||
|
cb3c19a339 | ||
|
d3d3629618 | ||
|
f37f4337de |
23
README.md
@@ -1,6 +1,15 @@
|
||||
# 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).
|
||||
|
||||

|
||||
|
||||
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)_
|
||||
|
||||
@@ -9,17 +18,7 @@ _(how input delay roughly works)_
|
||||
_(how rollback-and-chase in this project roughly works)_
|
||||
|
||||

|
||||
|
||||
_(in game screenshot)_
|
||||
|
||||

|
||||
|
||||
Please checkout [this demo video](https://pan.baidu.com/s/123LlWcT9X-wbcYybqYnvmA?pwd=qrlw) to see whether the source codes are doing what you expect for synchronization.
|
||||
|
||||
The video mainly shows the following features.
|
||||
- The backend receives inputs from frontend peers and [by a GGPO-alike manner](https://github.com/pond3r/ggpo/blob/master/doc/README.md) broadcasts back for synchronization.
|
||||
- The game is recovered for a player upon reconnection.
|
||||
- Both backend(Golang) and frontend(JavaScript) execute collision detection and handle collision contacts by the same algorithm. The backend dynamics can be toggled off by [Room.BackendDynamicsEnabled](https://github.com/genxium/DelayNoMore/blob/v0.5.2/battle_srv/models/room.go#L813), but **when turned off the game couldn't support recovery upon reconnection**.
|
||||

|
||||
|
||||
# 1. Building & running
|
||||
|
||||
|
@@ -1,6 +1,11 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"battle_srv/api"
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
"battle_srv/models"
|
||||
"battle_srv/storage"
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -10,11 +15,6 @@ import (
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"server/api"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
"strconv"
|
||||
|
||||
. "dnmshared"
|
||||
@@ -79,7 +79,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
c.Set(api.RET, Constants.RetCode.UnknownError)
|
||||
return
|
||||
}
|
||||
// Redis剩余时长校验
|
||||
if ttl >= ConstVals.Player.CaptchaMaxTTL {
|
||||
Logger.Info("There's an existing SmsCaptcha record in Redis-server: ", zap.String("key", redisKey), zap.Duration("ttl", ttl))
|
||||
c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently)
|
||||
@@ -89,7 +88,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
pass := false
|
||||
var succRet int
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
// 测试环境,优先从数据库校验`player.name`,不通过再走机器人magic name校验
|
||||
player, err := models.GetPlayerByName(req.Num)
|
||||
if nil == err && nil != player {
|
||||
pass = true
|
||||
@@ -98,7 +96,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !pass {
|
||||
// 机器人magic name校验,不通过再走手机号校验
|
||||
player, err := models.GetPlayerByName(req.Num)
|
||||
if nil == err && nil != player {
|
||||
pass = true
|
||||
@@ -111,7 +108,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
succRet = Constants.RetCode.Ok
|
||||
pass = true
|
||||
}
|
||||
// Hardecoded 只验证国内手机号格式
|
||||
if req.CountryCode == "86" {
|
||||
if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
|
||||
succRet = Constants.RetCode.Ok
|
||||
@@ -133,7 +129,6 @@ func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
}{Ret: succRet}
|
||||
var captcha string
|
||||
if ttl >= 0 {
|
||||
// 已有未过期的旧验证码记录,续验证码有效期。
|
||||
storage.RedisManagerIns.Expire(redisKey, ConstVals.Player.CaptchaExpire)
|
||||
captcha = storage.RedisManagerIns.Get(redisKey).Val()
|
||||
if ttl >= ConstVals.Player.CaptchaExpire/4 {
|
||||
@@ -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))
|
||||
} else {
|
||||
// 校验通过,进行验证码生成处理
|
||||
captcha = strconv.Itoa(utils.Rand.Number(1000, 9999))
|
||||
if succRet == Constants.RetCode.Ok {
|
||||
getSmsCaptchaRespErrorCode := sendSMSViaVendor(req.Num, req.CountryCode, captcha)
|
||||
@@ -234,7 +228,6 @@ func (p *playerController) WechatLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
|
||||
baseInfo, err := utils.WechatIns.GetOauth2Basic(req.Authcode)
|
||||
|
||||
if err != nil {
|
||||
@@ -250,7 +243,6 @@ func (p *playerController) WechatLogin(c *gin.Context) {
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
//fserver不会返回openId
|
||||
userInfo.OpenID = baseInfo.OpenID
|
||||
|
||||
player, err := p.maybeCreatePlayerWechatAuthBinding(userInfo)
|
||||
@@ -316,7 +308,6 @@ func (p *playerController) WechatGameLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
|
||||
baseInfo, err := utils.WechatGameIns.GetOauth2Basic(req.Authcode)
|
||||
|
||||
if err != nil {
|
||||
@@ -337,7 +328,6 @@ func (p *playerController) WechatGameLogin(c *gin.Context) {
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
//fserver不会返回openId
|
||||
userInfo.OpenID = baseInfo.OpenID
|
||||
|
||||
player, err := p.maybeCreatePlayerWechatGameAuthBinding(userInfo)
|
||||
@@ -395,7 +385,6 @@ func (p *playerController) IntAuthTokenLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
//kobako: 从player获取display name等
|
||||
player, err := models.GetPlayerById(playerLogin.PlayerID)
|
||||
if err != nil {
|
||||
Logger.Error("Get player by id in IntAuthTokenLogin function error: ", zap.Error(err))
|
||||
@@ -479,7 +468,6 @@ func (p *playerController) TokenAuth(c *gin.Context) {
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// 以下是内部私有函数
|
||||
func (p *playerController) maybeCreateNewPlayer(req smsCaptchaReq) (*models.Player, error) {
|
||||
extAuthID := req.extAuthID()
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
@@ -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))
|
||||
return player, nil
|
||||
}
|
||||
} else { //正式环境检查是否为bot用户
|
||||
} else {
|
||||
botPlayer, err := models.GetPlayerByName(req.Num)
|
||||
if err != nil {
|
||||
Logger.Error("Seeking bot player error:", zap.Error(err))
|
||||
@@ -537,19 +525,17 @@ func (p *playerController) maybeCreatePlayerWechatAuthBinding(userInfo utils.Use
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
{ //更新玩家姓名及头像
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
return player, nil
|
||||
}
|
||||
@@ -575,19 +561,17 @@ func (p *playerController) maybeCreatePlayerWechatGameAuthBinding(userInfo utils
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
{ //更新玩家姓名及头像
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
return player, nil
|
||||
}
|
||||
@@ -672,15 +656,13 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
|
||||
Nationcode: nationcode,
|
||||
}
|
||||
var captchaExpireMin string
|
||||
//短信有效期hardcode
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
//测试环境下有效期为20秒 先hardcode了
|
||||
captchaExpireMin = "0.5"
|
||||
captchaExpireMin = "0.5" // Hardcoded
|
||||
} else {
|
||||
captchaExpireMin = strconv.Itoa(int(ConstVals.Player.CaptchaExpire) / 60000000000)
|
||||
}
|
||||
params := [2]string{captchaCode, captchaExpireMin}
|
||||
appkey := "41a5142feff0b38ade02ea12deee9741" // TODO: Should read from config file!
|
||||
appkey := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // TODO: Should read from config file!
|
||||
rand := strconv.Itoa(utils.Rand.Number(1000, 9999))
|
||||
now := utils.UnixtimeSec()
|
||||
|
||||
@@ -694,7 +676,7 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
|
||||
Extend: "",
|
||||
Params: ¶ms,
|
||||
Sig: sig,
|
||||
Sign: "洛克互娱",
|
||||
Sign: "YYYYYYYYYYYYYYYYY",
|
||||
Tel: tel,
|
||||
Time: now,
|
||||
Tpl_id: 207399,
|
||||
@@ -705,7 +687,7 @@ func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int
|
||||
Logger.Info("json marshal", zap.Any("err:", err))
|
||||
return -1
|
||||
}
|
||||
resp, err := http.Post("https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=1400150185&random="+rand,
|
||||
resp, err := http.Post("https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=uuuuuuuuuuuuuuuuuuuuuuuu&random="+rand,
|
||||
"application/json",
|
||||
req)
|
||||
if err != nil {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
. "battle_srv/configs"
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
. "dnmshared"
|
||||
@@ -11,8 +13,6 @@ import (
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
. "server/common"
|
||||
. "server/configs"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
@@ -250,10 +250,6 @@ func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
//jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", w.config.AppID)
|
||||
//expires := ticket.ExpiresIn - 1500
|
||||
//set
|
||||
//err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,9 +272,6 @@ func (w *wechat) getAccessTokenFromServer() (accessToken string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
//accessTokenCacheKey := fmt.Sprintf("access_token_%s", w.config.AppID)
|
||||
//expires := r.ExpiresIn - 1500
|
||||
//set to redis err = ctx.Cache.Set(accessTokenCacheKey, r.AccessToken, time.Duration(expires)*time.Second)
|
||||
accessToken = r.AccessToken
|
||||
return
|
||||
}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
package env_tools
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
"battle_srv/models"
|
||||
"battle_srv/storage"
|
||||
. "dnmshared"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.uber.org/zap"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
)
|
||||
|
||||
func LoadPreConf() {
|
||||
|
@@ -1,11 +1,11 @@
|
||||
package env_tools
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
"battle_srv/models"
|
||||
"battle_srv/storage"
|
||||
. "dnmshared"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
module server
|
||||
module battle_srv
|
||||
|
||||
go 1.19
|
||||
|
||||
|
@@ -1,19 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"battle_srv/api"
|
||||
"battle_srv/api/v1"
|
||||
. "battle_srv/common"
|
||||
"battle_srv/env_tools"
|
||||
"battle_srv/models"
|
||||
"battle_srv/storage"
|
||||
"battle_srv/ws"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"server/api"
|
||||
"server/api/v1"
|
||||
. "server/common"
|
||||
"server/env_tools"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
"server/ws"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
. "dnmshared"
|
||||
. "dnmshared/sharedprotos"
|
||||
)
|
||||
|
||||
type Barrier struct {
|
||||
|
@@ -1,77 +1,22 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
. "dnmshared"
|
||||
pb "server/pb_output"
|
||||
. "battle_srv/protos"
|
||||
. "dnmshared/sharedprotos"
|
||||
)
|
||||
|
||||
func toPbVec2D(modelInstance *Vec2D) *pb.Vec2D {
|
||||
toRet := &pb.Vec2D{
|
||||
X: modelInstance.X,
|
||||
Y: modelInstance.Y,
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPolygon2D(modelInstance *Polygon2D) *pb.Polygon2D {
|
||||
toRet := &pb.Polygon2D{
|
||||
Anchor: toPbVec2D(modelInstance.Anchor),
|
||||
Points: make([]*pb.Vec2D, len(modelInstance.Points)),
|
||||
}
|
||||
for index, p := range modelInstance.Points {
|
||||
toRet.Points[index] = toPbVec2D(p)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbVec2DList(modelInstance *Vec2DList) *pb.Vec2DList {
|
||||
toRet := &pb.Vec2DList{
|
||||
Vec2DList: make([]*pb.Vec2D, len(*modelInstance)),
|
||||
}
|
||||
for k, v := range *modelInstance {
|
||||
toRet.Vec2DList[k] = toPbVec2D(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func ToPbVec2DListMap(modelInstances map[string]*Vec2DList) map[string]*pb.Vec2DList {
|
||||
toRet := make(map[string]*pb.Vec2DList, len(modelInstances))
|
||||
for k, v := range modelInstances {
|
||||
toRet[k] = toPbVec2DList(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPolygon2DList(modelInstance *Polygon2DList) *pb.Polygon2DList {
|
||||
toRet := &pb.Polygon2DList{
|
||||
Polygon2DList: make([]*pb.Polygon2D, len(*modelInstance)),
|
||||
}
|
||||
for k, v := range *modelInstance {
|
||||
toRet.Polygon2DList[k] = toPbPolygon2D(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func ToPbPolygon2DListMap(modelInstances map[string]*Polygon2DList) map[string]*pb.Polygon2DList {
|
||||
toRet := make(map[string]*pb.Polygon2DList, len(modelInstances))
|
||||
for k, v := range modelInstances {
|
||||
toRet[k] = toPbPolygon2DList(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPlayers(modelInstances map[int32]*Player) map[int32]*pb.Player {
|
||||
toRet := make(map[int32]*pb.Player, 0)
|
||||
func toPbPlayers(modelInstances map[int32]*Player) map[int32]*PlayerDownsync {
|
||||
toRet := make(map[int32]*PlayerDownsync, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.Player{
|
||||
Id: last.Id,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Dir: &pb.Direction{
|
||||
toRet[k] = &PlayerDownsync{
|
||||
Id: last.Id,
|
||||
VirtualGridX: last.VirtualGridX,
|
||||
VirtualGridY: last.VirtualGridY,
|
||||
Dir: &Direction{
|
||||
Dx: last.Dir.Dx,
|
||||
Dy: last.Dir.Dy,
|
||||
},
|
||||
|
@@ -2,7 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
. "dnmshared"
|
||||
. "dnmshared/sharedprotos"
|
||||
"fmt"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -33,30 +33,32 @@ func InitPlayerBattleStateIns() {
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
Id int32 `json:"id,omitempty" db:"id"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Dir *Direction `json:"dir,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
BattleState int32 `json:"battleState,omitempty"`
|
||||
LastMoveGmtMillis int32 `json:"lastMoveGmtMillis,omitempty"`
|
||||
Score int32 `json:"score,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
JoinIndex int32
|
||||
// Meta info fields
|
||||
Id int32 `json:"id,omitempty" db:"id"`
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
ColliderRadius float64 `json:"-"`
|
||||
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
// DB only fields
|
||||
CreatedAt int64 `db:"created_at"`
|
||||
UpdatedAt int64 `db:"updated_at"`
|
||||
DeletedAt NullInt64 `db:"deleted_at"`
|
||||
TutorialStage int `db:"tutorial_stage"`
|
||||
|
||||
FrozenAtGmtMillis int64 `json:"-" db:"-"`
|
||||
AddSpeedAtGmtMillis int64 `json:"-" db:"-"`
|
||||
CreatedAt int64 `json:"-" db:"created_at"`
|
||||
UpdatedAt int64 `json:"-" db:"updated_at"`
|
||||
DeletedAt NullInt64 `json:"-" db:"deleted_at"`
|
||||
TutorialStage int `json:"-" db:"tutorial_stage"`
|
||||
AckingFrameId int32 `json:"ackingFrameId"`
|
||||
AckingInputFrameId int32 `json:"-"`
|
||||
LastSentInputFrameId int32 `json:"-"`
|
||||
// in-battle info fields
|
||||
VirtualGridX int32
|
||||
VirtualGridY int32
|
||||
Dir *Direction
|
||||
Speed int32
|
||||
BattleState int32
|
||||
LastMoveGmtMillis int32
|
||||
Score int32
|
||||
Removed bool
|
||||
JoinIndex int32
|
||||
AckingFrameId int32
|
||||
AckingInputFrameId int32
|
||||
LastSentInputFrameId int32
|
||||
}
|
||||
|
||||
func ExistPlayerByName(name string) (bool, error) {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"battle_srv/storage"
|
||||
"database/sql"
|
||||
. "dnmshared"
|
||||
"server/storage"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
@@ -1,10 +1,10 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
"battle_srv/storage"
|
||||
"database/sql"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/storage"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
@@ -1,11 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
"database/sql"
|
||||
. "dnmshared"
|
||||
"errors"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
@@ -1,7 +1,11 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/common/utils"
|
||||
. "battle_srv/protos"
|
||||
. "dnmshared"
|
||||
. "dnmshared/sharedprotos"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/golang/protobuf/proto"
|
||||
@@ -13,9 +17,6 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
pb "server/pb_output"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -60,36 +61,17 @@ const (
|
||||
MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2
|
||||
)
|
||||
|
||||
// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged.
|
||||
var DIRECTION_DECODER = [][]int32{
|
||||
{0, 0},
|
||||
{0, +1},
|
||||
{0, -1},
|
||||
{0, +2},
|
||||
{0, -2},
|
||||
{+2, 0},
|
||||
{-2, 0},
|
||||
{+2, +1},
|
||||
{-2, -1},
|
||||
{+2, -1},
|
||||
{-2, +1},
|
||||
{+2, 0},
|
||||
{-2, 0},
|
||||
{0, +1},
|
||||
{0, -1},
|
||||
}
|
||||
|
||||
var DIRECTION_DECODER_INVERSE_LENGTH = []float64{
|
||||
0.0,
|
||||
1.0,
|
||||
1.0,
|
||||
0.5,
|
||||
0.5,
|
||||
0.4472,
|
||||
0.4472,
|
||||
0.4472,
|
||||
0.4472,
|
||||
0.5,
|
||||
0.5,
|
||||
1.0,
|
||||
1.0,
|
||||
{+1, +1},
|
||||
{-1, -1},
|
||||
{+1, -1},
|
||||
{-1, +1},
|
||||
}
|
||||
|
||||
type RoomBattleState struct {
|
||||
@@ -128,11 +110,13 @@ func calRoomScore(inRoomPlayerCount int32, roomPlayerCnt int, currentRoomBattleS
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
Id int32
|
||||
Capacity int
|
||||
Players map[int32]*Player
|
||||
PlayersArr []*Player // ordered by joinIndex
|
||||
CollisionSysMap map[int32]*resolv.Object
|
||||
Id int32
|
||||
Capacity int
|
||||
collisionSpaceOffsetX float64
|
||||
collisionSpaceOffsetY float64
|
||||
Players map[int32]*Player
|
||||
PlayersArr []*Player // ordered by joinIndex
|
||||
CollisionSysMap map[int32]*resolv.Object
|
||||
/**
|
||||
* The following `PlayerDownsyncSessionDict` is NOT individually put
|
||||
* under `type Player struct` for a reason.
|
||||
@@ -176,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"
|
||||
InputScaleFrames uint32 // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
|
||||
JoinIndexBooleanArr []bool
|
||||
RollbackEstimatedDt float64
|
||||
RollbackEstimatedDtMillis float64
|
||||
RollbackEstimatedDtNanos int64
|
||||
LastRenderFrameIdTriggeredAt int64
|
||||
|
||||
WorldToVirtualGridRatio float64
|
||||
VirtualGridToWorldRatio float64
|
||||
|
||||
PlayerDefaultSpeed int32
|
||||
|
||||
StageName string
|
||||
StageDiscreteW int32
|
||||
StageDiscreteH int32
|
||||
@@ -191,11 +179,6 @@ type Room struct {
|
||||
BackendDynamicsEnabled bool
|
||||
}
|
||||
|
||||
const (
|
||||
PLAYER_DEFAULT_SPEED = float64(200) // Hardcoded
|
||||
ADD_SPEED = float64(100) // Hardcoded
|
||||
)
|
||||
|
||||
func (pR *Room) updateScore() {
|
||||
pR.Score = calRoomScore(pR.EffectivePlayerCount, pR.Capacity, pR.State)
|
||||
}
|
||||
@@ -217,9 +200,8 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, session *websocke
|
||||
pPlayerFromDbInit.AckingInputFrameId = -1
|
||||
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
||||
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
|
||||
pPlayerFromDbInit.FrozenAtGmtMillis = -1 // Hardcoded temporarily.
|
||||
pPlayerFromDbInit.Speed = PLAYER_DEFAULT_SPEED // Hardcoded temporarily.
|
||||
pPlayerFromDbInit.AddSpeedAtGmtMillis = -1 // Hardcoded temporarily.
|
||||
pPlayerFromDbInit.Speed = pR.PlayerDefaultSpeed // Hardcoded
|
||||
pPlayerFromDbInit.ColliderRadius = float64(24) // Hardcoded
|
||||
|
||||
pR.Players[playerId] = pPlayerFromDbInit
|
||||
pR.PlayerDownsyncSessionDict[playerId] = session
|
||||
@@ -251,6 +233,8 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
|
||||
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
|
||||
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
|
||||
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
|
||||
pEffectiveInRoomPlayerInstance.Speed = pR.PlayerDefaultSpeed // Hardcoded
|
||||
pEffectiveInRoomPlayerInstance.ColliderRadius = float64(16) // Hardcoded
|
||||
|
||||
Logger.Warn("ReAddPlayerIfPossible finished.", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("joinIndex", pEffectiveInRoomPlayerInstance.JoinIndex), zap.Any("playerBattleState", pEffectiveInRoomPlayerInstance.BattleState), zap.Any("roomState", pR.State), zap.Any("roomEffectivePlayerCount", pR.EffectivePlayerCount), zap.Any("AckingFrameId", pEffectiveInRoomPlayerInstance.AckingFrameId), zap.Any("AckingInputFrameId", pEffectiveInRoomPlayerInstance.AckingInputFrameId), zap.Any("LastSentInputFrameId", pEffectiveInRoomPlayerInstance.LastSentInputFrameId))
|
||||
return true
|
||||
@@ -268,7 +252,7 @@ func (pR *Room) ChooseStage() error {
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
stageNameList := []string{ /*"simple" ,*/ "richsoil"}
|
||||
stageNameList := []string{"dungeon" /*"dungeon", "simple", "richsoil" */}
|
||||
chosenStageIndex := rand.Int() % len(stageNameList) // Hardcoded temporarily. -- YFLu
|
||||
|
||||
pR.StageName = stageNameList[chosenStageIndex]
|
||||
@@ -328,7 +312,7 @@ func (pR *Room) ChooseStage() error {
|
||||
barrierPolygon2DList := *(toRetStrToPolygon2DListMap["Barrier"])
|
||||
|
||||
var barrierLocalIdInBattle int32 = 0
|
||||
for _, polygon2DUnaligned := range barrierPolygon2DList {
|
||||
for _, polygon2DUnaligned := range barrierPolygon2DList.Eles {
|
||||
polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned)
|
||||
/*
|
||||
// For debug-printing only.
|
||||
@@ -361,7 +345,7 @@ func (pR *Room) ConvertToLastUsedRenderFrameId(inputFrameId int32, inputDelayFra
|
||||
return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames + (1 << pR.InputScaleFrames) - 1)
|
||||
}
|
||||
|
||||
func (pR *Room) EncodeUpsyncCmd(upsyncCmd *pb.InputFrameUpsync) uint64 {
|
||||
func (pR *Room) EncodeUpsyncCmd(upsyncCmd *InputFrameUpsync) uint64 {
|
||||
var ret uint64 = 0
|
||||
// There're 13 possible directions, occupying the first 4 bits, no need to shift
|
||||
ret += uint64(upsyncCmd.EncodedDir)
|
||||
@@ -385,7 +369,7 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
|
||||
if nil == tmp {
|
||||
break
|
||||
}
|
||||
f := tmp.(*pb.InputFrameDownsync)
|
||||
f := tmp.(*InputFrameDownsync)
|
||||
s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList))
|
||||
}
|
||||
|
||||
@@ -407,7 +391,7 @@ func (pR *Room) StartBattle() {
|
||||
|
||||
// Initialize the "collisionSys" as well as "RenderFrameBuffer"
|
||||
pR.CurDynamicsRenderFrameId = 0
|
||||
kickoffFrame := &pb.RoomDownsyncFrame{
|
||||
kickoffFrame := &RoomDownsyncFrame{
|
||||
Id: pR.RenderFrameId,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
CountdownNanos: pR.BattleDurationNanos,
|
||||
@@ -415,7 +399,11 @@ func (pR *Room) StartBattle() {
|
||||
pR.RenderFrameBuffer.Put(kickoffFrame)
|
||||
|
||||
// Refresh "Colliders"
|
||||
pR.refreshColliders()
|
||||
spaceW := pR.StageDiscreteW * pR.StageTileW
|
||||
spaceH := pR.StageDiscreteH * pR.StageTileH
|
||||
|
||||
pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY = float64(spaceW)*0.5, float64(spaceH)*0.5
|
||||
pR.refreshColliders(spaceW, spaceH)
|
||||
|
||||
/**
|
||||
* Will be triggered from a goroutine which executes the critical `Room.AddPlayerIfPossible`, thus the `battleMainLoop` should be detached.
|
||||
@@ -459,12 +447,11 @@ func (pR *Room) StartBattle() {
|
||||
pR.prefabInputFrameDownsync(noDelayInputFrameId)
|
||||
}
|
||||
|
||||
pR.markConfirmationIfApplicable()
|
||||
unconfirmedMask := uint64(0)
|
||||
if pR.BackendDynamicsEnabled {
|
||||
// Force setting all-confirmed of buffered inputFrames periodically
|
||||
unconfirmedMask = pR.forceConfirmationIfApplicable()
|
||||
} else {
|
||||
pR.markConfirmationIfApplicable()
|
||||
}
|
||||
|
||||
upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId))
|
||||
@@ -488,7 +475,7 @@ func (pR *Room) StartBattle() {
|
||||
// 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.applyInputFrameDownsyncDynamics(pR.CurDynamicsRenderFrameId, nextDynamicsRenderFrameId, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY)
|
||||
dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
|
||||
}
|
||||
|
||||
@@ -497,21 +484,22 @@ func (pR *Room) StartBattle() {
|
||||
refRenderFrameId = pR.CurDynamicsRenderFrameId
|
||||
}
|
||||
}
|
||||
|
||||
for playerId, player := range pR.Players {
|
||||
if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped {
|
||||
// [WARNING] DON'T send anything if the player is disconnected, because it could jam the channel and cause significant delay upon "battle recovery for reconnected player".
|
||||
continue
|
||||
}
|
||||
if 0 == pR.RenderFrameId {
|
||||
kickoffFrame := pR.RenderFrameBuffer.GetByFrameId(0).(*pb.RoomDownsyncFrame)
|
||||
kickoffFrame := pR.RenderFrameBuffer.GetByFrameId(0).(*RoomDownsyncFrame)
|
||||
pR.sendSafely(kickoffFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId)
|
||||
} else {
|
||||
// [WARNING] Websocket is TCP-based, thus no need to re-send a previously sent inputFrame to a same player!
|
||||
toSendInputFrames := make([]*pb.InputFrameDownsync, 0, pR.InputsBuffer.Cnt)
|
||||
toSendInputFrames := make([]*InputFrameDownsync, 0, pR.InputsBuffer.Cnt)
|
||||
candidateToSendInputFrameId := pR.Players[playerId].LastSentInputFrameId + 1
|
||||
if candidateToSendInputFrameId < pR.InputsBuffer.StFrameId {
|
||||
// [WARNING] As "player.LastSentInputFrameId <= lastAllConfirmedInputFrameIdWithChange" for each iteration, and "lastAllConfirmedInputFrameIdWithChange <= lastAllConfirmedInputFrameId" where the latter is used to "applyInputFrameDownsyncDynamics" and then evict "pR.InputsBuffer", thus there's a very high possibility that "player.LastSentInputFrameId" is already evicted.
|
||||
// Logger.Debug(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false)))
|
||||
Logger.Warn(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false)))
|
||||
candidateToSendInputFrameId = pR.InputsBuffer.StFrameId
|
||||
}
|
||||
|
||||
@@ -527,7 +515,7 @@ func (pR *Room) StartBattle() {
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("Required inputFrameId=%v for roomId=%v, playerId=%v doesn't exist! InputsBuffer=%v", candidateToSendInputFrameId, pR.Id, playerId, pR.InputsBufferString(false)))
|
||||
}
|
||||
f := tmp.(*pb.InputFrameDownsync)
|
||||
f := tmp.(*InputFrameDownsync)
|
||||
if pR.inputFrameIdDebuggable(candidateToSendInputFrameId) {
|
||||
Logger.Debug("inputFrame lifecycle#3[sending]:", zap.Any("roomId", pR.Id), zap.Any("playerId", playerId), zap.Any("playerAckingInputFrameId", player.AckingInputFrameId), zap.Any("inputFrameId", candidateToSendInputFrameId), zap.Any("inputFrameId-doublecheck", f.InputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)), zap.Any("ConfirmedList", f.ConfirmedList))
|
||||
}
|
||||
@@ -537,22 +525,26 @@ func (pR *Room) StartBattle() {
|
||||
|
||||
if 0 >= len(toSendInputFrames) {
|
||||
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"!
|
||||
|
||||
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId {
|
||||
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
|
||||
var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr)
|
||||
if pR.BackendDynamicsEnabled && (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || 0 < (unconfirmedMask&joinMask)) {
|
||||
// [WARNING] Even upon "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED", it could be true that "0 == (unconfirmedMask & joinMask)"!
|
||||
/*
|
||||
Resync helps
|
||||
1. when player with a slower frontend clock lags significantly behind and thus wouldn't get its inputUpsync recognized due to faster "forceConfirmation"
|
||||
2. reconnection
|
||||
*/
|
||||
shouldResync1 := (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId)
|
||||
// shouldResync2 := (0 < (unconfirmedMask & uint64(1 << uint32(player.JoinIndex-1)))) // This condition is critical, if we don't send resync upon this condition, the "reconnected or slowly-clocking player" might never get its input synced
|
||||
shouldResync2 := (0 < unconfirmedMask) // An easier version of the above, might keep sending "refRenderFrame"s to still connected players when any player is disconnected
|
||||
if pR.BackendDynamicsEnabled && (shouldResync1 || shouldResync2) {
|
||||
tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId)
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("Required refRenderFrameId=%v for roomId=%v, playerId=%v, candidateToSendInputFrameId=%v doesn't exist! InputsBuffer=%v, RenderFrameBuffer=%v", refRenderFrameId, pR.Id, playerId, candidateToSendInputFrameId, pR.InputsBufferString(false), pR.RenderFrameBufferString()))
|
||||
}
|
||||
refRenderFrame := tmp.(*pb.RoomDownsyncFrame)
|
||||
refRenderFrame := tmp.(*RoomDownsyncFrame)
|
||||
pR.sendSafely(refRenderFrame, toSendInputFrames, DOWNSYNC_MSG_ACT_FORCED_RESYNC, playerId)
|
||||
} else {
|
||||
pR.sendSafely(nil, toSendInputFrames, DOWNSYNC_MSG_ACT_INPUT_BATCH, playerId)
|
||||
@@ -561,35 +553,40 @@ func (pR *Room) StartBattle() {
|
||||
}
|
||||
}
|
||||
|
||||
// Evict no longer required "RenderFrameBuffer"
|
||||
for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) {
|
||||
_ = pR.RenderFrameBuffer.Pop()
|
||||
if pR.BackendDynamicsEnabled {
|
||||
// Evict no longer required "RenderFrameBuffer"
|
||||
for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) {
|
||||
_ = pR.RenderFrameBuffer.Pop()
|
||||
}
|
||||
}
|
||||
|
||||
toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames)
|
||||
if false == pR.BackendDynamicsEnabled {
|
||||
// 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
|
||||
/*
|
||||
[WARNING]
|
||||
The following updates to "toApplyInputFrameId" is necessary because when "false == pR.BackendDynamicsEnabled", the variable "refRenderFrameId" is not well defined.
|
||||
*/
|
||||
minLastSentInputFrameId := int32(math.MaxInt32)
|
||||
for _, player := range pR.Players {
|
||||
if player.LastSentInputFrameId >= minLastSentInputFrameId {
|
||||
continue
|
||||
}
|
||||
minLastSentInputFrameId = player.LastSentInputFrameId
|
||||
}
|
||||
if minLastSentInputFrameId < toApplyInputFrameId {
|
||||
toApplyInputFrameId = minLastSentInputFrameId
|
||||
}
|
||||
for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) {
|
||||
f := pR.InputsBuffer.Pop().(*pb.InputFrameDownsync)
|
||||
f := pR.InputsBuffer.Pop().(*InputFrameDownsync)
|
||||
if pR.inputFrameIdDebuggable(f.InputFrameId) {
|
||||
// Popping of an "inputFrame" would be AFTER its being all being confirmed, because it requires the "inputFrame" to be all acked
|
||||
Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)))
|
||||
Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("toApplyInputFrameId", toApplyInputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false)))
|
||||
}
|
||||
}
|
||||
|
||||
pR.RenderFrameId++
|
||||
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
|
||||
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))
|
||||
}
|
||||
@@ -605,7 +602,7 @@ func (pR *Room) toDiscreteInputsBufferIndex(inputFrameId int32, joinIndex int32)
|
||||
return (inputFrameId << 2) + joinIndex // allowing joinIndex upto 15
|
||||
}
|
||||
|
||||
func (pR *Room) OnBattleCmdReceived(pReq *pb.WsReq) {
|
||||
func (pR *Room) OnBattleCmdReceived(pReq *WsReq) {
|
||||
if swapped := atomic.CompareAndSwapInt32(&pR.State, RoomBattleStateIns.IN_BATTLE, RoomBattleStateIns.IN_BATTLE); !swapped {
|
||||
return
|
||||
}
|
||||
@@ -643,7 +640,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
|
||||
if -1 == pR.LastAllConfirmedInputFrameIdWithChange || false == pR.equalInputLists(inputFrameDownsync.InputList, pR.LastAllConfirmedInputList) {
|
||||
if -1 == playerId {
|
||||
@@ -685,7 +682,7 @@ func (pR *Room) StopBattleForSettlement() {
|
||||
Logger.Info("Stopping the `battleMainLoop` for:", zap.Any("roomId", pR.Id))
|
||||
pR.RenderFrameId++
|
||||
for playerId, _ := range pR.Players {
|
||||
assembledFrame := pb.RoomDownsyncFrame{
|
||||
assembledFrame := RoomDownsyncFrame{
|
||||
Id: pR.RenderFrameId,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
CountdownNanos: -1, // TODO: Replace this magic constant!
|
||||
@@ -711,18 +708,19 @@ func (pR *Room) onBattlePrepare(cb BattleStartCbType) {
|
||||
pR.State = RoomBattleStateIns.PREPARE
|
||||
Logger.Info("Battle state transitted to RoomBattleStateIns.PREPARE for:", zap.Any("roomId", pR.Id))
|
||||
|
||||
playerMetas := make(map[int32]*pb.PlayerMeta, 0)
|
||||
playerMetas := make(map[int32]*PlayerDownsyncMeta, 0)
|
||||
for _, player := range pR.Players {
|
||||
playerMetas[player.Id] = &pb.PlayerMeta{
|
||||
Id: player.Id,
|
||||
Name: player.Name,
|
||||
DisplayName: player.DisplayName,
|
||||
Avatar: player.Avatar,
|
||||
JoinIndex: player.JoinIndex,
|
||||
playerMetas[player.Id] = &PlayerDownsyncMeta{
|
||||
Id: player.Id,
|
||||
Name: player.Name,
|
||||
DisplayName: player.DisplayName,
|
||||
Avatar: player.Avatar,
|
||||
ColliderRadius: player.ColliderRadius, // hardcoded for now
|
||||
JoinIndex: player.JoinIndex,
|
||||
}
|
||||
}
|
||||
|
||||
battleReadyToStartFrame := &pb.RoomDownsyncFrame{
|
||||
battleReadyToStartFrame := &RoomDownsyncFrame{
|
||||
Id: DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
PlayerMetas: playerMetas,
|
||||
@@ -792,6 +790,9 @@ func (pR *Room) Dismiss() {
|
||||
func (pR *Room) OnDismissed() {
|
||||
|
||||
// Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference.
|
||||
pR.WorldToVirtualGridRatio = float64(1000)
|
||||
pR.VirtualGridToWorldRatio = float64(1.0) / pR.WorldToVirtualGridRatio // this is a one-off computation, should avoid division in iterations
|
||||
pR.PlayerDefaultSpeed = int32(float64(2) * pR.WorldToVirtualGridRatio) // in virtual grids per frame
|
||||
pR.Players = make(map[int32]*Player)
|
||||
pR.PlayersArr = make([]*Player, pR.Capacity)
|
||||
pR.CollisionSysMap = make(map[int32]*resolv.Object)
|
||||
@@ -813,13 +814,12 @@ func (pR *Room) OnDismissed() {
|
||||
pR.NstDelayFrames = 8
|
||||
pR.InputScaleFrames = uint32(2)
|
||||
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.RollbackEstimatedDtNanos = 16666666 // A little smaller than the actual per frame time, just for preventing FAST FRAME
|
||||
pR.BattleDurationFrames = 30 * pR.ServerFps
|
||||
pR.BattleDurationNanos = int64(pR.BattleDurationFrames) * (pR.RollbackEstimatedDtNanos + 1)
|
||||
pR.InputFrameUpsyncDelayTolerance = 2
|
||||
pR.MaxChasingRenderFramesPerUpdate = 10
|
||||
pR.MaxChasingRenderFramesPerUpdate = 5
|
||||
|
||||
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
|
||||
|
||||
@@ -932,16 +932,15 @@ func (pR *Room) onPlayerAdded(playerId int32) {
|
||||
|
||||
// Lazily assign the initial position of "Player" for "RoomDownsyncFrame".
|
||||
playerPosList := *(pR.RawBattleStrToVec2DListMap["PlayerStartingPos"])
|
||||
if index > len(playerPosList) {
|
||||
if index > len(playerPosList.Eles) {
|
||||
panic(fmt.Sprintf("onPlayerAdded error, index >= len(playerPosList), roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
|
||||
}
|
||||
playerPos := playerPosList[index]
|
||||
playerPos := playerPosList.Eles[index]
|
||||
|
||||
if nil == playerPos {
|
||||
panic(fmt.Sprintf("onPlayerAdded error, nil == playerPos, roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v", pR.Id, playerId, pR.State, pR.EffectivePlayerCount))
|
||||
}
|
||||
pR.Players[playerId].X = playerPos.X
|
||||
pR.Players[playerId].Y = playerPos.Y
|
||||
pR.Players[playerId].VirtualGridX, pR.Players[playerId].VirtualGridY = WorldToVirtualGridPos(playerPos.X, playerPos.Y, pR.WorldToVirtualGridRatio)
|
||||
|
||||
break
|
||||
}
|
||||
@@ -969,14 +968,15 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
playerMetas := make(map[int32]*pb.PlayerMeta, 0)
|
||||
playerMetas := make(map[int32]*PlayerDownsyncMeta, 0)
|
||||
for _, eachPlayer := range pR.Players {
|
||||
playerMetas[eachPlayer.Id] = &pb.PlayerMeta{
|
||||
Id: eachPlayer.Id,
|
||||
Name: eachPlayer.Name,
|
||||
DisplayName: eachPlayer.DisplayName,
|
||||
Avatar: eachPlayer.Avatar,
|
||||
JoinIndex: eachPlayer.JoinIndex,
|
||||
playerMetas[eachPlayer.Id] = &PlayerDownsyncMeta{
|
||||
Id: eachPlayer.Id,
|
||||
Name: eachPlayer.Name,
|
||||
DisplayName: eachPlayer.DisplayName,
|
||||
Avatar: eachPlayer.Avatar,
|
||||
JoinIndex: eachPlayer.JoinIndex,
|
||||
ColliderRadius: eachPlayer.ColliderRadius,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,14 +992,14 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
|
||||
*/
|
||||
switch targetPlayer.BattleState {
|
||||
case PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK:
|
||||
playerAckedFrame := &pb.RoomDownsyncFrame{
|
||||
playerAckedFrame := &RoomDownsyncFrame{
|
||||
Id: pR.RenderFrameId,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
PlayerMetas: playerMetas,
|
||||
}
|
||||
pR.sendSafely(playerAckedFrame, nil, DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED, eachPlayer.Id)
|
||||
case PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK:
|
||||
playerAckedFrame := &pb.RoomDownsyncFrame{
|
||||
playerAckedFrame := &RoomDownsyncFrame{
|
||||
Id: pR.RenderFrameId,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
PlayerMetas: playerMetas,
|
||||
@@ -1030,14 +1030,14 @@ func (pR *Room) OnPlayerBattleColliderAcked(playerId int32) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (pR *Room) sendSafely(roomDownsyncFrame *pb.RoomDownsyncFrame, toSendFrames []*pb.InputFrameDownsync, act int32, playerId int32) {
|
||||
func (pR *Room) sendSafely(roomDownsyncFrame *RoomDownsyncFrame, toSendFrames []*InputFrameDownsync, act int32, playerId int32) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
pR.PlayerSignalToCloseDict[playerId](Constants.RetCode.UnknownError, fmt.Sprintf("%v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
pResp := &pb.WsResp{
|
||||
pResp := &WsResp{
|
||||
Ret: int32(Constants.RetCode.Ok),
|
||||
Act: act,
|
||||
Rdf: roomDownsyncFrame,
|
||||
@@ -1058,16 +1058,16 @@ func (pR *Room) shouldPrefabInputFrameDownsync(renderFrameId int32) bool {
|
||||
return ((renderFrameId & ((1 << pR.InputScaleFrames) - 1)) == 0)
|
||||
}
|
||||
|
||||
func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDownsync {
|
||||
func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *InputFrameDownsync {
|
||||
/*
|
||||
Kindly note that on backend the prefab is much simpler than its frontend counterpart, because frontend will upsync its latest command immediately if there's any change w.r.t. its own prev cmd, thus if no upsync received from a frontend,
|
||||
- EITHER it's due to local lag and bad network,
|
||||
- OR there's no change w.r.t. to its prev cmd.
|
||||
*/
|
||||
var currInputFrameDownsync *pb.InputFrameDownsync = nil
|
||||
var currInputFrameDownsync *InputFrameDownsync = nil
|
||||
|
||||
if 0 == inputFrameId && 0 == pR.InputsBuffer.Cnt {
|
||||
currInputFrameDownsync = &pb.InputFrameDownsync{
|
||||
currInputFrameDownsync = &InputFrameDownsync{
|
||||
InputFrameId: 0,
|
||||
InputList: make([]uint64, pR.Capacity),
|
||||
ConfirmedList: uint64(0),
|
||||
@@ -1077,9 +1077,9 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDowns
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("Error prefabbing inputFrameDownsync: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false)))
|
||||
}
|
||||
prevInputFrameDownsync := tmp.(*pb.InputFrameDownsync)
|
||||
prevInputFrameDownsync := tmp.(*InputFrameDownsync)
|
||||
currInputList := prevInputFrameDownsync.InputList // Would be a clone of the values
|
||||
currInputFrameDownsync = &pb.InputFrameDownsync{
|
||||
currInputFrameDownsync = &InputFrameDownsync{
|
||||
InputFrameId: inputFrameId,
|
||||
InputList: currInputList,
|
||||
ConfirmedList: uint64(0),
|
||||
@@ -1105,20 +1105,19 @@ func (pR *Room) markConfirmationIfApplicable() {
|
||||
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.(*pb.InputFrameDownsync)
|
||||
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.(*pb.InputFrameUpsync)
|
||||
inputFrameUpsync := tmp.(*InputFrameUpsync)
|
||||
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
|
||||
inputFrameDownsync.InputList[indiceInJoinIndexBooleanArr] = pR.EncodeUpsyncCmd(inputFrameUpsync)
|
||||
inputFrameDownsync.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr)
|
||||
}
|
||||
|
||||
// Force confirmation of "inputFrame2"
|
||||
if allConfirmedMask == inputFrameDownsync.ConfirmedList {
|
||||
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
|
||||
} else {
|
||||
@@ -1149,24 +1148,12 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("inputFrameId2=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", inputFrameId2, pR.Id, pR.InputsBufferString(false)))
|
||||
}
|
||||
inputFrame2 := tmp.(*pb.InputFrameDownsync)
|
||||
for _, player := range pR.Players {
|
||||
// Enrich by already arrived player upsync commands
|
||||
bufIndex := pR.toDiscreteInputsBufferIndex(inputFrame2.InputFrameId, player.JoinIndex)
|
||||
tmp, loaded := pR.DiscreteInputsBuffer.LoadAndDelete(bufIndex)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
inputFrameUpsync := tmp.(*pb.InputFrameUpsync)
|
||||
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
|
||||
inputFrame2.InputList[indiceInJoinIndexBooleanArr] = pR.EncodeUpsyncCmd(inputFrameUpsync)
|
||||
inputFrame2.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr)
|
||||
}
|
||||
|
||||
totPlayerCnt := uint32(pR.Capacity)
|
||||
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
|
||||
|
||||
// Force confirmation of "inputFrame2"
|
||||
inputFrame2 := tmp.(*InputFrameDownsync)
|
||||
oldConfirmedList := inputFrame2.ConfirmedList
|
||||
unconfirmedMask := (oldConfirmedList ^ allConfirmedMask)
|
||||
inputFrame2.ConfirmedList = allConfirmedMask
|
||||
@@ -1175,7 +1162,7 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 {
|
||||
return unconfirmedMask
|
||||
}
|
||||
|
||||
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32) {
|
||||
func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRenderFrameId int32, spaceOffsetX, spaceOffsetY float64) {
|
||||
if fromRenderFrameId >= toRenderFrameId {
|
||||
return
|
||||
}
|
||||
@@ -1185,7 +1172,13 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
|
||||
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
|
||||
|
||||
for collisionSysRenderFrameId := fromRenderFrameId; collisionSysRenderFrameId < toRenderFrameId; collisionSysRenderFrameId++ {
|
||||
currRenderFrameTmp := pR.RenderFrameBuffer.GetByFrameId(collisionSysRenderFrameId)
|
||||
if nil == currRenderFrameTmp {
|
||||
panic(fmt.Sprintf("collisionSysRenderFrameId=%v doesn't exist for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v)! RenderFrameBuffer=%v", collisionSysRenderFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, pR.RenderFrameBufferString()))
|
||||
}
|
||||
currRenderFrame := currRenderFrameTmp.(*RoomDownsyncFrame)
|
||||
delayedInputFrameId := pR.ConvertToInputFrameId(collisionSysRenderFrameId, pR.InputDelayFrames)
|
||||
var delayedInputFrame *InputFrameDownsync = nil
|
||||
if 0 <= delayedInputFrameId {
|
||||
if delayedInputFrameId > pR.LastAllConfirmedInputFrameId {
|
||||
panic(fmt.Sprintf("delayedInputFrameId=%v is not yet all-confirmed for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! InputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.InputsBufferString(false)))
|
||||
@@ -1194,78 +1187,126 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("delayedInputFrameId=%v doesn't exist for roomId=%v, this is abnormal because it's to be used for applying dynamics to [fromRenderFrameId:%v, toRenderFrameId:%v) @ collisionSysRenderFrameId=%v! InputsBuffer=%v", delayedInputFrameId, pR.Id, fromRenderFrameId, toRenderFrameId, collisionSysRenderFrameId, pR.InputsBufferString(false)))
|
||||
}
|
||||
delayedInputFrame := tmp.(*pb.InputFrameDownsync)
|
||||
delayedInputFrame = tmp.(*InputFrameDownsync)
|
||||
// [WARNING] It's possible that by now "allConfirmedMask != delayedInputFrame.ConfirmedList && delayedInputFrameId <= pR.LastAllConfirmedInputFrameId", we trust "pR.LastAllConfirmedInputFrameId" as the TOP AUTHORITY.
|
||||
atomic.StoreUint64(&(delayedInputFrame.ConfirmedList), allConfirmedMask)
|
||||
}
|
||||
|
||||
inputList := delayedInputFrame.InputList
|
||||
// Ordered by joinIndex to guarantee determinism
|
||||
for _, player := range pR.PlayersArr {
|
||||
joinIndex := player.JoinIndex
|
||||
encodedInput := inputList[joinIndex-1]
|
||||
decodedInput := DIRECTION_DECODER[encodedInput]
|
||||
decodedInputSpeedFactor := DIRECTION_DECODER_INVERSE_LENGTH[encodedInput]
|
||||
if 0.0 == decodedInputSpeedFactor {
|
||||
continue
|
||||
}
|
||||
baseChange := player.Speed * pR.RollbackEstimatedDt * decodedInputSpeedFactor
|
||||
oldDx, oldDy := baseChange*float64(decodedInput[0]), baseChange*float64(decodedInput[1])
|
||||
dx, dy := oldDx, oldDy
|
||||
nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap)
|
||||
// Update in the latest player pointers
|
||||
for playerId, playerDownsync := range nextRenderFrame.Players {
|
||||
pR.Players[playerId].VirtualGridX = playerDownsync.VirtualGridX
|
||||
pR.Players[playerId].VirtualGridY = playerDownsync.VirtualGridY
|
||||
pR.Players[playerId].Dir.Dx = playerDownsync.Dir.Dx
|
||||
pR.Players[playerId].Dir.Dy = playerDownsync.Dir.Dy
|
||||
}
|
||||
pR.RenderFrameBuffer.Put(nextRenderFrame)
|
||||
pR.CurDynamicsRenderFrameId++
|
||||
}
|
||||
}
|
||||
|
||||
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
||||
playerCollider := pR.CollisionSysMap[collisionPlayerIndex]
|
||||
if collision := playerCollider.Check(oldDx, oldDy, "Barrier"); collision != nil {
|
||||
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
|
||||
for _, obj := range collision.Objects {
|
||||
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
if overlapped, pushbackX, pushbackY := CalcPushbacks(oldDx, oldDy, playerShape, barrierShape); overlapped {
|
||||
Logger.Debug(fmt.Sprintf("Collided & overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v, pushbackX=%v, pushbackY=%v", playerCollider.X, playerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY))
|
||||
dx -= pushbackX
|
||||
dy -= pushbackY
|
||||
} else {
|
||||
Logger.Debug(fmt.Sprintf("Collided BUT not overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v", playerCollider.X, playerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape)))
|
||||
}
|
||||
}
|
||||
}
|
||||
playerCollider.X += dx
|
||||
playerCollider.Y += dy
|
||||
// Update in "collision space"
|
||||
playerCollider.Update()
|
||||
// TODO: Write unit-test for this function to compare with its frontend counter part
|
||||
func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame {
|
||||
nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity)
|
||||
// Make a copy first
|
||||
for playerId, currPlayerDownsync := range currRenderFrame.Players {
|
||||
nextRenderFramePlayers[playerId] = &PlayerDownsync{
|
||||
Id: playerId,
|
||||
VirtualGridX: currPlayerDownsync.VirtualGridX,
|
||||
VirtualGridY: currPlayerDownsync.VirtualGridY,
|
||||
Dir: &Direction{
|
||||
Dx: currPlayerDownsync.Dir.Dx,
|
||||
Dy: currPlayerDownsync.Dir.Dy,
|
||||
},
|
||||
Speed: currPlayerDownsync.Speed,
|
||||
BattleState: currPlayerDownsync.BattleState,
|
||||
Score: currPlayerDownsync.Score,
|
||||
Removed: currPlayerDownsync.Removed,
|
||||
JoinIndex: currPlayerDownsync.JoinIndex,
|
||||
}
|
||||
}
|
||||
|
||||
player.Dir.Dx = decodedInput[0]
|
||||
player.Dir.Dy = decodedInput[1]
|
||||
player.X += dx
|
||||
player.Y += dy
|
||||
toRet := &RoomDownsyncFrame{
|
||||
Id: currRenderFrame.Id + 1,
|
||||
Players: nextRenderFramePlayers,
|
||||
CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos),
|
||||
}
|
||||
|
||||
if nil != delayedInputFrame {
|
||||
inputList := delayedInputFrame.InputList
|
||||
effPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order
|
||||
for playerId, player := range pR.Players {
|
||||
joinIndex := player.JoinIndex
|
||||
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
|
||||
currPlayerDownsync := currRenderFrame.Players[playerId]
|
||||
encodedInput := inputList[joinIndex-1]
|
||||
decodedInput := DIRECTION_DECODER[encodedInput]
|
||||
proposedVirtualGridDx, proposedVirtualGridDy := (decodedInput[0] + decodedInput[0]*currPlayerDownsync.Speed), (decodedInput[1] + decodedInput[1]*currPlayerDownsync.Speed)
|
||||
newVx, newVy := (currPlayerDownsync.VirtualGridX + proposedVirtualGridDx), (currPlayerDownsync.VirtualGridY + proposedVirtualGridDy)
|
||||
// Reset playerCollider position from the "virtual grid position"
|
||||
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
||||
playerCollider := collisionSysMap[collisionPlayerIndex]
|
||||
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio)
|
||||
|
||||
// Update in the collision system
|
||||
playerCollider.Update()
|
||||
|
||||
if 0 < encodedInput {
|
||||
Logger.Debug(fmt.Sprintf("Checking collision for playerId=%v: virtual (%d, %d) -> (%d, %d), now playerShape=%v", playerId, currPlayerDownsync.VirtualGridX, currPlayerDownsync.VirtualGridY, newVx, newVy, ConvexPolygonStr(playerCollider.Shape.(*resolv.ConvexPolygon))))
|
||||
nextRenderFramePlayers[playerId].Dir.Dx = decodedInput[0]
|
||||
nextRenderFramePlayers[playerId].Dir.Dy = decodedInput[1]
|
||||
}
|
||||
}
|
||||
|
||||
newRenderFrame := pb.RoomDownsyncFrame{
|
||||
Id: collisionSysRenderFrameId + 1,
|
||||
Players: toPbPlayers(pR.Players),
|
||||
CountdownNanos: (pR.BattleDurationNanos - int64(collisionSysRenderFrameId)*pR.RollbackEstimatedDtNanos),
|
||||
// handle pushbacks upon collision after all movements treated as simultaneous
|
||||
for _, player := range pR.Players {
|
||||
joinIndex := player.JoinIndex
|
||||
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
||||
playerCollider := collisionSysMap[collisionPlayerIndex]
|
||||
if collision := playerCollider.Check(0, 0); collision != nil {
|
||||
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
|
||||
for _, obj := range collision.Objects {
|
||||
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
|
||||
if overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape); overlapped {
|
||||
Logger.Debug(fmt.Sprintf("Overlapped: a=%v, b=%v, pushbackX=%v, pushbackY=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY))
|
||||
effPushbacks[joinIndex-1].X += pushbackX
|
||||
effPushbacks[joinIndex-1].Y += pushbackY
|
||||
} else {
|
||||
Logger.Debug(fmt.Sprintf("Collided BUT not overlapped: a=%v, b=%v, overlapResult=%v", ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), overlapResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pR.RenderFrameBuffer.Put(&newRenderFrame)
|
||||
pR.CurDynamicsRenderFrameId++
|
||||
|
||||
for playerId, player := range pR.Players {
|
||||
joinIndex := player.JoinIndex
|
||||
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
||||
playerCollider := collisionSysMap[collisionPlayerIndex]
|
||||
|
||||
// Update "virtual grid position"
|
||||
newVx, newVy := PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio)
|
||||
nextRenderFramePlayers[playerId].VirtualGridX = newVx
|
||||
nextRenderFramePlayers[playerId].VirtualGridY = newVy
|
||||
}
|
||||
|
||||
Logger.Debug(fmt.Sprintf("After applyInputFrameDownsyncDynamicsOnSingleRenderFrame: currRenderFrame.Id=%v, inputList=%v, currRenderFrame.Players=%v, nextRenderFramePlayers=%v, toRet.Players=%v", currRenderFrame.Id, inputList, currRenderFrame.Players, nextRenderFramePlayers, toRet.Players))
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func (pR *Room) inputFrameIdDebuggable(inputFrameId int32) bool {
|
||||
return 0 == (inputFrameId % 10)
|
||||
}
|
||||
|
||||
func (pR *Room) refreshColliders() {
|
||||
playerColliderRadius := float64(12) // hardcoded
|
||||
func (pR *Room) refreshColliders(spaceW, spaceH int32) {
|
||||
// Kindly note that by now, we've already got all the shapes in the tmx file into "pR.(Players | Barriers)" from "ParseTmxLayersAndGroups"
|
||||
spaceW := pR.StageDiscreteW * pR.StageTileW
|
||||
spaceH := pR.StageDiscreteH * pR.StageTileH
|
||||
|
||||
spaceOffsetX := float64(spaceW) * 0.5
|
||||
spaceOffsetY := float64(spaceH) * 0.5
|
||||
|
||||
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
|
||||
minStep := (int(float64(pR.PlayerDefaultSpeed)*pR.VirtualGridToWorldRatio) << 2) // the approx minimum distance a player can move per frame in world coordinate
|
||||
space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep) // allocate a new collision space everytime after a battle is settled
|
||||
for _, player := range pR.Players {
|
||||
playerCollider := GenerateRectCollider(player.X, player.Y, playerColliderRadius*2, playerColliderRadius*2, spaceOffsetX, spaceOffsetY, "Player")
|
||||
wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio)
|
||||
playerCollider := GenerateRectCollider(wx, wy, player.ColliderRadius*2, player.ColliderRadius*2, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player")
|
||||
space.Add(playerCollider)
|
||||
// Keep track of the collider in "pR.CollisionSysMap"
|
||||
joinIndex := player.JoinIndex
|
||||
@@ -1276,7 +1317,7 @@ func (pR *Room) refreshColliders() {
|
||||
|
||||
for _, barrier := range pR.Barriers {
|
||||
boundaryUnaligned := barrier.Boundary
|
||||
barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
|
||||
barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier")
|
||||
space.Add(barrierCollider)
|
||||
}
|
||||
}
|
||||
|
1264
battle_srv/protos/room_downsync_frame.pb.go
Normal file
@@ -1,7 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
. "server/common"
|
||||
. "battle_srv/common"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"fmt"
|
||||
. "server/common"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
. "battle_srv/common"
|
||||
"battle_srv/models"
|
||||
pb "battle_srv/protos"
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -8,14 +11,12 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
. "server/common"
|
||||
"server/models"
|
||||
pb "server/pb_output"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
. "dnmshared"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -104,7 +105,7 @@ func Serve(c *gin.Context) {
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Warn("Recovered from: ", zap.Any("panic", r))
|
||||
Logger.Error("Recovered from: ", zap.Any("panic", r))
|
||||
}
|
||||
}()
|
||||
/**
|
||||
@@ -246,8 +247,8 @@ func Serve(c *gin.Context) {
|
||||
bciFrame := &pb.BattleColliderInfo{
|
||||
BoundRoomId: pRoom.Id,
|
||||
StageName: pRoom.StageName,
|
||||
StrToVec2DListMap: models.ToPbVec2DListMap(pRoom.RawBattleStrToVec2DListMap),
|
||||
StrToPolygon2DListMap: models.ToPbPolygon2DListMap(pRoom.RawBattleStrToPolygon2DListMap),
|
||||
StrToVec2DListMap: pRoom.RawBattleStrToVec2DListMap,
|
||||
StrToPolygon2DListMap: pRoom.RawBattleStrToPolygon2DListMap,
|
||||
StageDiscreteW: pRoom.StageDiscreteW,
|
||||
StageDiscreteH: pRoom.StageDiscreteH,
|
||||
StageTileW: pRoom.StageTileW,
|
||||
@@ -263,9 +264,11 @@ func Serve(c *gin.Context) {
|
||||
InputFrameUpsyncDelayTolerance: pRoom.InputFrameUpsyncDelayTolerance,
|
||||
MaxChasingRenderFramesPerUpdate: pRoom.MaxChasingRenderFramesPerUpdate,
|
||||
PlayerBattleState: pThePlayer.BattleState, // For frontend to know whether it's rejoining
|
||||
RollbackEstimatedDt: pRoom.RollbackEstimatedDt,
|
||||
RollbackEstimatedDtMillis: pRoom.RollbackEstimatedDtMillis,
|
||||
RollbackEstimatedDtNanos: pRoom.RollbackEstimatedDtNanos,
|
||||
|
||||
WorldToVirtualGridRatio: pRoom.WorldToVirtualGridRatio,
|
||||
VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio,
|
||||
}
|
||||
|
||||
resp := &pb.WsResp{
|
||||
@@ -354,7 +357,7 @@ func Serve(c *gin.Context) {
|
||||
receivingLoopAgainstPlayer := func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Warn("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
|
||||
Logger.Error("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r), zap.Any("callstack", debug.Stack()))
|
||||
}
|
||||
Logger.Info("Goroutine `receivingLoopAgainstPlayer` is stopped for:", zap.Any("playerId", playerId), zap.Any("roomId", pRoom.Id))
|
||||
}()
|
||||
|
BIN
charts/AvoidingFloatingPointAccumulationErr.jpg
Normal file
After Width: | Height: | Size: 144 KiB |
4
charts/README.md
Normal 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/along_wall_interaction_with_reconnection.gif
Normal file
After Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 430 KiB |
@@ -3,11 +3,11 @@ module viscol
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
dnmshared v0.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/hajimehoshi/ebiten/v2 v2.4.7
|
||||
github.com/solarlune/resolv v0.5.1
|
||||
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69
|
||||
dnmshared v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -22,6 +22,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect
|
||||
golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105 // indirect
|
||||
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
)
|
||||
|
||||
replace dnmshared => ../dnmshared
|
||||
|
@@ -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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/hajimehoshi/bitmapfont/v2 v2.2.1 h1:y7zcy02/UgO24IL3COqYtrRZzhRucNBtmCo/SNU648k=
|
||||
github.com/hajimehoshi/bitmapfont/v2 v2.2.1/go.mod h1:wjrYAy8vKgj9JsFgnYAOK346/uvE22TlmqouzdnYIs0=
|
||||
github.com/hajimehoshi/ebiten/v2 v2.4.7 h1:XuvB7R0Rbw/O7g6vNU8gqr5b9e7MNhhAONMSsyreLDI=
|
||||
@@ -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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
|
@@ -85,8 +85,9 @@ type Game struct {
|
||||
|
||||
func NewGame() *Game {
|
||||
|
||||
stageName := "simple" // Use this for calibration
|
||||
// stageName := "simple" // Use this for calibration in isometric orientation
|
||||
// stageName := "richsoil"
|
||||
stageName := "dungeon"
|
||||
stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap, err := parseStage(stageName)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
|
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
. "dnmshared"
|
||||
. "dnmshared/sharedprotos"
|
||||
"fmt"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/solarlune/resolv"
|
||||
@@ -19,7 +20,7 @@ func (world *WorldColliderDisplay) Init() {
|
||||
|
||||
func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTileW, stageTileH int32, playerPosMap StrToVec2DListMap, barrierMap StrToPolygon2DListMap) *WorldColliderDisplay {
|
||||
|
||||
playerList := *(playerPosMap["PlayerStartingPos"])
|
||||
playerPosList := *(playerPosMap["PlayerStartingPos"])
|
||||
barrierList := *(barrierMap["Barrier"])
|
||||
|
||||
world := &WorldColliderDisplay{Game: game}
|
||||
@@ -32,46 +33,56 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi
|
||||
spaceOffsetX := float64(spaceW) * 0.5
|
||||
spaceOffsetY := float64(spaceH) * 0.5
|
||||
|
||||
playerColliderRadius := float64(32)
|
||||
playerColliders := make([]*resolv.Object, len(playerList))
|
||||
space := resolv.NewSpace(int(spaceW), int(spaceH), 16, 16)
|
||||
for i, player := range playerList {
|
||||
playerCollider := GenerateRectCollider(player.X, player.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.X=%v, player.Y=%v, radius=%v, spaceOffsetX=%v, spaceOffsetY=%v, shape=%v; calibrationCheckX=player.X-radius+spaceOffsetX=%v", i, player.X, player.Y, playerColliderRadius, spaceOffsetX, spaceOffsetY, playerCollider.Shape, player.X-playerColliderRadius+spaceOffsetX))
|
||||
virtualGridToWorldRatio := 0.1
|
||||
playerDefaultSpeed := 20
|
||||
minStep := (int(float64(playerDefaultSpeed)*virtualGridToWorldRatio) << 2)
|
||||
playerColliderRadius := float64(16)
|
||||
playerColliders := make([]*resolv.Object, len(playerPosList.Eles))
|
||||
space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep)
|
||||
for i, playerPos := range playerPosList.Eles {
|
||||
playerCollider := GenerateRectCollider(playerPos.X, playerPos.Y, playerColliderRadius*2, playerColliderRadius*2, spaceOffsetX, spaceOffsetY, "Player") // [WARNING] Deliberately not using a circle because "resolv v0.5.1" doesn't yet align circle center with space cell center, regardless of the "specified within-object offset"
|
||||
Logger.Info(fmt.Sprintf("Player Collider#%d: player world pos =(%.2f, %.2f), shape=%v", i, playerPos.X, playerPos.Y, ConvexPolygonStr(playerCollider.Shape.(*resolv.ConvexPolygon))))
|
||||
playerColliders[i] = playerCollider
|
||||
space.Add(playerCollider)
|
||||
}
|
||||
|
||||
barrierLocalId := 0
|
||||
for _, barrierUnaligned := range barrierList {
|
||||
for _, barrierUnaligned := range barrierList.Eles {
|
||||
barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
|
||||
Logger.Info(fmt.Sprintf("Added barrier: shape=%v", barrierCollider.Shape))
|
||||
Logger.Info(fmt.Sprintf("Added barrier: shape=%v", ConvexPolygonStr(barrierCollider.Shape.(*resolv.ConvexPolygon))))
|
||||
space.Add(barrierCollider)
|
||||
barrierLocalId++
|
||||
}
|
||||
|
||||
world.Space = space
|
||||
|
||||
moveToCollide := true
|
||||
moveToCollide := false
|
||||
if moveToCollide {
|
||||
newVx, newVy := int32(-2959), int32(-2261)
|
||||
effPushback := Vec2D{X: float64(0), Y: float64(0)}
|
||||
toTestPlayerCollider := playerColliders[0]
|
||||
oldDx, oldDy := -2.98, -50.0
|
||||
dx, dy := oldDx, oldDy
|
||||
if collision := toTestPlayerCollider.Check(oldDx, oldDy, "Barrier"); collision != nil {
|
||||
playerShape := toTestPlayerCollider.Shape.(*resolv.ConvexPolygon)
|
||||
barrierShape := collision.Objects[0].Shape.(*resolv.ConvexPolygon)
|
||||
if overlapped, pushbackX, pushbackY := CalcPushbacks(oldDx, oldDy, playerShape, barrierShape); overlapped {
|
||||
Logger.Info(fmt.Sprintf("Collided & overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v, pushbackX=%v, pushbackY=%v", toTestPlayerCollider.X, toTestPlayerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape), pushbackX, pushbackY))
|
||||
dx -= pushbackX
|
||||
dy -= pushbackY
|
||||
} else {
|
||||
Logger.Info(fmt.Sprintf("Collider BUT not overlapped: player.X=%v, player.Y=%v, oldDx=%v, oldDy=%v, playerShape=%v, toCheckBarrier=%v", toTestPlayerCollider.X, toTestPlayerCollider.Y, oldDx, oldDy, ConvexPolygonStr(playerShape), ConvexPolygonStr(barrierShape)))
|
||||
}
|
||||
}
|
||||
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.X += dx
|
||||
toTestPlayerCollider.Y += dy
|
||||
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
|
||||
|
@@ -1,47 +1,46 @@
|
||||
package dnmshared
|
||||
|
||||
import (
|
||||
. "dnmshared/sharedprotos"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Use type `float64` for json unmarshalling of numbers.
|
||||
type Direction struct {
|
||||
Dx int32 `json:"dx,omitempty"`
|
||||
Dy int32 `json:"dy,omitempty"`
|
||||
}
|
||||
|
||||
type Vec2D struct {
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
func NormVec2D(dx, dy float64) Vec2D {
|
||||
return Vec2D{dy, -dx}
|
||||
return Vec2D{X: dy, Y: -dx}
|
||||
}
|
||||
|
||||
type Polygon2D struct {
|
||||
Anchor *Vec2D `json:"-"` // This "Polygon2D.Anchor" is used to be assigned to "B2BodyDef.Position", which in turn is used as the position of the FIRST POINT of the polygon.
|
||||
Points []*Vec2D `json:"-"`
|
||||
func AlignPolygon2DToBoundingBox(input *Polygon2D) *Polygon2D {
|
||||
// Transform again to put "anchor" at the top-left point of the bounding box for "resolv"
|
||||
boundingBoxTL := &Vec2D{
|
||||
X: math.MaxFloat64,
|
||||
Y: math.MaxFloat64,
|
||||
}
|
||||
for _, p := range input.Points {
|
||||
if p.X < boundingBoxTL.X {
|
||||
boundingBoxTL.X = p.X
|
||||
}
|
||||
if p.Y < boundingBoxTL.Y {
|
||||
boundingBoxTL.Y = p.Y
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
When used to represent a "polyline directly drawn in a `Tmx file`", we can initialize both "Anchor" and "Points" simultaneously.
|
||||
// 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)),
|
||||
}
|
||||
|
||||
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`".
|
||||
for i, p := range input.Points {
|
||||
output.Points[i] = &Vec2D{
|
||||
X: p.X - boundingBoxTL.X,
|
||||
Y: p.Y - boundingBoxTL.Y,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return output
|
||||
}
|
||||
|
||||
func Distance(pt1 *Vec2D, pt2 *Vec2D) float64 {
|
||||
|
@@ -1,3 +1,3 @@
|
||||
module tiled
|
||||
module dnmshared
|
||||
|
||||
go 1.19
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package dnmshared
|
||||
|
||||
import (
|
||||
. "dnmshared/sharedprotos"
|
||||
"fmt"
|
||||
"github.com/kvartborg/vector"
|
||||
"github.com/solarlune/resolv"
|
||||
@@ -11,14 +12,15 @@ import (
|
||||
func ConvexPolygonStr(body *resolv.ConvexPolygon) string {
|
||||
var s []string = make([]string, len(body.Points))
|
||||
for i, p := range body.Points {
|
||||
s[i] = fmt.Sprintf("[%v, %v]", p[0]+body.X, p[1]+body.Y)
|
||||
s[i] = fmt.Sprintf("[%.2f, %.2f]", p[0]+body.X, p[1]+body.Y)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
|
||||
return fmt.Sprintf("{\n%s\n}", strings.Join(s, ",\n"))
|
||||
}
|
||||
|
||||
func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
|
||||
collider := resolv.NewObject(origX-w*0.5+spaceOffsetX, origY-h*0.5+spaceOffsetY, w, h, tag)
|
||||
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
|
||||
@@ -54,7 +56,7 @@ func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceO
|
||||
return collider
|
||||
}
|
||||
|
||||
func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64) {
|
||||
func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64, *SatResult) {
|
||||
origX, origY := playerShape.Position()
|
||||
defer func() {
|
||||
playerShape.SetPosition(origX, origY)
|
||||
@@ -70,9 +72,9 @@ func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.Conve
|
||||
}
|
||||
if overlapped := IsPolygonPairOverlapped(playerShape, barrierShape, overlapResult); overlapped {
|
||||
pushbackX, pushbackY := overlapResult.Overlap*overlapResult.OverlapX, overlapResult.Overlap*overlapResult.OverlapY
|
||||
return true, pushbackX, pushbackY
|
||||
return true, pushbackX, pushbackY, overlapResult
|
||||
} else {
|
||||
return false, 0, 0
|
||||
return false, 0, 0, overlapResult
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,3 +220,36 @@ func isPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e vector.Vector, re
|
||||
// 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)
|
||||
}
|
||||
|
427
dnmshared/sharedprotos/geometry.pb.go
Normal 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
|
||||
}
|
@@ -3,6 +3,7 @@ package dnmshared
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
. "dnmshared/sharedprotos"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
@@ -173,8 +174,6 @@ func (l *TmxLayer) decodeBase64() ([]uint32, error) {
|
||||
return gids, nil
|
||||
}
|
||||
|
||||
type Vec2DList []*Vec2D
|
||||
type Polygon2DList []*Polygon2D
|
||||
type StrToVec2DListMap map[string]*Vec2DList
|
||||
type StrToPolygon2DListMap map[string]*Polygon2DList
|
||||
|
||||
@@ -233,10 +232,8 @@ func tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns *TmxMap, singleObjInTsxFile *T
|
||||
pointsCount := len(singleValueArray)
|
||||
|
||||
thePolygon2DFromPolyline := &Polygon2D{
|
||||
Anchor: nil,
|
||||
Points: make([]*Vec2D, pointsCount),
|
||||
TileWidth: pTsxIns.TileWidth,
|
||||
TileHeight: pTsxIns.TileHeight,
|
||||
Anchor: nil,
|
||||
Points: make([]*Vec2D, pointsCount),
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -327,16 +324,17 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
|
||||
if _, ok := theStrToPolygon2DListMap[key]; ok {
|
||||
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
||||
} else {
|
||||
thePolygon2DList := make(Polygon2DList, 0)
|
||||
theStrToPolygon2DListMap[key] = &thePolygon2DList
|
||||
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
||||
pThePolygon2DList = &Polygon2DList{
|
||||
Eles: make([]*Polygon2D, 0),
|
||||
}
|
||||
theStrToPolygon2DListMap[key] = pThePolygon2DList
|
||||
}
|
||||
|
||||
thePolygon2DFromPolyline, err := tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
*pThePolygon2DList = append(*pThePolygon2DList, thePolygon2DFromPolyline)
|
||||
pThePolygon2DList.Eles = append(pThePolygon2DList.Eles, thePolygon2DFromPolyline)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -352,8 +350,10 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
|
||||
var pTheVec2DListToCache *Vec2DList
|
||||
_, ok := toRetStrToVec2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
theVec2DListToCache := make(Vec2DList, 0)
|
||||
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache
|
||||
pTheVec2DListToCache = &Vec2DList{
|
||||
Eles: make([]*Vec2D, 0),
|
||||
}
|
||||
toRetStrToVec2DListMap[objGroup.Name] = pTheVec2DListToCache
|
||||
}
|
||||
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
|
||||
for _, singleObjInTmxFile := range objGroup.Objects {
|
||||
@@ -362,17 +362,18 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
|
||||
Y: singleObjInTmxFile.Y,
|
||||
}
|
||||
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
|
||||
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld)
|
||||
pTheVec2DListToCache.Eles = append(pTheVec2DListToCache.Eles, &thePosInWorld)
|
||||
}
|
||||
case "Barrier":
|
||||
// Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is exactly overlapping with "Polygon2D.Points[0]".
|
||||
var pThePolygon2DListToCache *Polygon2DList
|
||||
_, ok := toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
thePolygon2DListToCache := make(Polygon2DList, 0)
|
||||
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
|
||||
pThePolygon2DListToCache = &Polygon2DList{
|
||||
Eles: make([]*Polygon2D, 0),
|
||||
}
|
||||
toRetStrToPolygon2DListMap[objGroup.Name] = pThePolygon2DListToCache
|
||||
}
|
||||
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
|
||||
for _, singleObjInTmxFile := range objGroup.Objects {
|
||||
if nil == singleObjInTmxFile.Polyline {
|
||||
@@ -386,7 +387,7 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToP
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
|
||||
pThePolygon2DListToCache.Eles = append(pThePolygon2DListToCache.Eles, thePolygon2DInWorld)
|
||||
}
|
||||
default:
|
||||
}
|
||||
@@ -405,6 +406,12 @@ type TileRectilinearSize struct {
|
||||
}
|
||||
|
||||
func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerVec *Vec2D) Vec2D {
|
||||
if "orthogonal" == pTmxMapIns.Orientation {
|
||||
return Vec2D{
|
||||
X: continuousObjLayerVec.X,
|
||||
Y: -continuousObjLayerVec.Y,
|
||||
}
|
||||
}
|
||||
var tileRectilinearSize TileRectilinearSize
|
||||
tileRectilinearSize.Width = float64(pTmxMapIns.TileWidth)
|
||||
tileRectilinearSize.Height = float64(pTmxMapIns.TileHeight)
|
||||
@@ -427,55 +434,24 @@ func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuous
|
||||
}
|
||||
|
||||
func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continuousObjLayerOffset *Vec2D) Vec2D {
|
||||
layerOffset := Vec2D{
|
||||
X: 0,
|
||||
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5,
|
||||
var layerOffset Vec2D
|
||||
if "orthogonal" == pTmxMapIns.Orientation {
|
||||
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(calibratedVec)
|
||||
convertedVec := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerOffset)
|
||||
|
||||
toRet := Vec2D{
|
||||
return Vec2D{
|
||||
X: layerOffset.X + convertedVec.X,
|
||||
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
|
||||
}
|
||||
|
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "bd514df4-095e-4088-9060-d99397a29a4f",
|
||||
"isPlugin": true,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"uuid": "51c54820-d753-4be8-a855-5760eed8f7ef",
|
||||
"uuid": "5650b341-a420-4d79-a969-39a461e13378",
|
||||
"isSubpackage": false,
|
||||
"subpackageName": "",
|
||||
"subMetas": {}
|
65
frontend/assets/resources/map/dungeon/map.tmx
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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="8">
|
||||
<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>
|
||||
</objectgroup>
|
||||
</map>
|
5
frontend/assets/resources/map/dungeon/map.tmx.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ver": "2.0.2",
|
||||
"uuid": "1b802c87-1978-4c6a-bd0b-1f6b8526b3ad",
|
||||
"subMetas": {}
|
||||
}
|
4
frontend/assets/resources/map/dungeon/tiles0.tsx
Normal 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>
|
5
frontend/assets/resources/map/dungeon/tiles0.tsx.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ver": "2.0.0",
|
||||
"uuid": "d4cf0e72-7454-4310-b975-beb5e81a63ae",
|
||||
"subMetas": {}
|
||||
}
|
4
frontend/assets/resources/map/dungeon/tiles1.tsx
Normal 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>
|
5
frontend/assets/resources/map/dungeon/tiles1.tsx.meta
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ver": "2.0.0",
|
||||
"uuid": "0bcabaac-a406-4b3d-9285-814e00c5b09d",
|
||||
"subMetas": {}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"uuid": "2a231040-0e69-42b4-a35b-9690e976e71a",
|
||||
"isSubpackage": false,
|
||||
"subpackageName": "",
|
||||
"subMetas": {}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"ver": "2.0.0",
|
||||
"uuid": "acb40b5d-372b-4502-a6bf-f756908f8221",
|
||||
"subMetas": {}
|
||||
}
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a",
|
||||
"uuid": "94f42afa-43e1-4936-b5ac-7bcfe252f9e1",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
@@ -9,21 +9,21 @@
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"Tile_W256_H256_S01": {
|
||||
"amulet": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "4a23290b-bf5a-4849-ac19-6ebd4b7daa59",
|
||||
"rawTextureUuid": "d8e6c175-1f17-48df-a0aa-cdd9785f4d3a",
|
||||
"uuid": "52580025-ae7a-4c48-878f-f685bc90e737",
|
||||
"rawTextureUuid": "94f42afa-43e1-4936-b5ac-7bcfe252f9e1",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 64,
|
||||
"width": 1280,
|
||||
"height": 896,
|
||||
"rawWidth": 1280,
|
||||
"rawHeight": 1024,
|
||||
"trimY": 0,
|
||||
"width": 32,
|
||||
"height": 32,
|
||||
"rawWidth": 32,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
After Width: | Height: | Size: 189 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "136d09e9-c33c-45dc-abb7-e367d730c814",
|
||||
"uuid": "a9781031-1dba-4214-9167-080fc1920f92",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
@@ -9,21 +9,21 @@
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"Tile_W256_H128_S01": {
|
||||
"arcs1": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "7acc48f5-d9c9-4438-8794-57a85590bd97",
|
||||
"rawTextureUuid": "136d09e9-c33c-45dc-abb7-e367d730c814",
|
||||
"uuid": "5430edd4-3e0b-4509-a575-d7c7f8db4e31",
|
||||
"rawTextureUuid": "a9781031-1dba-4214-9167-080fc1920f92",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 831,
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 2048,
|
||||
"height": 386,
|
||||
"rawWidth": 2048,
|
||||
"rawHeight": 2048,
|
||||
"width": 32,
|
||||
"height": 32,
|
||||
"rawWidth": 32,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
After Width: | Height: | Size: 288 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "74245e28-6cec-4960-ac41-5b482ad8fd13",
|
||||
"uuid": "940f2fcc-df03-46ed-ad32-d92880efe501",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
@@ -9,21 +9,21 @@
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"Tile_W256_H256_S02": {
|
||||
"arcs2": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "8fc46c1f-6fb4-4290-99f3-b773b92312b7",
|
||||
"rawTextureUuid": "74245e28-6cec-4960-ac41-5b482ad8fd13",
|
||||
"uuid": "11915948-c7bf-41db-b6eb-74b364df7688",
|
||||
"rawTextureUuid": "940f2fcc-df03-46ed-ad32-d92880efe501",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 19.5,
|
||||
"offsetY": 3.5,
|
||||
"trimX": 89,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 1409,
|
||||
"height": 251,
|
||||
"rawWidth": 1548,
|
||||
"rawHeight": 258,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"rawWidth": 64,
|
||||
"rawHeight": 64,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd",
|
||||
"uuid": "ac193c85-fb5d-4d66-b911-81e2254e0009",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
@@ -9,21 +9,21 @@
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"Tile_W300_H300_S01": {
|
||||
"avatars": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "66b49304-7b5b-442c-92a5-d2b368abf659",
|
||||
"rawTextureUuid": "c30bd4d7-efdc-410c-8bdf-4a3dfc77bebd",
|
||||
"uuid": "b472e0d1-c866-4a4d-b8f9-4daca275b8db",
|
||||
"rawTextureUuid": "ac193c85-fb5d-4d66-b911-81e2254e0009",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 4,
|
||||
"offsetY": -24.5,
|
||||
"trimX": 97,
|
||||
"trimY": 85,
|
||||
"width": 114,
|
||||
"height": 179,
|
||||
"rawWidth": 300,
|
||||
"rawHeight": 300,
|
||||
"offsetX": -16.5,
|
||||
"offsetY": 2,
|
||||
"trimX": 2,
|
||||
"trimY": 0,
|
||||
"width": 91,
|
||||
"height": 28,
|
||||
"rawWidth": 128,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
After Width: | Height: | Size: 7.1 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 14 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.4 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 918 B |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 5.6 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 16 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.0 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.7 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 15 KiB |
@@ -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": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "902e544c-494e-4d24-989c-8101b081d516",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"dm300": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "e18e981b-7790-45be-a877-fa1ffccd9cfd",
|
||||
"rawTextureUuid": "902e544c-494e-4d24-989c-8101b081d516",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -29,
|
||||
"offsetY": 6,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 198,
|
||||
"height": 20,
|
||||
"rawWidth": 256,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "84e847c4-c2d1-4d77-8b62-439c7cc10d16",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"effects": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "c0abc05f-e714-47bf-8c8a-95a26b09cadb",
|
||||
"rawTextureUuid": "84e847c4-c2d1-4d77-8b62-439c7cc10d16",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 4.5,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 32,
|
||||
"height": 23,
|
||||
"rawWidth": 32,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "8c4dbca2-14e2-4ef6-9544-ee04f69bdaaa",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"elemental": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "1da68822-1fd7-49fc-b855-6c5fbf8a9af3",
|
||||
"rawTextureUuid": "8c4dbca2-14e2-4ef6-9544-ee04f69bdaaa",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -45.5,
|
||||
"offsetY": 1,
|
||||
"trimX": 1,
|
||||
"trimY": 0,
|
||||
"width": 163,
|
||||
"height": 14,
|
||||
"rawWidth": 256,
|
||||
"rawHeight": 16,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 123 B |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "9883dd54-d76a-49bb-b343-422da4f78853",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"exp_bar": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "c5c8aca4-800c-49d4-9ee3-fc15b1f738d3",
|
||||
"rawTextureUuid": "9883dd54-d76a-49bb-b343-422da4f78853",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 16,
|
||||
"height": 1,
|
||||
"rawWidth": 16,
|
||||
"rawHeight": 1,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "2ae831f7-5ac0-487f-91e6-659a279ab8ab",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"eye": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "e1476f8f-750c-454a-842b-d9197eee5e6d",
|
||||
"rawTextureUuid": "2ae831f7-5ac0-487f-91e6-659a279ab8ab",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -48,
|
||||
"offsetY": 7,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 160,
|
||||
"height": 18,
|
||||
"rawWidth": 256,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "6e7e65bb-322a-42ba-97a4-e7d9115e87d2",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"fireball": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "141a794c-5dea-4d33-80a7-b5efdf08ade6",
|
||||
"rawTextureUuid": "6e7e65bb-322a-42ba-97a4-e7d9115e87d2",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -0.5,
|
||||
"offsetY": 0.5,
|
||||
"trimX": 4,
|
||||
"trimY": 0,
|
||||
"width": 119,
|
||||
"height": 31,
|
||||
"rawWidth": 128,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "faaa1229-6f2e-4127-b884-962f6a104ffb",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"font15x": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "da1bd7b3-b4c9-4cde-9def-258b1fcd80eb",
|
||||
"rawTextureUuid": "faaa1229-6f2e-4127-b884-962f6a104ffb",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -171,
|
||||
"offsetY": 2,
|
||||
"trimX": 5,
|
||||
"trimY": 0,
|
||||
"width": 672,
|
||||
"height": 12,
|
||||
"rawWidth": 1024,
|
||||
"rawHeight": 16,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "a0072e35-a681-4fc1-9c11-b8717990bbcc",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"font1x": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "7c0a9e28-00f7-44e5-8199-a5b291df02c5",
|
||||
"rawTextureUuid": "a0072e35-a681-4fc1-9c11-b8717990bbcc",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -231,
|
||||
"offsetY": 0,
|
||||
"trimX": 3,
|
||||
"trimY": 0,
|
||||
"width": 556,
|
||||
"height": 8,
|
||||
"rawWidth": 1024,
|
||||
"rawHeight": 8,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "d9805b59-f11e-4c47-ac44-61b701c5eb46",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"font25x": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "652701b3-3b87-4095-9e6c-a21e4b138f83",
|
||||
"rawTextureUuid": "d9805b59-f11e-4c47-ac44-61b701c5eb46",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -67.5,
|
||||
"offsetY": 7.5,
|
||||
"trimX": 7,
|
||||
"trimY": 0,
|
||||
"width": 875,
|
||||
"height": 17,
|
||||
"rawWidth": 1024,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "0c3f3c09-e2ac-4073-9e4d-5c3b3be91bd5",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"font2x": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "1b899d40-9891-41d0-95ff-858334262984",
|
||||
"rawTextureUuid": "0c3f3c09-e2ac-4073-9e4d-5c3b3be91bd5",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -133.5,
|
||||
"offsetY": 1,
|
||||
"trimX": 6,
|
||||
"trimY": 0,
|
||||
"width": 745,
|
||||
"height": 14,
|
||||
"rawWidth": 1024,
|
||||
"rawHeight": 16,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 7.3 KiB |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"ver": "2.3.3",
|
||||
"uuid": "54b6e1a4-f3b7-4501-be07-16801c60c2c2",
|
||||
"type": "sprite",
|
||||
"wrapMode": "clamp",
|
||||
"filterMode": "bilinear",
|
||||
"premultiplyAlpha": false,
|
||||
"genMipmaps": false,
|
||||
"packable": true,
|
||||
"platformSettings": {},
|
||||
"subMetas": {
|
||||
"font3x": {
|
||||
"ver": "1.0.4",
|
||||
"uuid": "a9fb03c3-f719-4ac9-b1fd-e34b344a0d8a",
|
||||
"rawTextureUuid": "54b6e1a4-f3b7-4501-be07-16801c60c2c2",
|
||||
"trimType": "auto",
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": -413.5,
|
||||
"offsetY": 5,
|
||||
"trimX": 11,
|
||||
"trimY": 0,
|
||||
"width": 1199,
|
||||
"height": 22,
|
||||
"rawWidth": 2048,
|
||||
"rawHeight": 32,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"subMetas": {}
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.1 KiB |