Merge pull request #2 from genxium/backend_render_frame_calc

Added rejoining feature.
This commit is contained in:
Wing 2022-10-04 11:31:27 +08:00 committed by GitHub
commit 1e5d7d1d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2190 additions and 5584 deletions

View File

@ -1,3 +1,4 @@
# Potential avalanche from local lag
Under the current "input delay" algorithm, the lag of a single player would cause all the other players to receive outdated commands, e.g. when at a certain moment
- player#1: renderFrameId = 100, significantly lagged due to local CPU overheated
- player#2: renderFrameId = 240
@ -9,3 +10,34 @@ players #2, #3 #4 would receive "outdated(in their subjective feelings) but all-
In a "no-server & p2p" setup, I couldn't think of a proper way to cope with such edge case. Solely on the frontend we could only mitigate the impact to players #2, #3, #4, e.g. a potential lag due to "large range of frame-chasing" is proactively avoided in `<proj-root>/frontend/assets/scripts/Map.js, function update(dt)`.
However in a "server as authority" setup, the server could force confirming an inputFrame without player#1's upsync, and notify player#1 to apply a "roomDownsyncFrame" as well as drop all its outdated local inputFrames.
# Start up frames
renderFrameId | generatedInputFrameId | toApplyInputFrameId
-------------------|----------------------------|----------------------
0, 1, 2, 3 | 0, _EMP_, _EMP_, _EMP_ | 0
4, 5, 6, 7 | 1, _EMP_, _EMP_, _EMP_ | 0
8, 9, 10, 11 | 2, _EMP_, _EMP_, _EMP_ | 1
12, 13, 14, 15 | 3, _EMP_, _EMP_, _EMP_ | 2
It should be reasonable to assume that inputFrameId=0 is always of all-empty content, because human has no chance of clicking at the very first render frame.
# Alignment of the current setup
The following setup is chosen deliberately for some "%4" number coincidence.
- NstDelayFrames = 2
- InputDelayFrames = 4
- InputScaleFrames = 2
If "InputDelayFrames" is changed, the impact would be as follows, kindly note that "372%4 == 0".
### pR.InputDelayFrames = 4
renderFrameId | toApplyInputFrameId
--------------------------|----------------------------------------------------
368, 369, 370, 371 | 91
372, 373, 374, 375 | 92
### pR.InputDelayFrames = 5
renderFrameId | toApplyInputFrameId
--------------------------|----------------------------------------------------
..., ..., ..., 368 | 90
369, 370, 371, 372 | 91
373, 374, 375, ... | 92

View File

@ -3,13 +3,15 @@ If you'd like to play with the backend code seriously, please read the detailed
There could be some left over wechat-game related code pieces, but they're neither meant to work nor supported anymore.
# 1. Database Server
# 1. Building & running
The database product to be used for this project is MySQL 5.7.
## 1.1 Golang1.19.1
Documentation TBD.
We use [skeema](https://github.com/skeema/skeema) for schematic synchronization under `<proj-root>/database/skeema-repo-root/` which intentionally doesn't contain a `.skeema` file. Please read [this tutorial](https://shimo.im/doc/wQ0LvB0rlZcbHF5V) for more information.
## 1.2 MySQL
The database product to be used for this project is MySQL 5.7, you can install and manage `MySQL` server by [these scripts](https://github.com/genxium/Ubuntu14InitScripts/tree/master/database/mysql).
You can use [this node module (still under development)](https://github.com/genxium/node-mysqldiff-bridge) instead under `Windows10`, other versions of Windows are not yet tested for compatibility.
We use [skeema](https://github.com/skeema/skeema) for schematic synchronization under `<proj-root>/database/skeema-repo-root/` which intentionally doesn't contain a `.skeema` file. Please read [this tutorial](https://shimo.im/doc/wQ0LvB0rlZcbHF5V) for more information. For `Windows 10/11`, you can compile `skeema` from source and config the host to be `127.0.0.1` instead of `localhost` to use it, i.e. circumventing the pitfall for MySQL unix socket connection on Windows.
The following command(s)
```
@ -21,33 +23,25 @@ user@proj-root/database/skeema-repo-root> skeema diff
```
is recommended to be used for checking difference from your "live MySQL server" to the latest expected schema tracked in git.
# 2. Building & running
## 1.3 Required Config Files
## 2.1 Golang1.11
See https://github.com/genxium/Go111ModulePrac for details.
## 2.2 MySQL
On a product machine, you can install and manage `MySQL` server by [these scripts](https://github.com/genxium/Ubuntu14InitScripts/tree/master/database/mysql).
## 2.3 Required Config Files
### 2.3.1 Backend
### 1.3.1 Backend
- It needs `<proj-root>/battle_srv/configs/*` which is generated by `cd <proj-root>/battle_srv && cp -r ./configs.template ./configs` and necessary customization.
### 2.3.2 Frontend
### 1.3.2 Frontend
- It needs CocosCreator v2.2.1 to build.
- A required "CocosCreator plugin `i18n`" is already enclosed in the project, if you have a globally installed "CocosCreator plugin `i18n`"(often located at `$HOME/.CocosCreator/packages/`) they should be OK to co-exist.
- It needs `<proj-root>/frontend/assets/plugin_scripts/conf.js` which is generated by `cd <proj-root>/frontend/assets/plugin_scripts && cp conf.js.template conf.js`.
## 2.4 Troubleshooting
## 1.4 Troubleshooting
### 2.4.1 Redis snapshot writing failure
### 1.4.1 Redis snapshot writing failure
```
ErrFatal {"err": "MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error."}
```
Just restart your `redis-server` process.
# 3. Git configs cautions
# 2. Git configs cautions
Please make sure that you've set `ignorecase = false` in your `[core] section of <proj-root>/.git/config`.

View File

@ -94,7 +94,7 @@ func (w *wechat) GetJsConfig(uri string) (config *JsConfig, err error) {
return
}
//TODO add cache, getTicket 获取jsapi_ticket
// TODO add cache, getTicket 获取jsapi_ticket
func (w *wechat) getTicket() (ticketStr string, err error) {
var ticket resTicket
ticket, err = w.getTicketFromServer()
@ -131,7 +131,7 @@ func (w *wechat) GetOauth2Basic(authcode string) (result resAccessToken, err err
return
}
//UserInfo 用户授权获取到用户信息
// UserInfo 用户授权获取到用户信息
type UserInfo struct {
CommonError
OpenID string `json:"openid"`
@ -164,7 +164,7 @@ func (w *wechat) GetMoreInfo(accessToken string, openId string) (result UserInfo
return
}
//HTTPGet get 请求
// HTTPGet get 请求
func get(uri string) ([]byte, error) {
response, err := http.Get(uri)
if err != nil {
@ -182,7 +182,7 @@ func get(uri string) ([]byte, error) {
return body, err
}
//PostJSON post json 数据请求
// PostJSON post json 数据请求
func post(uri string, obj interface{}) ([]byte, error) {
jsonData, err := json.Marshal(obj)
if err != nil {
@ -206,7 +206,7 @@ func post(uri string, obj interface{}) ([]byte, error) {
return ioutil.ReadAll(response.Body)
}
//Signature sha1签名
// Signature sha1签名
func signature(params ...string) string {
sort.Strings(params)
h := sha1.New()
@ -216,7 +216,7 @@ func signature(params ...string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
//RandomStr 随机生成字符串
// RandomStr 随机生成字符串
func randomStr(length int) string {
str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
bytes := []byte(str)
@ -228,7 +228,7 @@ func randomStr(length int) string {
return string(result)
}
//getTicketFromServer 强制从服务器中获取ticket
// getTicketFromServer 强制从服务器中获取ticket
func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
var accessToken string
accessToken, err = w.getAccessTokenFromServer()
@ -256,7 +256,7 @@ func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
return
}
//GetAccessTokenFromServer 强制从微信服务器获取token
// GetAccessTokenFromServer 强制从微信服务器获取token
func (w *wechat) getAccessTokenFromServer() (accessToken string, err error) {
AccessTokenURL := w.config.ApiProtocol + "://" + w.config.ApiGateway + "/cgi-bin/token"
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", AccessTokenURL, w.config.AppID, w.config.AppSecret)

View File

@ -66,7 +66,7 @@ func createMysqlData(rows *sqlx.Rows, v string) {
}
}
//加上tableName参数, 用于pre_conf_data.sqlite里bot_player表的复用 --kobako
// 加上tableName参数, 用于pre_conf_data.sqlite里bot_player表的复用 --kobako
func maybeCreateNewPlayerFromBotTable(db *sqlx.DB, tableName string) {
var ls []*dbBotPlayer
err := db.Select(&ls, "SELECT name, magic_phone_country_code, magic_phone_num, display_name FROM "+tableName)

View File

@ -1,37 +1,46 @@
module server
go 1.19
require (
github.com/ByteArena/box2d v1.0.2
github.com/ChimeraCoder/gojson v1.0.0 // indirect
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414
github.com/davecgh/go-spew v1.1.1
github.com/fatih/color v1.7.0 // indirect
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/gin-gonic/gin v1.3.0
github.com/githubnemo/CompileDaemon v1.0.0 // indirect
github.com/go-redis/redis v6.13.2+incompatible
github.com/go-sql-driver/mysql v1.4.0
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.9 // indirect
github.com/gorilla/websocket v1.2.0
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
github.com/howeyc/fsnotify v0.9.0 // indirect
github.com/imdario/mergo v0.3.6
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/solarlune/resolv v0.5.1
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713
go.uber.org/zap v1.9.1
google.golang.org/protobuf v1.28.1
)
require (
github.com/ChimeraCoder/gojson v1.0.0 // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
github.com/githubnemo/CompileDaemon v1.0.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/howeyc/fsnotify v0.9.0 // indirect
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mattn/go-sqlite3 v1.9.0
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/ugorji/go v1.1.1 // indirect
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.9.1
google.golang.org/protobuf v1.28.1
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
)

View File

@ -45,6 +45,8 @@ github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0 h1:5B0uxl2lzNRVkJVg+uGHxWtRt4C0Wjc6kJKo5XYx8xE=
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0 h1:v8lWpj5957KtDMKu+xQtlu6G3ZoZR6Tn9bsfZCRG5Xw=
github.com/kvartborg/vector v0.0.0-20200419093813-2cba0cabb4f0/go.mod h1:GAX7tMJqXx9fB1BrsTWPOXy6IBRX+J461BffVPAdpwo=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@ -55,12 +57,18 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/solarlune/resolv v0.5.1 h1:Ul6PAs/zaxiMUOEYz1Z6VeUj5k3CDcWMvSh+kivybDY=
github.com/solarlune/resolv v0.5.1/go.mod h1:HjM2f/0NoVjVdZsi26GtugX5aFbA62COEFEXkOhveRw=
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713 h1:knaxjm6QMbUMNvuaSnJZmw0gRX4V/79JVUQiziJGM84=
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA=
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
@ -71,6 +79,8 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

View File

@ -1,13 +1,5 @@
package models
import (
"github.com/ByteArena/box2d"
)
type Barrier struct {
X float64
Y float64
Type uint32
Boundary *Polygon2D
CollidableBody *box2d.B2Body
Boundary *Polygon2D
}

View File

@ -1,19 +0,0 @@
package models
import (
"github.com/ByteArena/box2d"
)
type Bullet struct {
LocalIdInBattle int32 `json:"-"`
LinearSpeed float64 `json:"-"`
X float64 `json:"-"`
Y float64 `json:"-"`
Removed bool `json:"-"`
Dir *Direction `json:"-"`
StartAtPoint *Vec2D `json:"-"`
EndAtPoint *Vec2D `json:"-"`
DamageBoundary *Polygon2D `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
RemovedAtFrameId int32 `json:"-"`
}

View File

@ -86,7 +86,7 @@ func (p *InRangePlayerCollection) NextPlayerToAttack() *InRangePlayerNode {
//TODO: 完成重构
/// Doubly circular linked list Implement
// / Doubly circular linked list Implement
type InRangePlayerNode struct {
Prev *InRangePlayerNode
Next *InRangePlayerNode

View File

@ -33,6 +33,14 @@ func toPbVec2DList(modelInstance *Vec2DList) *pb.Vec2DList {
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)),
@ -43,24 +51,10 @@ func toPbPolygon2DList(modelInstance *Polygon2DList) *pb.Polygon2DList {
return toRet
}
func ToPbStrToBattleColliderInfo(intervalToPing int32, willKickIfInactiveFor int32, boundRoomId int32, stageName string, modelInstance1 StrToVec2DListMap, modelInstance2 StrToPolygon2DListMap, stageDiscreteW int32, stageDiscreteH int32, stageTileW int32, stageTileH int32) *pb.BattleColliderInfo {
toRet := &pb.BattleColliderInfo{
IntervalToPing: intervalToPing,
WillKickIfInactiveFor: willKickIfInactiveFor,
BoundRoomId: boundRoomId,
StageName: stageName,
StrToVec2DListMap: make(map[string]*pb.Vec2DList, 0),
StrToPolygon2DListMap: make(map[string]*pb.Polygon2DList, 0),
StageDiscreteW: stageDiscreteW,
StageDiscreteH: stageDiscreteH,
StageTileW: stageTileW,
StageTileH: stageTileH,
}
for k, v := range modelInstance1 {
toRet.StrToVec2DListMap[k] = toPbVec2DList(v)
}
for k, v := range modelInstance2 {
toRet.StrToPolygon2DListMap[k] = toPbPolygon2DList(v)
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
}
@ -90,114 +84,3 @@ func toPbPlayers(modelInstances map[int32]*Player) map[int32]*pb.Player {
return toRet
}
func toPbTreasures(modelInstances map[int32]*Treasure) map[int32]*pb.Treasure {
toRet := make(map[int32]*pb.Treasure, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
toRet[k] = &pb.Treasure{
Id: last.Id,
LocalIdInBattle: last.LocalIdInBattle,
Score: last.Score,
X: last.X,
Y: last.Y,
Removed: last.Removed,
Type: last.Type,
}
}
return toRet
}
func toPbTraps(modelInstances map[int32]*Trap) map[int32]*pb.Trap {
toRet := make(map[int32]*pb.Trap, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
toRet[k] = &pb.Trap{
Id: last.Id,
LocalIdInBattle: last.LocalIdInBattle,
X: last.X,
Y: last.Y,
Removed: last.Removed,
Type: last.Type,
}
}
return toRet
}
func toPbBullets(modelInstances map[int32]*Bullet) map[int32]*pb.Bullet {
toRet := make(map[int32]*pb.Bullet, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
if nil == last.StartAtPoint || nil == last.EndAtPoint {
continue
}
toRet[k] = &pb.Bullet{
LocalIdInBattle: last.LocalIdInBattle,
LinearSpeed: last.LinearSpeed,
X: last.X,
Y: last.Y,
Removed: last.Removed,
StartAtPoint: &pb.Vec2D{
X: last.StartAtPoint.X,
Y: last.StartAtPoint.Y,
},
EndAtPoint: &pb.Vec2D{
X: last.EndAtPoint.X,
Y: last.EndAtPoint.Y,
},
}
}
return toRet
}
func toPbSpeedShoes(modelInstances map[int32]*SpeedShoe) map[int32]*pb.SpeedShoe {
toRet := make(map[int32]*pb.SpeedShoe, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
toRet[k] = &pb.SpeedShoe{
Id: last.Id,
LocalIdInBattle: last.LocalIdInBattle,
X: last.X,
Y: last.Y,
Removed: last.Removed,
Type: last.Type,
}
}
return toRet
}
func toPbGuardTowers(modelInstances map[int32]*GuardTower) map[int32]*pb.GuardTower {
toRet := make(map[int32]*pb.GuardTower, 0)
if nil == modelInstances {
return toRet
}
for k, last := range modelInstances {
toRet[k] = &pb.GuardTower{
Id: last.Id,
LocalIdInBattle: last.LocalIdInBattle,
X: last.X,
Y: last.Y,
Removed: last.Removed,
Type: last.Type,
}
}
return toRet
}

View File

@ -3,7 +3,6 @@ package models
import (
"database/sql"
"fmt"
"github.com/ByteArena/box2d"
sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx"
)
@ -37,7 +36,7 @@ type Player struct {
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Dir *Direction `json:"dir,omitempty"`
Speed int32 `json:"speed,omitempty"`
Speed float64 `json:"speed,omitempty"`
BattleState int32 `json:"battleState,omitempty"`
LastMoveGmtMillis int32 `json:"lastMoveGmtMillis,omitempty"`
Score int32 `json:"score,omitempty"`
@ -48,16 +47,15 @@ type Player struct {
DisplayName string `json:"displayName,omitempty" db:"display_name"`
Avatar string `json:"avatar,omitempty"`
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"`
CollidableBody *box2d.B2Body `json:"-"`
AckingFrameId int32 `json:"ackingFrameId"`
AckingInputFrameId int32 `json:"-"`
LastSentInputFrameId int32 `json:"-"`
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:"-"`
}
func ExistPlayerByName(name string) (bool, error) {

View File

@ -1,14 +0,0 @@
package models
import "github.com/ByteArena/box2d"
type Pumpkin struct {
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
LinearSpeed float64 `json:"linearSpeed,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Removed bool `json:"removed,omitempty"`
Dir *Direction `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
RemovedAtFrameId int32 `json:"-"`
}

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,6 @@ package models
import (
"container/heap"
"fmt"
"github.com/gorilla/websocket"
"go.uber.org/zap"
. "server/common"
"sync"
@ -92,43 +91,13 @@ func InitRoomHeapManager() {
for i := 0; i < initialCountOfRooms; i++ {
roomCapacity := 2
joinIndexBooleanArr := make([]bool, roomCapacity)
for index, _ := range joinIndexBooleanArr {
joinIndexBooleanArr[index] = false
}
currentRoomBattleState := RoomBattleStateIns.IDLE
pq[i] = &Room{
Id: int32(i + 1),
Players: make(map[int32]*Player),
PlayerDownsyncSessionDict: make(map[int32]*websocket.Conn),
PlayerSignalToCloseDict: make(map[int32]SignalToCloseConnCbType),
Capacity: roomCapacity,
Score: calRoomScore(0, roomCapacity, currentRoomBattleState),
State: currentRoomBattleState,
Index: i,
Tick: 0,
EffectivePlayerCount: 0,
//BattleDurationNanos: int64(5 * 1000 * 1000 * 1000),
BattleDurationNanos: int64(30 * 1000 * 1000 * 1000),
ServerFPS: 60,
Treasures: make(map[int32]*Treasure),
Traps: make(map[int32]*Trap),
GuardTowers: make(map[int32]*GuardTower),
Bullets: make(map[int32]*Bullet),
SpeedShoes: make(map[int32]*SpeedShoe),
Barriers: make(map[int32]*Barrier),
Pumpkins: make(map[int32]*Pumpkin),
AccumulatedLocalIdForBullets: 0,
AllPlayerInputsBuffer: NewRingBuffer(1024),
LastAllConfirmedInputFrameId: -1,
LastAllConfirmedInputFrameIdWithChange: -1,
LastAllConfirmedInputList: make([]uint64, roomCapacity),
InputDelayFrames: 4,
InputScaleFrames: 2,
JoinIndexBooleanArr: joinIndexBooleanArr,
Id: int32(i + 1),
Capacity: roomCapacity,
Index: i,
}
roomMap[pq[i].Id] = pq[i]
pq[i].ChooseStage()
pq[i].OnDismissed()
}
heap.Init(&pq)
RoomHeapManagerIns = &pq

View File

@ -1,17 +0,0 @@
package models
import (
"github.com/ByteArena/box2d"
)
type SpeedShoe struct {
Id int32 `json:"id,omitempty"`
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Removed bool `json:"removed,omitempty"`
Type int32 `json:"type,omitempty"`
PickupBoundary *Polygon2D `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
RemovedAtFrameId int32 `json:"-"`
}

View File

@ -6,8 +6,6 @@ import (
"encoding/base64"
"encoding/xml"
"errors"
"fmt"
"github.com/ByteArena/box2d"
"go.uber.org/zap"
"io/ioutil"
"math"
@ -19,8 +17,7 @@ import (
const (
LOW_SCORE_TREASURE_TYPE = 1
HIGH_SCORE_TREASURE_TYPE = 2
SPEED_SHOES_TYPE = 3
SPEED_SHOES_TYPE = 3
LOW_SCORE_TREASURE_SCORE = 100
HIGH_SCORE_TREASURE_SCORE = 200
@ -182,17 +179,12 @@ type Polygon2DList []*Polygon2D
type StrToVec2DListMap map[string]*Vec2DList // Note that it's deliberately NOT using "map[string]Vec2DList", for the easy of passing return value to "models/room.go".
type StrToPolygon2DListMap map[string]*Polygon2DList // Note that it's deliberately NOT using "map[string]Polygon2DList", for the easy of passing return value to "models/room.go".
func TmxPolylineToPolygon2DInB2World(pTmxMapIns *TmxMap, singleObjInTmxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline) (*Polygon2D, error) {
func tmxPolylineToPolygon2D(pTmxMapIns *TmxMap, singleObjInTmxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline) (*Polygon2D, error) {
if nil == targetPolyline {
return nil, nil
}
singleValueArray := strings.Split(targetPolyline.Points, " ")
pointsCount := len(singleValueArray)
if pointsCount >= box2d.B2_maxPolygonVertices {
return nil, errors.New(fmt.Sprintf("During `TmxPolylineToPolygon2DInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v, of polyines [%v]", pointsCount, box2d.B2_maxPolygonVertices, singleValueArray))
}
theUntransformedAnchor := &Vec2D{
X: singleObjInTmxFile.X,
@ -218,7 +210,6 @@ func TmxPolylineToPolygon2DInB2World(pTmxMapIns *TmxMap, singleObjInTmxFile *Tmx
}
}
// Transform to B2World space coordinate.
tmp := &Vec2D{
X: thePolygon2DFromPolyline.Points[k].X,
Y: thePolygon2DFromPolyline.Points[k].Y,
@ -231,7 +222,7 @@ func TmxPolylineToPolygon2DInB2World(pTmxMapIns *TmxMap, singleObjInTmxFile *Tmx
return thePolygon2DFromPolyline, nil
}
func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInTsxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline, pTsxIns *Tsx) (*Polygon2D, error) {
func tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns *TmxMap, singleObjInTsxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline, pTsxIns *Tsx) (*Polygon2D, error) {
if nil == targetPolyline {
return nil, nil
}
@ -242,10 +233,6 @@ func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInT
singleValueArray := strings.Split(targetPolyline.Points, " ")
pointsCount := len(singleValueArray)
if pointsCount >= box2d.B2_maxPolygonVertices {
return nil, errors.New(fmt.Sprintf("During `TsxPolylineToOffsetsWrtTileCenterInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v", pointsCount, box2d.B2_maxPolygonVertices))
}
thePolygon2DFromPolyline := &Polygon2D{
Anchor: nil,
Points: make([]*Vec2D, pointsCount),
@ -254,7 +241,7 @@ func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInT
}
/*
[WARNING] In this case, the "Treasure"s and "GuardTower"s are put into Tmx file as "ImageObject"s, of each the "ProportionalAnchor" is (0.5, 0). Therefore we calculate that "thePolygon2DFromPolyline.Points" are "offsets(in B2World) w.r.t. the BottomCenter". See https://shimo.im/docs/SmLJJhXm2C8XMzZT for details.
[WARNING] In this case, the "Treasure"s and "GuardTower"s are put into Tmx file as "ImageObject"s, of each the "ProportionalAnchor" is (0.5, 0). Therefore the "thePolygon2DFromPolyline.Points" are "offsets w.r.t. the BottomCenter". See https://shimo.im/docs/SmLJJhXm2C8XMzZT for details.
*/
for k, value := range singleValueArray {
@ -272,14 +259,12 @@ func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInT
thePolygon2DFromPolyline.Points[k].Y = float64(pTsxIns.TileHeight) - (coordinateValue + offsetFromTopLeftInTileLocalCoordY)
}
}
// No need to transform for B2World space coordinate because the marks in a Tsx file is already rectilinear.
}
return thePolygon2DFromPolyline, nil
}
func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, firstGid int, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) error {
func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, firstGid int, gidBoundariesMap map[int]StrToPolygon2DListMap) error {
pTsxIns := &Tsx{}
err := xml.Unmarshal(byteArrOfTsxFile, pTsxIns)
if nil != err {
@ -297,7 +282,7 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
for _, tile := range pTsxIns.Tiles {
globalGid := (firstGid + int(tile.Id))
/**
Per tile xml str could be
A tile xml string could be
```
<tile id="13">
@ -313,7 +298,7 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
```
, we currently REQUIRE that "`an object of a tile` with ONE OR MORE polylines must come with a single corresponding '<property name=`type` value=`...` />', and viceversa".
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for how we theoretically fit a "Polyline in Tsx" into a "Polygon2D" and then into the corresponding "B2BodyDef & B2Body in the `world of colliding bodies`".
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for how we theoretically fit a "Polyline in Tsx" into a "Polygon2D".
*/
theObjGroup := tile.ObjectGroup
@ -332,11 +317,11 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
key := singleObj.Properties.Property[0].Value
var theStrToPolygon2DListMap StrToPolygon2DListMap
if existingStrToPolygon2DListMap, ok := gidBoundariesMapInB2World[globalGid]; ok {
if existingStrToPolygon2DListMap, ok := gidBoundariesMap[globalGid]; ok {
theStrToPolygon2DListMap = existingStrToPolygon2DListMap
} else {
gidBoundariesMapInB2World[globalGid] = make(StrToPolygon2DListMap, 0)
theStrToPolygon2DListMap = gidBoundariesMapInB2World[globalGid]
gidBoundariesMap[globalGid] = make(StrToPolygon2DListMap, 0)
theStrToPolygon2DListMap = gidBoundariesMap[globalGid]
}
var pThePolygon2DList *Polygon2DList
@ -348,7 +333,7 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
pThePolygon2DList = theStrToPolygon2DListMap[key]
}
thePolygon2DFromPolyline, err := TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
thePolygon2DFromPolyline, err := tsxPolylineToOffsetsWrtTileCenter(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
if nil != err {
panic(err)
}
@ -358,18 +343,9 @@ func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, f
return nil
}
func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) (int32, int32, int32, int32, StrToVec2DListMap, StrToPolygon2DListMap, error) {
func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMap map[int]StrToPolygon2DListMap) (int32, int32, int32, int32, StrToVec2DListMap, StrToPolygon2DListMap, error) {
toRetStrToVec2DListMap := make(StrToVec2DListMap, 0)
toRetStrToPolygon2DListMap := make(StrToPolygon2DListMap, 0)
/*
Note that both
- "Vec2D"s of "toRetStrToVec2DListMap", and
- "Polygon2D"s of "toRetStrToPolygon2DListMap"
are already transformed into the "coordinate of B2World".
-- YFLu
*/
for _, objGroup := range pTmxMapIns.ObjectGroups {
switch objGroup.Name {
@ -379,10 +355,8 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[i
if false == ok {
theVec2DListToCache := make(Vec2DList, 0)
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
} else {
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
}
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects {
theUntransformedPos := &Vec2D{
X: singleObjInTmxFile.X,
@ -391,22 +365,15 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[i
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld)
}
case "Pumpkin", "SpeedShoe":
case "Barrier":
/*
Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is located exactly in an overlapping with "Polygon2D.Points[0]" w.r.t. B2World.
-- YFLu
*/
// 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 = toRetStrToPolygon2DListMap[objGroup.Name]
} else {
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
}
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
for _, singleObjInTmxFile := range objGroup.Objects {
if nil == singleObjInTmxFile.Polyline {
@ -416,71 +383,12 @@ func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[i
continue
}
thePolygon2DInWorld, err := TmxPolylineToPolygon2DInB2World(pTmxMapIns, singleObjInTmxFile, singleObjInTmxFile.Polyline)
thePolygon2DInWorld, err := tmxPolylineToPolygon2D(pTmxMapIns, singleObjInTmxFile, singleObjInTmxFile.Polyline)
if nil != err {
panic(err)
}
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
}
case "LowScoreTreasure", "GuardTower", "HighScoreTreasure":
/*
Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" ISN'T located exactly in an overlapping with "Polygon2D.Points[0]" w.r.t. B2World, refer to "https://shimo.im/docs/SmLJJhXm2C8XMzZT" for details.
-- YFLu
*/
for _, singleObjInTmxFile := range objGroup.Objects {
if nil == singleObjInTmxFile.Gid {
continue
}
theGlobalGid := singleObjInTmxFile.Gid
theStrToPolygon2DListMap, ok := gidBoundariesMapInB2World[*theGlobalGid]
if false == ok {
continue
}
pThePolygon2DList, ok := theStrToPolygon2DListMap[objGroup.Name]
if false == ok {
continue
}
var pThePolygon2DListToCache *Polygon2DList
_, ok = toRetStrToPolygon2DListMap[objGroup.Name]
if false == ok {
thePolygon2DListToCache := make(Polygon2DList, 0)
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
} else {
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
}
for _, thePolygon2D := range *pThePolygon2DList {
theUntransformedBottomCenterAsAnchor := &Vec2D{
X: singleObjInTmxFile.X,
Y: singleObjInTmxFile.Y,
}
theTransformedBottomCenterAsAnchor := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedBottomCenterAsAnchor)
thePolygon2DInWorld := &Polygon2D{
Anchor: &theTransformedBottomCenterAsAnchor,
Points: make([]*Vec2D, len(thePolygon2D.Points)),
TileWidth: thePolygon2D.TileWidth,
TileHeight: thePolygon2D.TileHeight,
}
if nil != singleObjInTmxFile.Width && nil != singleObjInTmxFile.Height {
thePolygon2DInWorld.TmxObjectWidth = *singleObjInTmxFile.Width
thePolygon2DInWorld.TmxObjectHeight = *singleObjInTmxFile.Height
}
for kk, p := range thePolygon2D.Points {
// [WARNING] It's intentionally recreating a copy of "Vec2D" here.
thePolygon2DInWorld.Points[kk] = &Vec2D{
X: p.X,
Y: p.Y,
}
}
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
}
}
default:
}
}

View File

@ -1,39 +0,0 @@
package models
import (
"github.com/ByteArena/box2d"
)
type Trap struct {
Id int32 `json:"id,omitempty"`
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
Type int32 `json:"type,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Removed bool `json:"removed,omitempty"`
PickupBoundary *Polygon2D `json:"-"`
TrapBullets []*Bullet `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
RemovedAtFrameId int32 `json:"-"`
}
type GuardTower struct {
Id int32 `json:"id,omitempty"`
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
Type int32 `json:"type,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Removed bool `json:"removed,omitempty"`
PickupBoundary *Polygon2D `json:"-"`
TrapBullets []*Bullet `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
RemovedAtFrameId int32 `json:"-"`
InRangePlayers *InRangePlayerCollection `json:"-"`
LastAttackTick int64 `json:"-"`
TileWidth float64 `json:"-"`
TileHeight float64 `json:"-"`
WidthInB2World float64 `json:"-"`
HeightInB2World float64 `json:"-"`
}

View File

@ -1,18 +0,0 @@
package models
import (
"github.com/ByteArena/box2d"
)
type Treasure struct {
Id int32 `json:"id,omitempty"`
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
Score int32 `json:"score,omitempty"`
X float64 `json:"x,omitempty"`
Y float64 `json:"y,omitempty"`
Removed bool `json:"removed,omitempty"`
Type int32 `json:"type,omitempty"`
PickupBoundary *Polygon2D `json:"-"`
CollidableBody *box2d.B2Body `json:"-"`
}

File diff suppressed because it is too large Load Diff

View File

@ -240,16 +240,37 @@ func Serve(c *gin.Context) {
})
}()
playerBattleColliderInfo := models.ToPbStrToBattleColliderInfo(int32(Constants.Ws.IntervalToPing), int32(Constants.Ws.WillKickIfInactiveFor), pRoom.Id, pRoom.StageName, pRoom.RawBattleStrToVec2DListMap, pRoom.RawBattleStrToPolygon2DListMap, pRoom.StageDiscreteW, pRoom.StageDiscreteH, pRoom.StageTileW, pRoom.StageTileH)
// Construct "battleColliderInfo" to downsync
bciFrame := &pb.BattleColliderInfo{
BoundRoomId: pRoom.Id,
StageName: pRoom.StageName,
StrToVec2DListMap: models.ToPbVec2DListMap(pRoom.RawBattleStrToVec2DListMap),
StrToPolygon2DListMap: models.ToPbPolygon2DListMap(pRoom.RawBattleStrToPolygon2DListMap),
StageDiscreteW: pRoom.StageDiscreteW,
StageDiscreteH: pRoom.StageDiscreteH,
StageTileW: pRoom.StageTileW,
StageTileH: pRoom.StageTileH,
IntervalToPing: int32(Constants.Ws.IntervalToPing),
WillKickIfInactiveFor: int32(Constants.Ws.WillKickIfInactiveFor),
BattleDurationNanos: pRoom.BattleDurationNanos,
ServerFps: pRoom.ServerFps,
InputDelayFrames: pRoom.InputDelayFrames,
InputScaleFrames: pRoom.InputScaleFrames,
NstDelayFrames: pRoom.NstDelayFrames,
InputFrameUpsyncDelayTolerance: pRoom.InputFrameUpsyncDelayTolerance,
MaxChasingRenderFramesPerUpdate: pRoom.MaxChasingRenderFramesPerUpdate,
PlayerBattleState: pThePlayer.BattleState, // For frontend to know whether it's rejoining
}
resp := &pb.WsResp{
Ret: int32(Constants.RetCode.Ok),
EchoedMsgId: int32(0),
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
BciFrame: playerBattleColliderInfo,
BciFrame: bciFrame,
}
// Logger.Info("Sending downsync HeartbeatRequirements:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("resp", resp))
Logger.Debug("Sending downsync HeartbeatRequirements:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("resp", resp))
theBytes, marshalErr := proto.Marshal(resp)
if nil != marshalErr {

View File

@ -1,6 +1,5 @@
"use strict";
var _ROUTE_PATH;
function _defineProperty(obj, key, value) {
@ -29,23 +28,6 @@ var constants = {
BGM: "BGM"
}
},
PLAYER_NAME: {
1: "Merdan",
2: "Monroe",
},
SOCKET_EVENT: {
CONTROL: "control",
SYNC: "sync",
LOGIN: "login",
CREATE: "create"
},
WECHAT: {
AUTHORIZE_PATH: "/connect/oauth2/authorize",
REDIRECT_RUI_KEY: "redirect_uri=",
RESPONSE_TYPE: "response_type=code",
SCOPE: "scope=snsapi_userinfo",
FIN: "#wechat_redirect"
},
ROUTE_PATH: (_ROUTE_PATH = {
PLAYER: "/player",
JSCONFIG: "/jsconfig",
@ -61,8 +43,6 @@ var constants = {
LIST: "/list",
READ: "/read",
PROFILE: "/profile",
WECHAT: "/wechat",
WECHATGAME: "/wechatGame",
FETCH: "/fetch",
}, _defineProperty(_ROUTE_PATH, "LOGIN", "/login"), _defineProperty(_ROUTE_PATH, "RET_CODE", "/retCode"), _defineProperty(_ROUTE_PATH, "REGEX", "/regex"), _defineProperty(_ROUTE_PATH, "SMS_CAPTCHA", "/SmsCaptcha"), _defineProperty(_ROUTE_PATH, "GET", "/get"), _ROUTE_PATH),
REQUEST_QUERY: {
@ -138,7 +118,6 @@ var constants = {
INCORRECT_PHONE_NUMBER: '手机号不正确',
LOG_OUT: '您已在其他地方登陆',
GAME_OVER: '游戏结束,您的得分是',
WECHAT_LOGIN_FAILS: "微信登录失败",
},
CONFIRM_BUTTON_LABEL: {
RESTART: '重新开始'

View File

@ -27,17 +27,25 @@ message Polygon2DList {
}
message BattleColliderInfo {
int32 intervalToPing = 1;
int32 willKickIfInactiveFor = 2;
int32 boundRoomId = 3;
string stageName = 1;
map<string, Vec2DList> strToVec2DListMap = 2;
map<string, Polygon2DList> strToPolygon2DListMap = 3;
int32 stageDiscreteW = 4;
int32 stageDiscreteH = 5;
int32 stageTileW = 6;
int32 stageTileH = 7;
string stageName = 4;
map<string, Vec2DList> strToVec2DListMap = 5;
map<string, Polygon2DList> strToPolygon2DListMap = 6;
int32 StageDiscreteW = 7;
int32 StageDiscreteH = 8;
int32 StageTileW = 9;
int32 StageTileH = 10;
int32 intervalToPing = 8;
int32 willKickIfInactiveFor = 9;
int32 boundRoomId = 10;
int64 battleDurationNanos = 11;
int32 serverFps = 12;
int32 inputDelayFrames = 13;
uint32 inputScaleFrames = 14;
int32 nstDelayFrames = 15;
int32 inputFrameUpsyncDelayTolerance = 16;
int32 maxChasingRenderFramesPerUpdate = 17;
int32 playerBattleState = 18;
}
message Player {
@ -45,7 +53,7 @@ message Player {
double x = 2;
double y = 3;
Direction dir = 4;
int32 speed = 5;
double speed = 5;
int32 battleState = 6;
int32 lastMoveGmtMillis = 7;
int32 score = 10;
@ -61,75 +69,6 @@ message PlayerMeta {
int32 joinIndex = 5;
}
message Treasure {
int32 id = 1;
int32 localIdInBattle = 2;
int32 score = 3;
double x = 4;
double y = 5;
bool removed = 6;
int32 type = 7;
}
message Bullet {
int32 localIdInBattle = 1;
double linearSpeed = 2;
double x = 3;
double y = 4;
bool removed = 5;
Vec2D startAtPoint = 6;
Vec2D endAtPoint = 7;
}
message Trap {
int32 id = 1;
int32 localIdInBattle = 2;
int32 type = 3;
double x = 4;
double y = 5;
bool removed = 6;
}
message SpeedShoe {
int32 id = 1;
int32 localIdInBattle = 2;
double x = 3;
double y = 4;
bool removed = 5;
int32 type = 6;
}
message Pumpkin {
int32 localIdInBattle = 1;
double linearSpeed = 2;
double x = 3;
double y = 4;
bool removed = 5;
}
message GuardTower {
int32 id = 1;
int32 localIdInBattle = 2;
int32 type = 3;
double x = 4;
double y = 5;
bool removed = 6;
}
message RoomDownsyncFrame {
int32 id = 1;
int32 refFrameId = 2;
map<int32, Player> players = 3;
int64 sentAt = 4;
int64 countdownNanos = 5;
map<int32, Treasure> treasures = 6;
map<int32, Trap> traps = 7;
map<int32, Bullet> bullets = 8;
map<int32, SpeedShoe> speedShoes = 9;
map<int32, GuardTower> guardTowers = 10;
map<int32, PlayerMeta> playerMetas = 11;
}
message InputFrameUpsync {
int32 inputFrameId = 1;
int32 encodedDir = 6;
@ -145,6 +84,13 @@ message HeartbeatUpsync {
int64 clientTimestamp = 1;
}
message RoomDownsyncFrame {
int32 id = 1;
map<int32, Player> players = 2;
int64 countdownNanos = 3;
map<int32, PlayerMeta> playerMetas = 4;
}
message WsReq {
int32 msgId = 1;
int32 playerId = 2;

View File

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

View File

@ -18,11 +18,18 @@ window.ALL_BATTLE_STATES = {
};
window.MAGIC_ROOM_DOWNSYNC_FRAME_ID = {
PLAYER_ADDED_AND_ACKED: -98,
PLAYER_READDED_AND_ACKED: -97,
BATTLE_READY_TO_START: -1,
BATTLE_START: 0,
BATTLE_START: 0
};
window.PlayerBattleState = {
ADDED_PENDING_BATTLE_COLLIDER_ACK: 0,
READDED_PENDING_BATTLE_COLLIDER_ACK: 1,
ACTIVE: 2,
DISCONNECTED: 3,
LOST: 4,
EXPELLED_DURING_GAME: 5,
EXPELLED_IN_DISMISSAL: 6
};
cc.Class({
@ -45,18 +52,6 @@ cc.Class({
type: cc.Prefab,
default: null,
},
treasurePrefab: {
type: cc.Prefab,
default: null,
},
trapPrefab: {
type: cc.Prefab,
default: null,
},
speedShoePrefab: {
type: cc.Prefab,
default: null,
},
polygonBoundaryBarrierPrefab: {
type: cc.Prefab,
default: null,
@ -85,10 +80,6 @@ cc.Class({
type: cc.Label,
default: null
},
trapBulletPrefab: {
type: cc.Prefab,
default: null
},
resultPanelPrefab: {
type: cc.Prefab,
default: null
@ -109,10 +100,6 @@ cc.Class({
type: cc.Prefab,
default: null
},
guardTowerPrefab: {
type: cc.Prefab,
default: null
},
forceBigEndianFloatingNumDecoding: {
default: false,
},
@ -120,61 +107,40 @@ cc.Class({
type: cc.TiledMap,
default: null
},
rollbackEstimatedDt: {
type: cc.Float,
default: 1.0/60
},
maxChasingRenderFramesPerUpdate: {
renderFrameIdLagTolerance: {
type: cc.Integer,
default: 10
default: 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
},
},
_inputFrameIdDebuggable(inputFrameId) {
return (0 == inputFrameId%10);
return (0 == inputFrameId % 10);
},
_dumpToRenderCache: function(roomDownsyncFrame) {
dumpToRenderCache: function(roomDownsyncFrame) {
const self = this;
const minToKeepRenderFrameId = self.lastAllConfirmedRenderFrameId;
while (0 < self.recentRenderCache.cnt && self.recentRenderCache.stFrameId < minToKeepRenderFrameId) {
self.recentRenderCache.pop();
}
if (self.recentRenderCache.stFrameId < minToKeepRenderFrameId) {
console.warn("Weird dumping of RENDER frame: self.renderFrame=", self.renderFrame, ", self.recentInputCache=", self._stringifyRecentInputCache(false), ", self.recentRenderCache=", self._stringifyRecentRenderCache(false), ", self.lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", self.lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId);
}
const existing = self.recentRenderCache.getByFrameId(roomDownsyncFrame.id);
if (null != existing) {
existing.players = roomDownsyncFrame.players;
existing.sentAt = roomDownsyncFrame.sentAt;
existing.countdownNanos = roomDownsyncFrame.countdownNanos;
existing.treasures = roomDownsyncFrame.treasures;
existing.bullets = roomDownsyncFrame.bullets;
existing.speedShoes = roomDownsyncFrame.speedShoes;
existing.guardTowers = roomDownsyncFrame.guardTowers;
existing.playerMetas = roomDownsyncFrame.playerMetas;
} else {
self.recentRenderCache.put(roomDownsyncFrame);
}
const ret = self.recentRenderCache.setByFrameId(roomDownsyncFrame, roomDownsyncFrame.id);
return ret;
},
_dumpToInputCache: function(inputFrameDownsync) {
dumpToInputCache: function(inputFrameDownsync) {
const self = this;
let minToKeepInputFrameId = self._convertToInputFrameId(self.lastAllConfirmedRenderFrameId, self.inputDelayFrames); // [WARNING] This could be different from "self.lastAllConfirmedInputFrameId". We'd like to keep the corresponding inputFrame for "self.lastAllConfirmedRenderFrameId" such that a rollback could place "self.chaserRenderFrameId = self.lastAllConfirmedRenderFrameId" for the worst case incorrect prediction.
if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) minToKeepInputFrameId = self.lastAllConfirmedInputFrameId;
if (minToKeepInputFrameId > self.lastAllConfirmedInputFrameId) {
minToKeepInputFrameId = self.lastAllConfirmedInputFrameId;
}
while (0 < self.recentInputCache.cnt && self.recentInputCache.stFrameId < minToKeepInputFrameId) {
self.recentInputCache.pop();
}
if (self.recentInputCache.stFrameId < minToKeepInputFrameId) {
console.warn("Weird dumping of INPUT frame: self.renderFrame=", self.renderFrame, ", self.recentInputCache=", self._stringifyRecentInputCache(false), ", self.recentRenderCache=", self._stringifyRecentRenderCache(false), ", self.lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", self.lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId);
}
const existing = self.recentInputCache.getByFrameId(inputFrameDownsync.inputFrameId);
if (null != existing) {
existing.inputList = inputFrameDownsync.inputList;
existing.confirmedList = inputFrameDownsync.confirmedList;
} else {
self.recentInputCache.put(inputFrameDownsync);
const ret = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId);
if (-1 < self.lastAllConfirmedInputFrameId && self.recentInputCache.stFrameId > self.lastAllConfirmedInputFrameId) {
console.error("Invalid input cache dumped! lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
}
return ret;
},
_convertToInputFrameId(renderFrameId, inputDelayFrames) {
@ -182,16 +148,16 @@ cc.Class({
return ((renderFrameId - inputDelayFrames) >> this.inputScaleFrames);
},
_convertToRenderFrameId(inputFrameId, inputDelayFrames) {
_convertToFirstUsedRenderFrameId(inputFrameId, inputDelayFrames) {
return ((inputFrameId << this.inputScaleFrames) + inputDelayFrames);
},
_shouldGenerateInputFrameUpsync(renderFrameId) {
return ((renderFrameId & ((1 << this.inputScaleFrames)-1)) == 0);
shouldGenerateInputFrameUpsync(renderFrameId) {
return ((renderFrameId & ((1 << this.inputScaleFrames) - 1)) == 0);
},
_allConfirmed(confirmedList) {
return (confirmedList+1) == (1 << this.playerRichInfoDict.size);
return (confirmedList + 1) == (1 << this.playerRichInfoDict.size);
},
_generateInputFrameUpsync(inputFrameId) {
@ -207,40 +173,48 @@ cc.Class({
const discreteDir = self.ctrl.getDiscretizedDirection();
const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId);
const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice());
prefabbedInputList[(joinIndex-1)] = discreteDir.encodedIdx;
const prefabbedInputFrameDownsync = {
prefabbedInputList[(joinIndex - 1)] = discreteDir.encodedIdx;
const prefabbedInputFrameDownsync = {
inputFrameId: inputFrameId,
inputList: prefabbedInputList,
confirmedList: (1 << (self.selfPlayerInfo.joinIndex-1))
confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1))
};
self._dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex-1]);
const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]);
return [previousSelfInput, discreteDir.encodedIdx];
},
shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, lastUpsyncInputFrameId, currInputFrameId) {
/*
For a 2-player-battle, this "shouldUpsyncForEarlyAllConfirmedOnServer" can be omitted, however for more players in a same battle, to avoid a "long time non-moving player" jamming the downsync of other moving players, we should use this flag.
For a 2-player-battle, this "shouldUpsyncForEarlyAllConfirmedOnBackend" can be omitted, however for more players in a same battle, to avoid a "long time non-moving player" jamming the downsync of other moving players, we should use this flag.
When backend implements the "force confirmation" feature, we can have "false == shouldUpsyncForEarlyAllConfirmedOnBackend" all the time as well!
*/
if (null == currSelfInput) return false;
const shouldUpsyncForEarlyAllConfirmedOnServer = (currInputFrameId - lastUpsyncInputFrameId >= this.inputFrameUpsyncDelayTolerance);
return shouldUpsyncForEarlyAllConfirmedOnServer || (prevSelfInput != currSelfInput);
const shouldUpsyncForEarlyAllConfirmedOnBackend = (currInputFrameId - lastUpsyncInputFrameId >= this.inputFrameUpsyncDelayTolerance);
return shouldUpsyncForEarlyAllConfirmedOnBackend || (prevSelfInput != currSelfInput);
},
sendInputFrameUpsyncBatch(inputFrameId) {
// [WARNING] Why not just send the latest input? Because different player would have a different "inputFrameId" of changing its last input, and that could make the server not recognizing any "all-confirmed inputFrame"!
sendInputFrameUpsyncBatch(latestLocalInputFrameId) {
// [WARNING] Why not just send the latest input? Because different player would have a different "latestLocalInputFrameId" of changing its last input, and that could make the server not recognizing any "all-confirmed inputFrame"!
const self = this;
let inputFrameUpsyncBatch = [];
for (let i = self.lastUpsyncInputFrameId+1; i <= inputFrameId; ++i) {
let batchInputFrameIdSt = self.lastUpsyncInputFrameId + 1;
if (batchInputFrameIdSt < self.recentInputCache.stFrameId) {
// Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly.
batchInputFrameIdSt = self.recentInputCache.stFrameId;
}
for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.getByFrameId(i);
if (null == inputFrameDownsync) {
console.warn("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, "; recentInputCache=", self._stringifyRecentInputCache(false));
console.error("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, ": latestLocalInputFrameId=", latestLocalInputFrameId, ", recentInputCache=", self._stringifyRecentInputCache(false));
} else {
const inputFrameUpsync = {
inputFrameId: i,
encodedDir: inputFrameDownsync.inputList[self.selfPlayerInfo.joinIndex-1],
encodedDir: inputFrameDownsync.inputList[self.selfPlayerInfo.joinIndex - 1],
};
inputFrameUpsyncBatch.push(inputFrameUpsync);
}
@ -255,7 +229,7 @@ cc.Class({
inputFrameUpsyncBatch: inputFrameUpsyncBatch,
}).finish();
window.sendSafely(reqData);
self.lastUpsyncInputFrameId = inputFrameId;
self.lastUpsyncInputFrameId = latestLocalInputFrameId;
},
onEnable() {
@ -272,12 +246,6 @@ cc.Class({
if (null == self.battleState || ALL_BATTLE_STATES.WAITING == self.battleState) {
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
}
if (null != window.handleRoomDownsyncFrame) {
window.handleRoomDownsyncFrame = null;
}
if (null != window.handleInputFrameDownsyncBatch) {
window.handleInputFrameDownsyncBatch = null;
}
if (null != window.handleBattleColliderInfo) {
window.handleBattleColliderInfo = null;
}
@ -344,12 +312,8 @@ cc.Class({
self.renderFrameId = 0; // After battle started
self.lastAllConfirmedRenderFrameId = -1;
self.lastAllConfirmedInputFrameId = -1;
self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "handleInputFrameDownsyncBatch"
self.inputDelayFrames = 8;
self.inputScaleFrames = 2;
self.lastUpsyncInputFrameId = -1;
self.inputFrameUpsyncDelayTolerance = 2;
self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch"
self.recentRenderCache = new RingBuffer(1024);
@ -384,10 +348,10 @@ cc.Class({
window.handleClientSessionCloseOrError = function() {
console.warn('+++++++ Common handleClientSessionCloseOrError()');
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) { //如果是游戏时间结束引起的断连
console.log("游戏结束引起的断连, 不需要回到登录页面");
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
console.log("Battled ended by settlement");
} else {
console.warn("意外断连,即将回到登录页面");
console.warn("Connection lost, going back to login page");
window.clearLocalStorageAndBackToLoginScene(true);
}
};
@ -414,9 +378,7 @@ cc.Class({
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
};
resultPanelScriptIns.onCloseDelegate = () => {
};
resultPanelScriptIns.onCloseDelegate = () => {};
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
self.gameRuleNode.width = self.canvasNode.width;
@ -447,10 +409,16 @@ cc.Class({
/** Init required prefab ended. */
self.clientUpsyncFps = 60;
window.handleBattleColliderInfo = function(parsedBattleColliderInfo) {
self.battleColliderInfo = parsedBattleColliderInfo;
self.inputDelayFrames = parsedBattleColliderInfo.inputDelayFrames;
self.inputScaleFrames = parsedBattleColliderInfo.inputScaleFrames;
self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance;
self.rollbackEstimatedDt = 1.0 / parsedBattleColliderInfo.serverFps;
self.rollbackEstimatedDtMillis = 1000.0 * self.rollbackEstimatedDt;
self.rollbackEstimatedDtToleranceMillis = self.rollbackEstimatedDtMillis / 1000.0;
self.maxChasingRenderFramesPerUpdate = parsedBattleColliderInfo.maxChasingRenderFramesPerUpdate;
const tiledMapIns = self.node.getComponent(cc.TiledMap);
const fullPathOfTmxFile = cc.js.formatStr("map/%s/map", parsedBattleColliderInfo.stageName);
@ -475,7 +443,7 @@ cc.Class({
tiledMapIns.tmxAsset = tmxAsset;
const newMapSize = tiledMapIns.getMapSize();
const newTileSize = tiledMapIns.getTileSize();
self.node.setContentSize(newMapSize.width*newTileSize.width, newMapSize.height*newTileSize.height);
self.node.setContentSize(newMapSize.width * newTileSize.width, newMapSize.height * newTileSize.height);
self.node.setPosition(cc.v2(0, 0));
/*
* Deliberately hiding "ImageLayer"s. This dirty fix is specific to "CocosCreator v2.2.1", where it got back the rendering capability of "ImageLayer of Tiled", yet made incorrectly. In this game our "markers of ImageLayers" are rendered by dedicated prefabs with associated colliders.
@ -490,11 +458,12 @@ cc.Class({
let barrierIdCounter = 0;
const boundaryObjs = tileCollisionManager.extractBoundaryObjects(self.node);
for (let boundaryObj of boundaryObjs.barriers) {
const x0 = boundaryObj[0].x, y0 = boundaryObj[0].y;
const x0 = boundaryObj[0].x,
y0 = boundaryObj[0].y;
let pts = [];
// TODO: Simplify this redundant coordinate conversion within "extractBoundaryObjects", but since this routine is only called once per battle, not urgent.
for (let i = 0; i < boundaryObj.length; ++i) {
pts.push([boundaryObj[i].x-x0, boundaryObj[i].y-y0]);
pts.push([boundaryObj[i].x - x0, boundaryObj[i].y - y0]);
}
const newBarrierLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
const newBarrierChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts);
@ -521,7 +490,7 @@ cc.Class({
self.backgroundMapTiledIns.tmxAsset = backgroundMapTmxAsset;
const newBackgroundMapSize = self.backgroundMapTiledIns.getMapSize();
const newBackgroundMapTileSize = self.backgroundMapTiledIns.getTileSize();
self.backgroundMapTiledIns.node.setContentSize(newBackgroundMapSize.width*newBackgroundMapTileSize.width, newBackgroundMapSize.height*newBackgroundMapTileSize.height);
self.backgroundMapTiledIns.node.setContentSize(newBackgroundMapSize.width * newBackgroundMapTileSize.width, newBackgroundMapSize.height * newBackgroundMapTileSize.height);
self.backgroundMapTiledIns.node.setPosition(cc.v2(0, 0));
const reqData = window.WsReq.encode({
@ -538,103 +507,6 @@ cc.Class({
self.hideGameRuleNode();
self.transitToState(ALL_MAP_STATES.WAITING);
self._inputControlEnabled = false;
let findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
window.handleRoomDownsyncFrame = function(rdf) {
if (ALL_BATTLE_STATES.WAITING != self.battleState
&& ALL_BATTLE_STATES.IN_BATTLE != self.battleState
&& ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) {
return;
}
const frameId = rdf.id;
// Right upon establishment of the "PersistentSessionClient", we should receive an initial signal "BattleColliderInfo" earlier than any "RoomDownsyncFrame" containing "PlayerMeta" data.
const refFrameId = rdf.refFrameId;
switch (refFrameId) {
case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.PLAYER_ADDED_AND_ACKED:
// Update the "finding player" GUI and show it if not previously present
if (!self.findingPlayerNode.parent) {
self.showPopupInCanvas(self.findingPlayerNode);
}
findingPlayerScriptIns.updatePlayersInfo(rdf.playerMetas);
return;
case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_READY_TO_START:
self.onBattleReadyToStart(rdf.playerMetas, false);
return;
case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START:
self.onBattleStarted(rdf);
return;
case window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.PLAYER_READDED_AND_ACKED:
// [WARNING] The "frameId" from server could be quite fast-forwarding, don't assign it in other cases.
self.renderFrameId = frameId;
self.lastAllConfirmedRenderFrameId = frameId;
self.onBattleReadyToStart(rdf.playerMetas, true);
self.onBattleStarted(rdf);
return;
}
// TODO: Inject a NetworkDoctor as introduced in https://app.yinxiang.com/shard/s61/nl/13267014/5c575124-01db-419b-9c02-ec81f78c6ddc/.
};
window.handleInputFrameDownsyncBatch = function(batch) {
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState
&& ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) {
return;
}
// console.log("Received inputFrameDownsyncBatch=", batch, ", now correspondingLastLocalInputFrame=", self.recentInputCache.getByFrameId(batch[batch.length-1].inputFrameId));
let firstPredictedYetIncorrectInputFrameId = null;
let firstPredictedYetIncorrectInputFrameJoinIndex = null;
for (let k in batch) {
const inputFrameDownsync = batch[k];
const inputFrameDownsyncId = inputFrameDownsync.inputFrameId;
const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId);
if (null == localInputFrame) {
console.warn("handleInputFrameDownsyncBatch: recentInputCache is NOT having inputFrameDownsyncId=", inputFrameDownsyncId, "; now recentInputCache=", self._stringifyRecentInputCache(false));
} else {
if (null == firstPredictedYetIncorrectInputFrameId) {
for (let i in localInputFrame.inputList) {
if (localInputFrame.inputList[i] != inputFrameDownsync.inputList[i]) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
firstPredictedYetIncorrectInputFrameJoinIndex = (parseInt(i)+1);
break;
}
}
}
}
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
self._dumpToInputCache(inputFrameDownsync);
}
if (null != firstPredictedYetIncorrectInputFrameId) {
const inputFrameId1 = firstPredictedYetIncorrectInputFrameId;
const renderFrameId1 = self._convertToRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId"
if (renderFrameId1 < self.renderFrameId) {
/*
A typical case is as follows.
--------------------------------------------------------
[self.lastAllConfirmedRenderFrameId] : 22
<renderFrameId1> : 36
<self.chaserRenderFrameId> : 62
[self.renderFrameId] : 64
--------------------------------------------------------
*/
if (renderFrameId1 < self.chaserRenderFrameId) {
// The actual rollback-and-chase would later be executed in update(dt).
console.warn("Mismatched input detected, resetting chaserRenderFrameId: inputFrameId1:", inputFrameId1, ", renderFrameId1:", renderFrameId1, ", chaserRenderFrameId before reset: ", self.chaserRenderFrameId);
self.chaserRenderFrameId = renderFrameId1;
} else {
// Deliberately left blank, chasing is ongoing.
}
} else {
// No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId2" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range.
}
}
};
}
// The player is now viewing "self.gameRuleNode" with button(s) to start an actual battle. -- YFLu
@ -652,10 +524,10 @@ cc.Class({
} else if (null != boundRoomId) {
self.disableGameRuleNode();
self.battleState = ALL_BATTLE_STATES.WAITING;
window.initPersistentSessionClient(self.initAfterWSConnected, expectedRoomId);
window.initPersistentSessionClient(self.initAfterWSConnected, boundRoomId);
} else {
self.showPopupInCanvas(self.gameRuleNode);
// Deliberately left blank. -- YFLu
// Deliberately left blank. -- YFLu
}
},
@ -689,10 +561,35 @@ cc.Class({
this._inputControlEnabled = false;
},
onBattleStarted(rdf) {
onRoomDownsyncFrame(rdf) {
// This function is also applicable to "re-joining".
console.log('On battle started!');
const self = window.mapIns;
if (rdf.id < self.lastAllConfirmedRenderFrameId) {
return window.RING_BUFF_FAILED_TO_SET;
}
const dumpRenderCacheRet = self.dumpToRenderCache(rdf);
if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) {
console.error("Something is wrong while setting the RingBuffer by frameId!");
return dumpRenderCacheRet;
}
if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START < rdf.id && window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet) {
/*
Don't change
- lastAllConfirmedRenderFrameId, it's updated only in "rollbackAndChase > _createRoomDownsyncFrameLocally" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
- chaserRenderFrameId, it's updated only in "onInputFrameDownsyncBatch" (except for when RING_BUFF_NON_CONSECUTIVE_SET)
*/
return dumpRenderCacheRet;
}
// The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
console.log('On battle started or resynced! renderFrameId=', rdf.id);
self.renderFrameId = rdf.id;
self.lastRenderFrameIdTriggeredAt = performance.now();
// In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId".
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id;
const players = rdf.players;
const playerMetas = rdf.playerMetas;
self._initPlayerRichInfoDict(players, playerMetas);
@ -717,19 +614,95 @@ cc.Class({
self.countdownToBeginGameNode.parent.removeChild(self.countdownToBeginGameNode);
}
self.transitToState(ALL_MAP_STATES.VISUAL);
self.chaserRenderFrameId = rdf.id;
self.battleState = ALL_BATTLE_STATES.IN_BATTLE;
self.applyRoomDownsyncFrameDynamics(rdf);
self._dumpToRenderCache(rdf);
self.battleState = ALL_BATTLE_STATES.IN_BATTLE; // Starts the increment of "self.renderFrameId" in "self.update(dt)"
if (null != window.boundRoomId) {
self.boundRoomIdLabel.string = window.boundRoomId;
return dumpRenderCacheRet;
},
equalInputLists(lhs, rhs) {
if (null == lhs || null == rhs) return false;
if (lhs.length != rhs.length) return false;
for (let i in lhs) {
if (lhs[i] == rhs[i]) continue;
return false;
}
return true;
},
onInputFrameDownsyncBatch(batch, dumpRenderCacheRet /* second param is default to null */ ) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState
&& ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) {
return;
}
let firstPredictedYetIncorrectInputFrameId = null;
for (let k in batch) {
const inputFrameDownsync = batch[k];
const inputFrameDownsyncId = inputFrameDownsync.inputFrameId;
if (inputFrameDownsyncId < self.lastAllConfirmedInputFrameId) {
continue;
}
if (window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) {
// Deliberately left blank, in this case "chaserRenderFrameId" is already reset to proper value.
} else {
const inputFrameIdConsecutive = (inputFrameDownsyncId == self.lastAllConfirmedInputFrameId + 1);
const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId);
if (null == localInputFrame && false == inputFrameIdConsecutive) {
throw "localInputFrame not existing and is NOT CONSECUTIVELY EXTENDING recentInputCache: inputFrameDownsyncId=" + inputFrameDownsyncId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId + ", recentInputCache=" + self._stringifyRecentInputCache(false);
} else if (null == firstPredictedYetIncorrectInputFrameId && null != localInputFrame && !self.equalInputLists(localInputFrame.inputList, inputFrameDownsync.inputList)) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId;
}
}
self.lastAllConfirmedInputFrameId = inputFrameDownsyncId;
self.dumpToInputCache(inputFrameDownsync);
}
if (null != firstPredictedYetIncorrectInputFrameId) {
const inputFrameId1 = firstPredictedYetIncorrectInputFrameId;
const renderFrameId1 = self._convertToFirstUsedRenderFrameId(inputFrameId1, self.inputDelayFrames); // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId"
if (renderFrameId1 < self.renderFrameId) {
/*
A typical case is as follows.
--------------------------------------------------------
[self.lastAllConfirmedRenderFrameId] : 22
<renderFrameId1> : 36
<self.chaserRenderFrameId> : 62
[self.renderFrameId] : 64
--------------------------------------------------------
*/
if (renderFrameId1 < self.chaserRenderFrameId) {
// The actual rollback-and-chase would later be executed in update(dt).
console.warn("Mismatched input detected, resetting chaserRenderFrameId: inputFrameId1:", inputFrameId1, ", renderFrameId1:", renderFrameId1, ", chaserRenderFrameId before reset: ", self.chaserRenderFrameId);
self.chaserRenderFrameId = renderFrameId1;
} else {
// Deliberately left blank, chasing is ongoing.
}
} else {
// No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId2" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range.
}
}
},
onPlayerAdded(rdf) {
const self = this;
// Update the "finding player" GUI and show it if not previously present
if (!self.findingPlayerNode.parent) {
self.showPopupInCanvas(self.findingPlayerNode);
}
let findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.updatePlayersInfo(rdf.playerMetas);
},
logBattleStats() {
const self = this;
let s = [];
s.push("Battle stats: lastUpsyncInputFrameId=" + self.lastUpsyncInputFrameId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId);
s.push("Battle stats: renderFrameId=" + self.renderFrameId + ", lastAllConfirmedRenderFrameId=" + self.lastAllConfirmedRenderFrameId + ", lastUpsyncInputFrameId=" + self.lastUpsyncInputFrameId + ", lastAllConfirmedInputFrameId=" + self.lastAllConfirmedInputFrameId);
for (let i = self.recentInputCache.stFrameId; i < self.recentInputCache.edFrameId; ++i) {
const inputFrameDownsync = self.recentInputCache.getByFrameId(i);
@ -741,6 +714,9 @@ cc.Class({
onBattleStopped() {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState) {
return;
}
self.countdownNanos = null;
self.logBattleStats();
if (self.musicEffectManagerScriptIns) {
@ -776,7 +752,10 @@ cc.Class({
newPlayerNode.active = true;
const playerScriptIns = newPlayerNode.getComponent("SelfPlayer");
playerScriptIns.scheduleNewDirection({dx: 0, dy: 0}, true);
playerScriptIns.scheduleNewDirection({
dx: 0,
dy: 0
}, true);
return [newPlayerNode, playerScriptIns];
},
@ -784,46 +763,49 @@ cc.Class({
update(dt) {
const self = this;
if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) {
const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt;
if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) {
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
return;
}
try {
let prevSelfInput = null, currSelfInput = null;
const noDelayInputFrameId = self._convertToInputFrameId(self.renderFrameId, 0); // It's important that "inputDelayFrames == 0" here
if (self._shouldGenerateInputFrameUpsync(self.renderFrameId)) {
const prevAndCurrInputs = self._generateInputFrameUpsync(noDelayInputFrameId);
prevSelfInput = prevAndCurrInputs[0];
currSelfInput = prevAndCurrInputs[1];
}
let st = performance.now();
let prevSelfInput = null,
currSelfInput = null;
const noDelayInputFrameId = self._convertToInputFrameId(self.renderFrameId, 0); // It's important that "inputDelayFrames == 0" here
if (self.shouldGenerateInputFrameUpsync(self.renderFrameId)) {
const prevAndCurrInputs = self._generateInputFrameUpsync(noDelayInputFrameId);
prevSelfInput = prevAndCurrInputs[0];
currSelfInput = prevAndCurrInputs[1];
}
let t0 = performance.now();
if (self.shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, self.lastUpsyncInputFrameId, noDelayInputFrameId)) {
// TODO: Is the following statement run asynchronously in an implicit manner? Should I explicitly run it asynchronously?
self.sendInputFrameUpsyncBatch(noDelayInputFrameId);
}
let t0 = performance.now();
if (self.shouldSendInputFrameUpsyncBatch(prevSelfInput, currSelfInput, self.lastUpsyncInputFrameId, noDelayInputFrameId)) {
// TODO: Is the following statement run asynchronously in an implicit manner? Should I explicitly run it asynchronously?
self.sendInputFrameUpsyncBatch(noDelayInputFrameId);
}
let t1 = performance.now();
// Use "fractional-frame-chasing" to guarantee that "self.update(dt)" is not jammed by a "large range of frame-chasing". See `<proj-root>/ConcerningEdgeCases.md` for the motivation.
const prevChaserRenderFrameId = self.chaserRenderFrameId;
let nextChaserRenderFrameId = (prevChaserRenderFrameId + self.maxChasingRenderFramesPerUpdate);
if (nextChaserRenderFrameId > self.renderFrameId) nextChaserRenderFrameId = self.renderFrameId;
self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap);
self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
let t2 = performance.now();
let t1 = performance.now();
// Use "fractional-frame-chasing" to guarantee that "self.update(dt)" is not jammed by a "large range of frame-chasing". See `<proj-root>/ConcerningEdgeCases.md` for the motivation.
const prevChaserRenderFrameId = self.chaserRenderFrameId;
let nextChaserRenderFrameId = (prevChaserRenderFrameId + self.maxChasingRenderFramesPerUpdate);
if (nextChaserRenderFrameId > self.renderFrameId) {
nextChaserRenderFrameId = self.renderFrameId;
}
self.rollbackAndChase(prevChaserRenderFrameId, nextChaserRenderFrameId, self.chaserCollisionSys, self.chaserCollisionSysMap);
self.chaserRenderFrameId = nextChaserRenderFrameId; // Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
let t2 = performance.now();
// Inside "self.rollbackAndChase", the "self.latestCollisionSys" is ALWAYS ROLLED BACK to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding inputFrameDownsync, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId+1, self.latestCollisionSys, self.latestCollisionSysMap);
self.applyRoomDownsyncFrameDynamics(rdf);
let t3 = performance.now();
/*
if (prevChaserRenderFrameId < nextChaserRenderFrameId) {
console.log("Took ", t1-t0, " milliseconds to send upsync cmds, ", t2-t1, " milliseconds to chase renderFrameIds=[", prevChaserRenderFrameId, ", ", nextChaserRenderFrameId, "], @renderFrameId=", self.renderFrameId);
}
*/
// Inside "self.rollbackAndChase", the "self.latestCollisionSys" is ALWAYS ROLLED BACK to "self.recentRenderCache.get(self.renderFrameId)" before being applied dynamics from corresponding inputFrameDownsync, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
const rdf = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.latestCollisionSys, self.latestCollisionSysMap);
self.applyRoomDownsyncFrameDynamics(rdf);
let t3 = performance.now();
} catch (err) {
console.error("Error during Map.update", err);
} finally {
// Update countdown
if (null != self.countdownNanos) {
self.countdownNanos -= self.rollbackEstimatedDt*1000000000;
self.countdownNanos -= (performance.now() - self.lastRenderFrameIdTriggeredAt) * 1000000;
if (self.countdownNanos <= 0) {
self.onBattleStopped(self.playerRichInfoDict);
return;
@ -836,6 +818,7 @@ cc.Class({
self.countdownLabel.string = countdownSeconds;
}
++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self.lastRenderFrameIdTriggeredAt = performance.now();
}
}
},
@ -861,7 +844,9 @@ cc.Class({
NetworkUtils.ajax({
url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.INT_AUTH_TOKEN + constants.ROUTE_PATH.LOGOUT,
type: "POST",
data: { intAuthToken: selfPlayerInfo.intAuthToken },
data: {
intAuthToken: selfPlayerInfo.intAuthToken
},
success: function(res) {
if (res.ret != constants.RET_CODE.OK) {
console.log("Logout failed: ", res);
@ -909,36 +894,33 @@ cc.Class({
setLocalZOrder(toShowNode, 10);
},
onBattleReadyToStart(playerMetas, isSelfRejoining) {
hideFindingPlayersGUI() {
const self = this;
if (null == self.findingPlayerNode.parent) return;
self.findingPlayerNode.parent.removeChild(self.findingPlayerNode);
},
onBattleReadyToStart(playerMetas) {
console.log("Calling `onBattleReadyToStart` with:", playerMetas);
const self = this;
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.hideExitButton();
findingPlayerScriptIns.updatePlayersInfo(playerMetas);
const hideFindingPlayersGUI = function() {
if (null == self.findingPlayerNode.parent) return;
self.findingPlayerNode.parent.removeChild(self.findingPlayerNode);
};
if (true == isSelfRejoining) {
hideFindingPlayersGUI();
} else {
// Delay to hide the "finding player" GUI, then show a countdown clock
window.setTimeout(() => {
hideFindingPlayersGUI();
const countDownScriptIns = self.countdownToBeginGameNode.getComponent("CountdownToBeginGame");
countDownScriptIns.setData();
self.showPopupInCanvas(self.countdownToBeginGameNode);
}, 1500);
}
// Delay to hide the "finding player" GUI, then show a countdown clock
window.setTimeout(() => {
self.hideFindingPlayersGUI();
const countDownScriptIns = self.countdownToBeginGameNode.getComponent("CountdownToBeginGame");
countDownScriptIns.setData();
self.showPopupInCanvas(self.countdownToBeginGameNode);
}, 1500);
},
_createRoomDownsyncFrameLocally(renderFrameId, collisionSys, collisionSysMap) {
const self = this;
const prevRenderFrameId = renderFrameId-1;
const inputFrameForPrevRenderFrame = (
0 > prevRenderFrameId
const prevRenderFrameId = renderFrameId - 1;
const inputFrameAppliedOnPrevRenderFrame = (
0 > prevRenderFrameId
?
null
:
@ -948,11 +930,11 @@ cc.Class({
// TODO: Find a better way to assign speeds instead of using "speedRefRenderFrameId".
const speedRefRenderFrameId = prevRenderFrameId;
const speedRefRenderFrame = (
0 > prevRenderFrameId
0 > speedRefRenderFrameId
?
null
:
self.recentRenderCache.getByFrameId(prevRenderFrameId)
self.recentRenderCache.getByFrameId(speedRefRenderFrameId)
);
const rdf = {
@ -968,21 +950,22 @@ cc.Class({
id: playerRichInfo.id,
x: playerCollider.x,
y: playerCollider.y,
dir: self.ctrl.decodeDirection(null == inputFrameForPrevRenderFrame ? 0 : inputFrameForPrevRenderFrame.inputList[joinIndex-1]),
dir: self.ctrl.decodeDirection(null == inputFrameAppliedOnPrevRenderFrame ? 0 : inputFrameAppliedOnPrevRenderFrame.inputList[joinIndex - 1]),
speed: (null == speedRefRenderFrame ? playerRichInfo.speed : speedRefRenderFrame.players[playerRichInfo.id].speed),
joinIndex: joinIndex
};
});
if (
null != inputFrameForPrevRenderFrame && self._allConfirmed(inputFrameForPrevRenderFrame.confirmedList)
null != inputFrameAppliedOnPrevRenderFrame && self._allConfirmed(inputFrameAppliedOnPrevRenderFrame.confirmedList)
&&
self.lastAllConfirmedRenderFrameId >= prevRenderFrameId
&&
rdf.id > self.lastAllConfirmedRenderFrameId
) {
self.lastAllConfirmedRenderFrameId = rdf.id;
self.chaserRenderFrameId = rdf.id; // it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId"
}
self._dumpToRenderCache(rdf);
self.dumpToRenderCache(rdf);
return rdf;
},
@ -1003,7 +986,7 @@ cc.Class({
if (null != inputFrameDownsync && -1 != self.lastAllConfirmedInputFrameId && inputFrameId > self.lastAllConfirmedInputFrameId) {
const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId);
for (let i = 0; i < inputFrameDownsync.inputList.length; ++i) {
if (i == self.selfPlayerInfo.joinIndex-1) continue;
if (i == self.selfPlayerInfo.joinIndex - 1) continue;
inputFrameDownsync.inputList[i] = lastAllConfirmedInputFrame.inputList[i];
}
}
@ -1012,23 +995,23 @@ cc.Class({
},
rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap) {
if (renderFrameSt >= renderFrameIdEd) {
return;
const self = this;
let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame"
if (null == latestRdf) {
console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
}
const self = this;
const renderFrameSt = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame"
if (null == renderFrameSt) {
console.error("Couldn't find renderFrameId=", renderFrameIdSt, " to rollback, recentRenderCache=", self._stringifyRecentRenderCache(false));
if (renderFrameIdSt >= renderFrameIdEd) {
return latestRdf;
}
/*
Reset "position" of players in "collisionSys" according to "renderFrameSt". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics.
Reset "position" of players in "collisionSys" according to "renderFrameIdSt". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics.
*/
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
const joinIndex = playerRichInfo.joinIndex;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = renderFrameSt.players[playerId];
const player = latestRdf.players[playerId];
playerCollider.x = player.x;
playerCollider.y = player.y;
});
@ -1039,28 +1022,28 @@ cc.Class({
for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) {
const renderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"
const j = self._convertToInputFrameId(i, self.inputDelayFrames);
const inputList = self.getCachedInputFrameDownsyncWithPrediction(j).inputList;
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
const joinIndex = playerRichInfo.joinIndex;
const inputFrameDownsync = self.getCachedInputFrameDownsyncWithPrediction(j);
if (null == inputFrameDownsync) {
console.error("Failed to get cached inputFrameDownsync for renderFrameId=", i, ", inputFrameId=", j, "lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false));
}
const inputList = inputFrameDownsync.inputList;
// [WARNING] Traverse in the order of joinIndices to guarantee determinism.
for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1;
const playerId = self.playerRichInfoArr[j].id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = renderFrame.players[playerId];
const encodedInput = inputList[joinIndex-1];
const encodedInput = inputList[joinIndex - 1];
const decodedInput = self.ctrl.decodeDirection(encodedInput);
const baseChange = player.speed*self.rollbackEstimatedDt*decodedInput.speedFactor;
playerCollider.x += baseChange*decodedInput.dx;
playerCollider.y += baseChange*decodedInput.dy;
/*
if (0 < encodedInput) {
console.log("playerId=", playerId, "@renderFrameId=", i, ", delayedInputFrameId=", j, ", baseChange=", baseChange, ": x=", playerCollider.x, ", y=", playerCollider.y);
}
*/
});
const baseChange = player.speed * self.rollbackEstimatedDt * decodedInput.speedFactor;
playerCollider.x += baseChange * decodedInput.dx;
playerCollider.y += baseChange * decodedInput.dy;
}
collisionSys.update();
const result = collisionSys.createResult(); // Can I reuse a "self.latestCollisionSysResult" object throughout the whole battle?
// [WARNING] Traverse in the order of joinIndices to guarantee determinism.
for (let i in self.playerRichInfoArr) {
const joinIndex = parseInt(i) + 1;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
@ -1074,9 +1057,11 @@ cc.Class({
playerCollider.y -= result.overlap * result.overlap_y;
}
}
latestRdf = self._createRoomDownsyncFrameLocally(i + 1, collisionSys, collisionSysMap);
}
return self._createRoomDownsyncFrameLocally(renderFrameIdEd, collisionSys, collisionSysMap);
return latestRdf;
},
_initPlayerRichInfoDict(players, playerMetas) {
@ -1101,7 +1086,7 @@ cc.Class({
}
self.playerRichInfoArr = new Array(self.playerRichInfoDict.size);
self.playerRichInfoDict.forEach((playerRichInfo, playerId) => {
self.playerRichInfoArr[playerRichInfo.joinIndex-1] = playerRichInfo;
self.playerRichInfoArr[playerRichInfo.joinIndex - 1] = playerRichInfo;
});
},

View File

@ -1,3 +1,7 @@
window.RING_BUFF_CONSECUTIVE_SET = 0;
window.RING_BUFF_NON_CONSECUTIVE_SET = 1;
window.RING_BUFF_FAILED_TO_SET = 2;
var RingBuffer = function(capacity) {
this.ed = 0; // write index, open index
this.st = 0; // read index, closed index
@ -32,15 +36,15 @@ RingBuffer.prototype.pop = function() {
return item;
};
RingBuffer.prototype.getByOffset = function(offsetFromSt) {
if (0 == this.cnt) {
RingBuffer.prototype.getArrIdxByOffset = function(offsetFromSt) {
if (0 > offsetFromSt || 0 == this.cnt) {
return null;
}
let arrIdx = this.st + offsetFromSt;
if (this.st < this.ed) {
// case#1: 0...st...ed...n-1
if (this.st <= arrIdx && arrIdx < this.ed) {
return this.eles[arrIdx];
return arrIdx;
}
} else {
// if this.st >= this.sd
@ -49,7 +53,7 @@ RingBuffer.prototype.getByOffset = function(offsetFromSt) {
arrIdx -= this.n
}
if (arrIdx >= this.st || arrIdx < this.ed) {
return this.eles[arrIdx];
return arrIdx;
}
}
@ -57,7 +61,40 @@ RingBuffer.prototype.getByOffset = function(offsetFromSt) {
};
RingBuffer.prototype.getByFrameId = function(frameId) {
return this.getByOffset(frameId - this.stFrameId);
const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
return (null == arrIdx ? null : this.eles[arrIdx]);
};
// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly.
RingBuffer.prototype.setByFrameId = function(item, frameId) {
if (frameId < this.stFrameId) {
console.error("Invalid putByFrameId#1: stFrameId=", this.stFrameId, ", edFrameId=", this.edFrameId, ", incoming item=", item);
return window.RING_BUFF_FAILED_TO_SET;
}
const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
if (null != arrIdx) {
this.eles[arrIdx] = item;
return window.RING_BUFF_CONSECUTIVE_SET;
}
// When "null == arrIdx", should it still be deemed consecutive if "frameId == edFrameId" prior to the reset?
let ret = window.RING_BUFF_CONSECUTIVE_SET;
if (this.edFrameId < frameId) {
this.st = this.ed = 0;
this.stFrameId = this.edFrameId = frameId;
this.cnt = 0;
ret = window.RING_BUFF_NON_CONSECUTIVE_SET;
}
this.eles[this.ed] = item
this.edFrameId++;
this.cnt++;
this.ed++;
if (this.ed >= this.n) {
this.ed -= this.n; // Deliberately not using "%" operator for performance concern
}
return ret;
};
module.exports = RingBuffer;

View File

@ -1,10 +1,18 @@
const RingBuffer = require('./RingBuffer');
window.UPSYNC_MSG_ACT_HB_PING = 1;
window.UPSYNC_MSG_ACT_PLAYER_CMD = 2;
window.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = 3;
window.DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED = -98;
window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED = -97;
window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START = -1;
window.DOWNSYNC_MSG_ACT_BATTLE_START = 0;
window.DOWNSYNC_MSG_ACT_HB_REQ = 1;
window.DOWNSYNC_MSG_ACT_INPUT_BATCH = 2;
window.DOWNSYNC_MSG_ACT_ROOM_FRAME = 3;
window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED = 3;
window.DOWNSYNC_MSG_ACT_FORCED_RESYNC = 4;
window.sendSafely = function(msgStr) {
/**
@ -153,14 +161,41 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
case window.DOWNSYNC_MSG_ACT_HB_REQ:
window.handleHbRequirements(resp); // 获取boundRoomId并存储到localStorage
break;
case window.DOWNSYNC_MSG_ACT_ROOM_FRAME:
if (window.handleRoomDownsyncFrame) {
window.handleRoomDownsyncFrame(resp.rdf);
}
case window.DOWNSYNC_MSG_ACT_PLAYER_ADDED_AND_ACKED:
mapIns.onPlayerAdded(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_PLAYER_READDED_AND_ACKED:
// Deliberately left blank for now
mapIns.hideFindingPlayersGUI();
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_READY_TO_START:
mapIns.onBattleReadyToStart(resp.rdf.playerMetas);
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_START:
mapIns.onRoomDownsyncFrame(resp.rdf);
break;
case window.DOWNSYNC_MSG_ACT_BATTLE_STOPPED:
mapIns.onBattleStopped();
break;
case window.DOWNSYNC_MSG_ACT_INPUT_BATCH:
if (window.handleInputFrameDownsyncBatch) {
window.handleInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
break;
case window.DOWNSYNC_MSG_ACT_FORCED_RESYNC:
if (null == resp.inputFrameDownsyncBatch || 0 >= resp.inputFrameDownsyncBatch.length) {
console.error("Got empty inputFrameDownsyncBatch upon resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp, null, 2));
return;
}
// Unless upon ws session lost and reconnected, it's maintained true that "inputFrameDownsyncBatch[0].inputFrameId == frontend.lastAllConfirmedInputFrameId+1", and in this case we should try to keep frontend moving only by "frontend.recentInputCache" to avoid jiggling of synced positions
const inputFrameIdConsecutive = (resp.inputFrameDownsyncBatch[0].inputFrameId == mapIns.lastAllConfirmedInputFrameId + 1);
const renderFrameIdConsecutive = (resp.rdf.id <= mapIns.renderFrameId + mapIns.renderFrameIdLagTolerance);
if (inputFrameIdConsecutive && renderFrameIdConsecutive) {
console.log("Got consecutive resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp));
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
} else {
console.warn("Got forced resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", the incoming resp=\n", JSON.stringify(resp, null, 2));
// The following order of execution is important
const dumpRenderCacheRet = mapIns.onRoomDownsyncFrame(resp.rdf);
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch, dumpRenderCacheRet);
}
break;
default: