mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-16 12:09:03 +00:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1a3b3a0a7a | ||
|
4f1ce0d71a | ||
|
1f728071a9 | ||
|
4b68917337 | ||
|
0cbf968228 | ||
|
ec2a21dbe7 | ||
|
dc6402c2b7 | ||
|
8038b393e0 | ||
|
4e0f7b52d4 | ||
|
486c46f608 | ||
|
6d075877ec | ||
|
fe826b393b | ||
|
c69aa25353 | ||
|
0f4d067c06 | ||
|
cff31d295c | ||
|
150e30db2a | ||
|
bc8989a0e6 | ||
|
1959a7fd9a | ||
|
3baaf1d52c | ||
|
62f10e0877 |
26
README.md
26
README.md
@@ -1,11 +1,25 @@
|
||||
# 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 input synchronization method inspired by [GGPO](https://www.ggpo.net/).
|
||||
|
||||
Please checkout [this demo video](https://pan.baidu.com/s/1aM6e8IWaJszFCYAsRjt19g?pwd=z02c) to see whether the source codes are doing what you expect for synchronization.
|
||||
_(how input delay roughly works)_
|
||||
|
||||
The video mainly shows the following feature (yet I'm not surprised if they're not obvious): when a player didn't have its input arrived at the backend in time (e.g. due to local lag, network delay or reconnection), backend forces confirmation of a prediction of its own and sends the confirmed input together w/ a reference render frame to that player.
|
||||

|
||||
|
||||
_(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
|
||||
|
||||
@@ -19,7 +33,7 @@ The video mainly shows the following feature (yet I'm not surprised if they're n
|
||||
- [protobuf CLI](https://developers.google.com/protocol-buffers/docs/downloads) (optional, only for development)
|
||||
|
||||
### Frontend
|
||||
- [CocosCreator v2.2.1](https://www.cocos.com/en/cocos-creator-2-2-1-released-with-performance-improvements) (mandatory, **ONLY AVAILABLE on Windows or OSX and should be exactly this version**, DON'T use any other version because CocosCreator is well-known for new versions not being backward incompatible)
|
||||
- [CocosCreator v2.2.1](https://www.cocos.com/en/cocos-creator-2-2-1-released-with-performance-improvements) (mandatory, **ONLY AVAILABLE on Windows or OSX and should be exactly this version**, DON'T use any other version because CocosCreator is well-known for new versions not being backward compatible)
|
||||
- [protojs](https://www.npmjs.com/package/protojs) (optional, only for development)
|
||||
|
||||
## 1.2 Provisioning
|
||||
@@ -64,7 +78,7 @@ The easy way is to try out 2 players with test accounts on a same machine.
|
||||
- Open one browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `add`on the username box and click to request a captcha, this is a test account so a captcha would be returned by the backend and filled automatically (as shown in the figure below), then click and click to proceed to a matching scene.
|
||||
- Open another browser instance, visit _http://localhost:7456?expectedRoomId=1_, input `bdd`on the username box and click to request a captcha, this is another test account so a captcha would be returned by the backend and filled automatically, then click and click to proceed, when matched a `battle`(but no competition rule yet) would start.
|
||||
- Try out the onscreen virtual joysticks to move the cars and see if their movements are in-sync.
|
||||

|
||||

|
||||
|
||||
## 2 Troubleshooting
|
||||
|
||||
|
@@ -2,13 +2,17 @@ package models
|
||||
|
||||
import (
|
||||
. "dnmshared"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/solarlune/resolv"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
pb "server/pb_output"
|
||||
@@ -16,11 +20,6 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"encoding/xml"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -189,6 +188,7 @@ type Room struct {
|
||||
StageTileH int32
|
||||
RawBattleStrToVec2DListMap StrToVec2DListMap
|
||||
RawBattleStrToPolygon2DListMap StrToPolygon2DListMap
|
||||
BackendDynamicsEnabled bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -329,7 +329,7 @@ func (pR *Room) ChooseStage() error {
|
||||
|
||||
var barrierLocalIdInBattle int32 = 0
|
||||
for _, polygon2DUnaligned := range barrierPolygon2DList {
|
||||
polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned)
|
||||
polygon2D := AlignPolygon2DToBoundingBox(polygon2DUnaligned)
|
||||
/*
|
||||
// For debug-printing only.
|
||||
Logger.Info("ChooseStage printing polygon2D for barrierPolygon2DList", zap.Any("barrierLocalIdInBattle", barrierLocalIdInBattle), zap.Any("polygon2D.Anchor", polygon2D.Anchor), zap.Any("polygon2D.Points", polygon2D.Points))
|
||||
@@ -459,17 +459,12 @@ func (pR *Room) StartBattle() {
|
||||
pR.prefabInputFrameDownsync(noDelayInputFrameId)
|
||||
}
|
||||
|
||||
// Force setting all-confirmed of buffered inputFrames periodically
|
||||
unconfirmedMask := pR.forceConfirmationIfApplicable()
|
||||
|
||||
dynamicsDuration := int64(0)
|
||||
if 0 <= pR.LastAllConfirmedInputFrameId {
|
||||
dynamicsStartedAt := utils.UnixtimeNano()
|
||||
// Apply "all-confirmed inputFrames" to move forward "pR.CurDynamicsRenderFrameId"
|
||||
nextDynamicsRenderFrameId := pR.ConvertToLastUsedRenderFrameId(pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames)
|
||||
Logger.Debug(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, LastAllConfirmedInputFrameId=%v, InputDelayFrames=%v, nextDynamicsRenderFrameId=%v", pR.Id, pR.RenderFrameId, pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames, nextDynamicsRenderFrameId))
|
||||
pR.applyInputFrameDownsyncDynamics(pR.CurDynamicsRenderFrameId, nextDynamicsRenderFrameId)
|
||||
dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
|
||||
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))
|
||||
@@ -479,15 +474,28 @@ func (pR *Room) StartBattle() {
|
||||
|
||||
If "NstDelayFrames" becomes larger, "pR.RenderFrameId - refRenderFrameId" possibly becomes larger because the force confirmation is delayed more.
|
||||
|
||||
Hence even upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId".
|
||||
Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed.
|
||||
*/
|
||||
refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId) + (1 << pR.InputScaleFrames) - 1
|
||||
// [WARNING] The following inequalities are seldom true, but just to avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId".
|
||||
if refRenderFrameId > pR.RenderFrameId {
|
||||
refRenderFrameId = pR.RenderFrameId
|
||||
}
|
||||
if refRenderFrameId > pR.CurDynamicsRenderFrameId {
|
||||
refRenderFrameId = pR.CurDynamicsRenderFrameId
|
||||
|
||||
dynamicsDuration := int64(0)
|
||||
if pR.BackendDynamicsEnabled {
|
||||
if 0 <= pR.LastAllConfirmedInputFrameId {
|
||||
dynamicsStartedAt := utils.UnixtimeNano()
|
||||
// Apply "all-confirmed inputFrames" to move forward "pR.CurDynamicsRenderFrameId"
|
||||
nextDynamicsRenderFrameId := pR.ConvertToLastUsedRenderFrameId(pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames)
|
||||
Logger.Debug(fmt.Sprintf("roomId=%v, room.RenderFrameId=%v, LastAllConfirmedInputFrameId=%v, InputDelayFrames=%v, nextDynamicsRenderFrameId=%v", pR.Id, pR.RenderFrameId, pR.LastAllConfirmedInputFrameId, pR.InputDelayFrames, nextDynamicsRenderFrameId))
|
||||
pR.applyInputFrameDownsyncDynamics(pR.CurDynamicsRenderFrameId, nextDynamicsRenderFrameId)
|
||||
dynamicsDuration = utils.UnixtimeNano() - dynamicsStartedAt
|
||||
}
|
||||
|
||||
// [WARNING] The following inequality are seldom true, but just to avoid that in good network condition the frontend resyncs itself to a "too advanced frontend.renderFrameId", and then starts upsyncing "too advanced inputFrameId".
|
||||
if refRenderFrameId > pR.CurDynamicsRenderFrameId {
|
||||
refRenderFrameId = pR.CurDynamicsRenderFrameId
|
||||
}
|
||||
}
|
||||
for playerId, player := range pR.Players {
|
||||
if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped {
|
||||
@@ -531,14 +539,14 @@ func (pR *Room) StartBattle() {
|
||||
// [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"!
|
||||
|
||||
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId {
|
||||
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, candidateToSendInputFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, candidateToSendInputFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
|
||||
Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1)
|
||||
var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr)
|
||||
if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || 0 < (unconfirmedMask&joinMask) {
|
||||
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)"!
|
||||
tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId)
|
||||
if nil == tmp {
|
||||
@@ -559,6 +567,17 @@ func (pR *Room) StartBattle() {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
toApplyInputFrameId = minLastSentInputFrameId
|
||||
}
|
||||
for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) {
|
||||
f := pR.InputsBuffer.Pop().(*pb.InputFrameDownsync)
|
||||
if pR.inputFrameIdDebuggable(f.InputFrameId) {
|
||||
@@ -802,6 +821,8 @@ func (pR *Room) OnDismissed() {
|
||||
pR.InputFrameUpsyncDelayTolerance = 2
|
||||
pR.MaxChasingRenderFramesPerUpdate = 10
|
||||
|
||||
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
|
||||
|
||||
pR.ChooseStage()
|
||||
pR.EffectivePlayerCount = 0
|
||||
|
||||
@@ -1069,6 +1090,43 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *pb.InputFrameDowns
|
||||
return currInputFrameDownsync
|
||||
}
|
||||
|
||||
func (pR *Room) markConfirmationIfApplicable() {
|
||||
inputFrameId1 := pR.LastAllConfirmedInputFrameId + 1
|
||||
gap := int32(4) // This value is hardcoded and doesn't need be much bigger, because the backend side is supposed to never lag when "false == BackendDynamicsEnabled".
|
||||
inputFrameId2 := inputFrameId1 + gap
|
||||
if inputFrameId2 > pR.InputsBuffer.EdFrameId {
|
||||
inputFrameId2 = pR.InputsBuffer.EdFrameId
|
||||
}
|
||||
|
||||
totPlayerCnt := uint32(pR.Capacity)
|
||||
allConfirmedMask := uint64((1 << totPlayerCnt) - 1)
|
||||
for inputFrameId := inputFrameId1; inputFrameId < inputFrameId2; inputFrameId++ {
|
||||
tmp := pR.InputsBuffer.GetByFrameId(inputFrameId)
|
||||
if nil == tmp {
|
||||
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", inputFrameId, pR.Id, pR.InputsBufferString(false)))
|
||||
}
|
||||
inputFrameDownsync := tmp.(*pb.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)
|
||||
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 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pR *Room) forceConfirmationIfApplicable() uint64 {
|
||||
// Force confirmation of non-all-confirmed inputFrame EXACTLY ONE AT A TIME, returns the non-confirmed mask of players, e.g. in a 4-player-battle returning 1001 means that players with JoinIndex=1 and JoinIndex=4 are non-confirmed for inputFrameId2
|
||||
renderFrameId1 := (pR.RenderFrameId - pR.NstDelayFrames) // the renderFrameId which should've been rendered on frontend
|
||||
@@ -1151,19 +1209,23 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
|
||||
continue
|
||||
}
|
||||
baseChange := player.Speed * pR.RollbackEstimatedDt * decodedInputSpeedFactor
|
||||
dx := baseChange * float64(decodedInput[0])
|
||||
dy := baseChange * float64(decodedInput[1])
|
||||
oldDx, oldDy := baseChange*float64(decodedInput[0]), baseChange*float64(decodedInput[1])
|
||||
dx, dy := oldDx, oldDy
|
||||
|
||||
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
||||
playerCollider := pR.CollisionSysMap[collisionPlayerIndex]
|
||||
if collision := playerCollider.Check(dx, dy, "Barrier"); collision != nil {
|
||||
changeWithCollision := collision.ContactWithObject(collision.Objects[0])
|
||||
Logger.Info(fmt.Sprintf("Collided: roomId=%v, playerId=%v, orig dx=%v, orig dy=%v, proposed new dx =%v, proposed new dy=%v", pR.Id, player.Id, dx, dy, changeWithCollision.X(), changeWithCollision.Y()))
|
||||
// FIXME: Use a mechanism equivalent to that of the frontend!
|
||||
// dx = changeWithCollision.X()
|
||||
// dy = changeWithCollision.Y()
|
||||
dx = 0
|
||||
dy = 0
|
||||
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
|
||||
@@ -1200,12 +1262,10 @@ func (pR *Room) refreshColliders() {
|
||||
spaceOffsetX := float64(spaceW) * 0.5
|
||||
spaceOffsetY := float64(spaceH) * 0.5
|
||||
|
||||
minStep := int(3) // the approx minimum distance a player can move per frame
|
||||
minStep := int(3) // the approx minimum distance a player can move per frame
|
||||
space := resolv.NewSpace(int(spaceW), int(spaceH), minStep, minStep) // allocate a new collision space everytime after a battle is settled
|
||||
for _, player := range pR.Players {
|
||||
playerCollider := resolv.NewObject(player.X+spaceOffsetX, player.Y+spaceOffsetY, playerColliderRadius*2, playerColliderRadius*2)
|
||||
playerColliderShape := resolv.NewCircle(0, 0, playerColliderRadius*2)
|
||||
playerCollider.SetShape(playerColliderShape)
|
||||
playerCollider := GenerateRectCollider(player.X, player.Y, playerColliderRadius*2, playerColliderRadius*2, spaceOffsetX, spaceOffsetY, "Player")
|
||||
space.Add(playerCollider)
|
||||
// Keep track of the collider in "pR.CollisionSysMap"
|
||||
joinIndex := player.JoinIndex
|
||||
@@ -1215,31 +1275,8 @@ func (pR *Room) refreshColliders() {
|
||||
}
|
||||
|
||||
for _, barrier := range pR.Barriers {
|
||||
|
||||
var w float64 = 0
|
||||
var h float64 = 0
|
||||
|
||||
for i, pi := range barrier.Boundary.Points {
|
||||
for j, pj := range barrier.Boundary.Points {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if math.Abs(pj.X-pi.X) > w {
|
||||
w = math.Abs(pj.X - pi.X)
|
||||
}
|
||||
if math.Abs(pj.Y-pi.Y) > h {
|
||||
h = math.Abs(pj.Y - pi.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
barrierColliderShape := resolv.NewConvexPolygon()
|
||||
for _, p := range barrier.Boundary.Points {
|
||||
barrierColliderShape.AddPoints(p.X, p.Y)
|
||||
}
|
||||
|
||||
barrierCollider := resolv.NewObject(barrier.Boundary.Anchor.X+spaceOffsetX, barrier.Boundary.Anchor.Y+spaceOffsetY, w, h, "Barrier")
|
||||
barrierCollider.SetShape(barrierColliderShape)
|
||||
boundaryUnaligned := barrier.Boundary
|
||||
barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
|
||||
space.Add(barrierCollider)
|
||||
}
|
||||
}
|
||||
|
1
charts/DelayNoMore.drawio
Normal file
1
charts/DelayNoMore.drawio
Normal file
File diff suppressed because one or more lines are too long
BIN
charts/InputDelayIntro.jpg
Normal file
BIN
charts/InputDelayIntro.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
BIN
charts/RollbackAndChase.jpg
Normal file
BIN
charts/RollbackAndChase.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 190 KiB |
Before Width: | Height: | Size: 430 KiB After Width: | Height: | Size: 430 KiB |
Before Width: | Height: | Size: 684 KiB After Width: | Height: | Size: 684 KiB |
@@ -85,8 +85,8 @@ type Game struct {
|
||||
|
||||
func NewGame() *Game {
|
||||
|
||||
// stageName := "simple" // Use this for calibration
|
||||
stageName := "richsoil"
|
||||
stageName := "simple" // Use this for calibration
|
||||
// stageName := "richsoil"
|
||||
stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, playerPosMap, barrierMap, err := parseStage(stageName)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
@@ -160,7 +160,6 @@ func (g *Game) DebugDraw(screen *ebiten.Image, space *resolv.Space) {
|
||||
ebitenutil.DrawLine(screen, cx, cy+ch, cx, cy, drawColor)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (g *Game) Layout(w, h int) (int, int) {
|
||||
|
@@ -4,12 +4,9 @@ import (
|
||||
. "dnmshared"
|
||||
"fmt"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
"github.com/solarlune/resolv"
|
||||
"go.uber.org/zap"
|
||||
"image/color"
|
||||
|
||||
"math"
|
||||
)
|
||||
|
||||
type WorldColliderDisplay struct {
|
||||
@@ -35,52 +32,48 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi
|
||||
spaceOffsetX := float64(spaceW) * 0.5
|
||||
spaceOffsetY := float64(spaceH) * 0.5
|
||||
|
||||
// TODO: Move collider y-axis transformation to a "dnmshared"
|
||||
playerColliderRadius := float64(12) // hardcoded
|
||||
playerColliderRadius := float64(32)
|
||||
playerColliders := make([]*resolv.Object, len(playerList))
|
||||
space := resolv.NewSpace(int(spaceW), int(spaceH), 16, 16)
|
||||
for _, player := range playerList {
|
||||
playerCollider := resolv.NewObject(player.X+spaceOffsetX, player.Y+spaceOffsetY, playerColliderRadius*2, playerColliderRadius*2, "Player")
|
||||
playerColliderShape := resolv.NewCircle(0, 0, playerColliderRadius*2)
|
||||
playerCollider.SetShape(playerColliderShape)
|
||||
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))
|
||||
playerColliders[i] = playerCollider
|
||||
space.Add(playerCollider)
|
||||
}
|
||||
|
||||
barrierLocalId := 0
|
||||
for _, barrierUnaligned := range barrierList {
|
||||
barrier := AlignPolygon2DToBoundingBox(barrierUnaligned)
|
||||
|
||||
var w float64 = 0
|
||||
var h float64 = 0
|
||||
|
||||
for i, pi := range barrier.Points {
|
||||
for j, pj := range barrier.Points {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if math.Abs(pj.X-pi.X) > w {
|
||||
w = math.Abs(pj.X - pi.X)
|
||||
}
|
||||
if math.Abs(pj.Y-pi.Y) > h {
|
||||
h = math.Abs(pj.Y - pi.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
barrierColliderShape := resolv.NewConvexPolygon()
|
||||
for i := 0; i < len(barrier.Points); i++ {
|
||||
p := barrier.Points[i]
|
||||
barrierColliderShape.AddPoints(p.X, p.Y)
|
||||
}
|
||||
|
||||
barrierCollider := resolv.NewObject(barrier.Anchor.X+spaceOffsetX, barrier.Anchor.Y+spaceOffsetY, w, h, "Barrier")
|
||||
barrierCollider.SetShape(barrierColliderShape)
|
||||
|
||||
barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, "Barrier")
|
||||
Logger.Info(fmt.Sprintf("Added barrier: shape=%v", barrierCollider.Shape))
|
||||
space.Add(barrierCollider)
|
||||
|
||||
barrierLocalId++
|
||||
}
|
||||
|
||||
world.Space = space
|
||||
|
||||
moveToCollide := true
|
||||
if moveToCollide {
|
||||
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 += dx
|
||||
toTestPlayerCollider.Y += dy
|
||||
toTestPlayerCollider.Update()
|
||||
}
|
||||
|
||||
return world
|
||||
}
|
||||
|
||||
@@ -92,9 +85,8 @@ func (world *WorldColliderDisplay) Draw(screen *ebiten.Image) {
|
||||
|
||||
for _, o := range world.Space.Objects() {
|
||||
if o.HasTags("Player") {
|
||||
circle := o.Shape.(*resolv.Circle)
|
||||
drawColor := color.RGBA{0, 255, 0, 255}
|
||||
ebitenutil.DrawCircle(screen, circle.X, circle.Y, circle.Radius, drawColor)
|
||||
DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)
|
||||
} else {
|
||||
drawColor := color.RGBA{60, 60, 60, 255}
|
||||
DrawPolygon(screen, o.Shape.(*resolv.ConvexPolygon), drawColor)
|
||||
|
@@ -15,6 +15,10 @@ type Vec2D struct {
|
||||
Y float64 `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
func NormVec2D(dx, dy float64) Vec2D {
|
||||
return Vec2D{dy, -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:"-"`
|
||||
|
220
dnmshared/resolv_helper.go
Normal file
220
dnmshared/resolv_helper.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package dnmshared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kvartborg/vector"
|
||||
"github.com/solarlune/resolv"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ConvexPolygonStr(body *resolv.ConvexPolygon) string {
|
||||
var s []string = make([]string, len(body.Points))
|
||||
for i, p := range body.Points {
|
||||
s[i] = fmt.Sprintf("[%v, %v]", p[0]+body.X, p[1]+body.Y)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s]", strings.Join(s, ", "))
|
||||
}
|
||||
|
||||
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)
|
||||
shape := resolv.NewRectangle(0, 0, w, h)
|
||||
collider.SetShape(shape)
|
||||
return collider
|
||||
}
|
||||
|
||||
func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
|
||||
aligned := AlignPolygon2DToBoundingBox(unalignedSrc)
|
||||
var w, h float64 = 0, 0
|
||||
|
||||
shape := resolv.NewConvexPolygon()
|
||||
for i, pi := range aligned.Points {
|
||||
for j, pj := range aligned.Points {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if math.Abs(pj.X-pi.X) > w {
|
||||
w = math.Abs(pj.X - pi.X)
|
||||
}
|
||||
if math.Abs(pj.Y-pi.Y) > h {
|
||||
h = math.Abs(pj.Y - pi.Y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(aligned.Points); i++ {
|
||||
p := aligned.Points[i]
|
||||
shape.AddPoints(p.X, p.Y)
|
||||
}
|
||||
|
||||
collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag)
|
||||
collider.SetShape(shape)
|
||||
|
||||
return collider
|
||||
}
|
||||
|
||||
func CalcPushbacks(oldDx, oldDy float64, playerShape, barrierShape *resolv.ConvexPolygon) (bool, float64, float64) {
|
||||
origX, origY := playerShape.Position()
|
||||
defer func() {
|
||||
playerShape.SetPosition(origX, origY)
|
||||
}()
|
||||
playerShape.SetPosition(origX+oldDx, origY+oldDy)
|
||||
overlapResult := &SatResult{
|
||||
Overlap: 0,
|
||||
OverlapX: 0,
|
||||
OverlapY: 0,
|
||||
AContainedInB: true,
|
||||
BContainedInA: true,
|
||||
Axis: vector.Vector{0, 0},
|
||||
}
|
||||
if overlapped := IsPolygonPairOverlapped(playerShape, barrierShape, overlapResult); overlapped {
|
||||
pushbackX, pushbackY := overlapResult.Overlap*overlapResult.OverlapX, overlapResult.Overlap*overlapResult.OverlapY
|
||||
return true, pushbackX, pushbackY
|
||||
} else {
|
||||
return false, 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
type SatResult struct {
|
||||
Overlap float64
|
||||
OverlapX float64
|
||||
OverlapY float64
|
||||
AContainedInB bool
|
||||
BContainedInA bool
|
||||
Axis vector.Vector
|
||||
}
|
||||
|
||||
func IsPolygonPairOverlapped(a, b *resolv.ConvexPolygon, result *SatResult) bool {
|
||||
aCnt, bCnt := len(a.Points), len(b.Points)
|
||||
// Single point case
|
||||
if 1 == aCnt && 1 == bCnt {
|
||||
if nil != result {
|
||||
result.Overlap = 0
|
||||
}
|
||||
return a.Points[0].X() == b.Points[0].X() && a.Points[0].Y() == b.Points[0].Y()
|
||||
}
|
||||
|
||||
if 1 < aCnt {
|
||||
for _, axis := range a.SATAxes() {
|
||||
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 1 < bCnt {
|
||||
for _, axis := range b.SATAxes() {
|
||||
if isPolygonPairSeparatedByDir(a, b, axis.Unit(), result) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isPolygonPairSeparatedByDir(a, b *resolv.ConvexPolygon, e vector.Vector, result *SatResult) bool {
|
||||
/*
|
||||
[WARNING] This function is deliberately made private, it shouldn't be used alone (i.e. not along the norms of a polygon), otherwise the pushbacks calculated would be meaningless.
|
||||
|
||||
Consider the following example
|
||||
a: {
|
||||
anchor: [1337.19 1696.74]
|
||||
points: [[0 0] [24 0] [24 24] [0 24]]
|
||||
},
|
||||
b: {
|
||||
anchor: [1277.72 1570.56]
|
||||
points: [[642.57 319.16] [0 319.16] [5.73 0] [643.75 0.90]]
|
||||
}
|
||||
|
||||
e = (-2.98, 1.49).Unit()
|
||||
*/
|
||||
|
||||
var aStart, aEnd, bStart, bEnd float64 = math.MaxFloat64, -math.MaxFloat64, math.MaxFloat64, -math.MaxFloat64
|
||||
for _, p := range a.Points {
|
||||
dot := (p.X()+a.X)*e.X() + (p.Y()+a.Y)*e.Y()
|
||||
|
||||
if aStart > dot {
|
||||
aStart = dot
|
||||
}
|
||||
|
||||
if aEnd < dot {
|
||||
aEnd = dot
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range b.Points {
|
||||
dot := (p.X()+b.X)*e.X() + (p.Y()+b.Y)*e.Y()
|
||||
|
||||
if bStart > dot {
|
||||
bStart = dot
|
||||
}
|
||||
|
||||
if bEnd < dot {
|
||||
bEnd = dot
|
||||
}
|
||||
}
|
||||
|
||||
if aStart > bEnd || aEnd < bStart {
|
||||
// Separated by unit vector "e"
|
||||
return true
|
||||
}
|
||||
|
||||
if nil != result {
|
||||
result.Axis = e
|
||||
overlap := float64(0)
|
||||
|
||||
if aStart < bStart {
|
||||
result.AContainedInB = false
|
||||
|
||||
if aEnd < bEnd {
|
||||
overlap = aEnd - bStart
|
||||
result.BContainedInA = false
|
||||
} else {
|
||||
option1 := aEnd - bStart
|
||||
option2 := bEnd - aStart
|
||||
if option1 < option2 {
|
||||
overlap = option1
|
||||
} else {
|
||||
overlap = -option2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.BContainedInA = false
|
||||
|
||||
if aEnd > bEnd {
|
||||
overlap = aStart - bEnd
|
||||
result.AContainedInB = false
|
||||
} else {
|
||||
option1 := aEnd - bStart
|
||||
option2 := bEnd - aStart
|
||||
if option1 < option2 {
|
||||
overlap = option1
|
||||
} else {
|
||||
overlap = -option2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentOverlap := result.Overlap
|
||||
absoluteOverlap := overlap
|
||||
if overlap < 0 {
|
||||
absoluteOverlap = -overlap
|
||||
}
|
||||
|
||||
if 0 == currentOverlap || currentOverlap > absoluteOverlap {
|
||||
var sign float64 = 1
|
||||
if overlap < 0 {
|
||||
sign = -1
|
||||
}
|
||||
|
||||
result.Overlap = absoluteOverlap
|
||||
result.OverlapX = e.X() * sign
|
||||
result.OverlapY = e.Y() * sign
|
||||
}
|
||||
}
|
||||
|
||||
// the specified unit vector "e" doesn't separate "a" and "b", overlap result is generated
|
||||
return false
|
||||
}
|
@@ -444,38 +444,38 @@ func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continu
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
// 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
|
||||
}
|
||||
for i, p := range input.Points {
|
||||
output.Points[i] = &Vec2D{
|
||||
X: p.X - boundingBoxTL.X,
|
||||
Y: p.Y - boundingBoxTL.Y,
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
@@ -539,7 +539,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
210.4441731196186,
|
||||
342.9460598986377,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
@@ -67,19 +67,6 @@ cc.Class({
|
||||
|
||||
onLoad() {
|
||||
|
||||
//kobako: 腾讯统计代码
|
||||
//WARN: 打包到微信小游戏的时候会导致出错
|
||||
/*
|
||||
(function() {
|
||||
var mta = document.createElement("script");
|
||||
mta.src = "//pingjs.qq.com/h5/stats.js?v2.0.4";
|
||||
mta.setAttribute("name", "MTAH5");
|
||||
mta.setAttribute("sid", "500674632");
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(mta, s);
|
||||
})();
|
||||
*/
|
||||
|
||||
window.atFirstLocationHref = window.location.href.split('#')[0];
|
||||
const self = this;
|
||||
self.getRetCodeList();
|
||||
@@ -97,10 +84,8 @@ cc.Class({
|
||||
self.smsLoginCaptchaLabel.active = true;
|
||||
|
||||
self.loginButton.active = true;
|
||||
self.checkPhoneNumber = self.checkPhoneNumber.bind(self);
|
||||
self.checkIntAuthTokenExpire = self.checkIntAuthTokenExpire.bind(self);
|
||||
self.checkCaptcha = self.checkCaptcha.bind(self);
|
||||
self.onSMSCaptchaGetButtonClicked = self.onSMSCaptchaGetButtonClicked.bind(self);
|
||||
self.onLoginButtonClicked = self.onLoginButtonClicked.bind(self);
|
||||
self.onSMSCaptchaGetButtonClicked = self.onSMSCaptchaGetButtonClicked.bind(self);
|
||||
self.smsLoginCaptchaButton.on('click', self.onSMSCaptchaGetButtonClicked);
|
||||
|
||||
self.loadingNode = cc.instantiate(this.loadingPrefab);
|
||||
@@ -125,11 +110,12 @@ cc.Class({
|
||||
window.WsReq = protoRoot.lookupType("treasurehunterx.WsReq");
|
||||
window.WsResp = protoRoot.lookupType("treasurehunterx.WsResp");
|
||||
self.checkIntAuthTokenExpire().then(
|
||||
() => {
|
||||
const intAuthToken = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken;
|
||||
(intAuthToken) => {
|
||||
console.log("Successfully found `intAuthToken` in local cache");
|
||||
self.useTokenLogin(intAuthToken);
|
||||
},
|
||||
() => {
|
||||
console.warn("Failed to find `intAuthToken` in local cache");
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
}
|
||||
);
|
||||
@@ -221,11 +207,28 @@ cc.Class({
|
||||
checkIntAuthTokenExpire() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!cc.sys.localStorage.getItem('selfPlayer')) {
|
||||
console.warn("Couldn't find selfPlayer key in local cache");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
const selfPlayer = JSON.parse(cc.sys.localStorage.getItem('selfPlayer'));
|
||||
(selfPlayer.intAuthToken && new Date().getTime() < selfPlayer.expiresAt) ? resolve() : reject();
|
||||
if (null == selfPlayer) {
|
||||
console.warn("Couldn't find selfPlayer object in local cache");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
if (null == selfPlayer.intAuthToken) {
|
||||
console.warn("Couldn't find selfPlayer object with key `intAuthToken` in local cache");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
if (new Date().getTime() > selfPlayer.expiresAt) {
|
||||
console.warn("Couldn't find unexpired selfPlayer `intAuthToken` in local cache");
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
resolve(selfPlayer.intAuthToken);
|
||||
})
|
||||
},
|
||||
|
||||
@@ -278,13 +281,15 @@ cc.Class({
|
||||
intAuthToken: _intAuthToken
|
||||
},
|
||||
success: function(resp) {
|
||||
console.log("Login attempt `useTokenLogin` succeeded.");
|
||||
self.onLoggedIn(resp);
|
||||
},
|
||||
error: function(xhr, status, errMsg) {
|
||||
console.log("Login attempt `useTokenLogin` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`.");
|
||||
console.warn("Login attempt `useTokenLogin` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`.");
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage()
|
||||
},
|
||||
timeout: function() {
|
||||
console.warn("Login attempt `useTokenLogin` timed out, about to enable interactive controls.");
|
||||
self.enableInteractiveControls(true);
|
||||
},
|
||||
});
|
||||
@@ -335,7 +340,7 @@ cc.Class({
|
||||
|
||||
onLoggedIn(res) {
|
||||
const self = this;
|
||||
cc.log(`OnLoggedIn ${JSON.stringify(res)}.`)
|
||||
console.log("OnLoggedIn ", JSON.stringify(res))
|
||||
if (res.ret === self.retCodeDict.OK) {
|
||||
self.enableInteractiveControls(false);
|
||||
const date = Number(res.expiresAt);
|
||||
@@ -360,6 +365,7 @@ cc.Class({
|
||||
);
|
||||
cc.director.loadScene('default_map');
|
||||
} else {
|
||||
console.log("OnLoggedIn failed, about to remove `selfPlayer` in local cache.")
|
||||
cc.sys.localStorage.removeItem("selfPlayer");
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
self.enableInteractiveControls(true);
|
||||
|
@@ -253,8 +253,8 @@ cc.Class({
|
||||
if (null != window.handleBattleColliderInfo) {
|
||||
window.handleBattleColliderInfo = null;
|
||||
}
|
||||
if (null != window.handleClientSessionCloseOrError) {
|
||||
window.handleClientSessionCloseOrError = null;
|
||||
if (null != window.handleClientSessionError) {
|
||||
window.handleClientSessionError = null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -349,8 +349,8 @@ cc.Class({
|
||||
window.forceBigEndianFloatingNumDecoding = self.forceBigEndianFloatingNumDecoding;
|
||||
|
||||
console.warn("+++++++ Map onLoad()");
|
||||
window.handleClientSessionCloseOrError = function() {
|
||||
console.warn('+++++++ Common handleClientSessionCloseOrError()');
|
||||
window.handleClientSessionError = function() {
|
||||
console.warn('+++++++ Common handleClientSessionError()');
|
||||
|
||||
if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) {
|
||||
console.log("Battled ended by settlement");
|
||||
@@ -382,7 +382,8 @@ 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;
|
||||
@@ -472,7 +473,7 @@ cc.Class({
|
||||
pts.push([boundaryObj[i].x - x0, boundaryObj[i].y - y0]);
|
||||
}
|
||||
const newBarrierLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
|
||||
console.log("Created barrier: ", newBarrierLatest);
|
||||
// console.log("Created barrier: ", newBarrierLatest);
|
||||
const newBarrierChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts);
|
||||
++barrierIdCounter;
|
||||
const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter);
|
||||
@@ -740,9 +741,15 @@ cc.Class({
|
||||
newPlayerNode.setPosition(cc.v2(x, y));
|
||||
newPlayerNode.getComponent("SelfPlayer").mapNode = self.node;
|
||||
const currentSelfColliderCircle = newPlayerNode.getComponent(cc.CircleCollider);
|
||||
const r = currentSelfColliderCircle.radius,
|
||||
d = 2 * r;
|
||||
// The collision box of an individual player is a polygon instead of a circle, because the backend collision engine doesn't handle circle alignment well.
|
||||
const x0 = x - r,
|
||||
y0 = y - r;
|
||||
let pts = [[0, 0], [d, 0], [d, d], [0, d]];
|
||||
|
||||
const newPlayerColliderLatest = self.latestCollisionSys.createCircle(x, y, currentSelfColliderCircle.radius);
|
||||
const newPlayerColliderChaser = self.chaserCollisionSys.createCircle(x, y, currentSelfColliderCircle.radius);
|
||||
const newPlayerColliderLatest = self.latestCollisionSys.createPolygon(x0, y0, pts);
|
||||
const newPlayerColliderChaser = self.chaserCollisionSys.createPolygon(x0, y0, pts);
|
||||
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
|
||||
self.latestCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderLatest);
|
||||
self.chaserCollisionSysMap.set(collisionPlayerIndex, newPlayerColliderChaser);
|
||||
@@ -952,10 +959,12 @@ cc.Class({
|
||||
const joinIndex = playerRichInfo.joinIndex;
|
||||
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
|
||||
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
|
||||
const currentSelfColliderCircle = playerRichInfo.node.getComponent(cc.CircleCollider);
|
||||
const r = currentSelfColliderCircle.radius;
|
||||
rdf.players[playerRichInfo.id] = {
|
||||
id: playerRichInfo.id,
|
||||
x: playerCollider.x,
|
||||
y: playerCollider.y,
|
||||
x: playerCollider.x + r, // [WARNING] the (x, y) of "playerCollider" is offset to the anchor (i.e. first point of all points) of the polygon shape
|
||||
y: playerCollider.y + r,
|
||||
dir: self.ctrl.decodeDirection(null == inputFrameAppliedOnPrevRenderFrame ? 0 : inputFrameAppliedOnPrevRenderFrame.inputList[joinIndex - 1]),
|
||||
speed: (null == speedRefRenderFrame ? playerRichInfo.speed : speedRefRenderFrame.players[playerRichInfo.id].speed),
|
||||
joinIndex: joinIndex
|
||||
@@ -1025,8 +1034,11 @@ cc.Class({
|
||||
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
|
||||
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
|
||||
const player = latestRdf.players[playerId];
|
||||
playerCollider.x = player.x;
|
||||
playerCollider.y = player.y;
|
||||
|
||||
const currentSelfColliderCircle = playerRichInfo.node.getComponent(cc.CircleCollider);
|
||||
const r = currentSelfColliderCircle.radius;
|
||||
playerCollider.x = player.x - r;
|
||||
playerCollider.y = player.y - r;
|
||||
});
|
||||
|
||||
/*
|
||||
|
@@ -104,18 +104,6 @@ window.getExpectedRoomIdSync = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
window.unsetClientSessionCloseOrErrorFlag = function() {
|
||||
cc.sys.localStorage.removeItem("ClientSessionCloseOrErrorFlag");
|
||||
return;
|
||||
}
|
||||
|
||||
window.setClientSessionCloseOrErrorFlag = function() {
|
||||
const oldVal = cc.sys.localStorage.getItem("ClientSessionCloseOrErrorFlag");
|
||||
if (true == oldVal) return false;
|
||||
cc.sys.localStorage.setItem("ClientSessionCloseOrErrorFlag", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
if (window.clientSession && window.clientSession.readyState == WebSocket.OPEN) {
|
||||
if (null != onopenCb) {
|
||||
@@ -124,7 +112,9 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intAuthToken = cc.sys.localStorage.getItem("selfPlayer") ? JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken : "";
|
||||
const selfPlayerStr = cc.sys.localStorage.getItem("selfPlayer");
|
||||
const selfPlayer = null == selfPlayerStr ? null : JSON.parse(selfPlayerStr);
|
||||
const intAuthToken = null == selfPlayer ? "" : selfPlayer.intAuthToken;
|
||||
|
||||
let urlToConnect = backendAddress.PROTOCOL.replace('http', 'ws') + '://' + backendAddress.HOST + ":" + backendAddress.PORT + backendAddress.WS_PATH_PREFIX + "?intAuthToken=" + intAuthToken;
|
||||
|
||||
@@ -144,19 +134,19 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
const clientSession = new WebSocket(urlToConnect);
|
||||
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
|
||||
|
||||
clientSession.onopen = function(event) {
|
||||
console.log("The WS clientSession is opened.");
|
||||
clientSession.onopen = function(evt) {
|
||||
console.log("The WS clientSession is opened. clientSession.id=", clientSession.id);
|
||||
window.clientSession = clientSession;
|
||||
if (null == onopenCb) return;
|
||||
onopenCb();
|
||||
};
|
||||
|
||||
clientSession.onmessage = function(event) {
|
||||
if (null == event || null == event.data) {
|
||||
clientSession.onmessage = function(evt) {
|
||||
if (null == evt || null == evt.data) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = window.WsResp.decode(new Uint8Array(event.data));
|
||||
const resp = window.WsResp.decode(new Uint8Array(evt.data));
|
||||
switch (resp.act) {
|
||||
case window.DOWNSYNC_MSG_ACT_HB_REQ:
|
||||
window.handleHbRequirements(resp); // 获取boundRoomId并存储到localStorage
|
||||
@@ -189,10 +179,11 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
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));
|
||||
// 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));
|
||||
// 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));
|
||||
console.warn("Got forced resync@localRenderFrameId=", mapIns.renderFrameId, ", @lastAllConfirmedRenderFrameId=", mapIns.lastAllConfirmedRenderFrameId, "@lastAllConfirmedInputFrameId=", mapIns.lastAllConfirmedInputFrameId, ", @localRecentInputCache=", mapIns._stringifyRecentInputCache(false), ", inputFrameIdConsecutive=", inputFrameIdConsecutive, ", renderFrameIdConsecutive=", renderFrameIdConsecutive);
|
||||
// The following order of execution is important
|
||||
mapIns.onRoomDownsyncFrame(resp.rdf);
|
||||
mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch);
|
||||
@@ -202,52 +193,46 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Unexpected error when parsing data of:", event.data, e);
|
||||
console.error("Unexpected error when parsing data of:", evt.data, e);
|
||||
}
|
||||
};
|
||||
|
||||
clientSession.onerror = function(event) {
|
||||
if (!window.setClientSessionCloseOrErrorFlag()) {
|
||||
return;
|
||||
clientSession.onerror = function(evt) {
|
||||
console.error("Error caught on the WS clientSession: ", evt);
|
||||
if (window.handleClientSessionError) {
|
||||
window.handleClientSessionError();
|
||||
}
|
||||
console.error("Error caught on the WS clientSession: ", event);
|
||||
if (window.clientSessionPingInterval) {
|
||||
clearInterval(window.clientSessionPingInterval);
|
||||
}
|
||||
if (window.handleClientSessionCloseOrError) {
|
||||
window.handleClientSessionCloseOrError();
|
||||
}
|
||||
window.unsetClientSessionCloseOrErrorFlag();
|
||||
};
|
||||
|
||||
clientSession.onclose = function(event) {
|
||||
if (!window.setClientSessionCloseOrErrorFlag()) {
|
||||
return;
|
||||
}
|
||||
console.warn("The WS clientSession is closed: ", event);
|
||||
if (window.clientSessionPingInterval) {
|
||||
clearInterval(window.clientSessionPingInterval);
|
||||
}
|
||||
if (false == event.wasClean) {
|
||||
// Chrome doesn't allow the use of "CustomCloseCode"s (yet) and will callback with a "WebsocketStdCloseCode 1006" and "false == event.wasClean" here. See https://tools.ietf.org/html/rfc6455#section-7.4 for more information.
|
||||
if (window.handleClientSessionCloseOrError) {
|
||||
window.handleClientSessionCloseOrError();
|
||||
clientSession.onclose = function(evt) {
|
||||
// [WARNING] The callback "onclose" might be called AFTER the webpage is refreshed with "1001 == evt.code".
|
||||
console.warn("The WS clientSession is closed: ", evt, clientSession);
|
||||
if (false == evt.wasClean) {
|
||||
/*
|
||||
Chrome doesn't allow the use of "CustomCloseCode"s (yet) and will callback with a "WebsocketStdCloseCode 1006" and "false == evt.wasClean" here. See https://tools.ietf.org/html/rfc6455#section-7.4 for more information.
|
||||
*/
|
||||
if (window.handleClientSessionError) {
|
||||
window.handleClientSessionError();
|
||||
}
|
||||
} else {
|
||||
switch (event.code) {
|
||||
switch (evt.code) {
|
||||
case constants.RET_CODE.PLAYER_NOT_ADDABLE_TO_ROOM:
|
||||
case constants.RET_CODE.PLAYER_NOT_READDABLE_TO_ROOM:
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
break;
|
||||
case constants.RET_CODE.UNKNOWN_ERROR:
|
||||
case constants.RET_CODE.MYSQL_ERROR:
|
||||
case constants.RET_CODE.PLAYER_NOT_FOUND:
|
||||
case constants.RET_CODE.PLAYER_CHEATING:
|
||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||
case 1006: // Peer(i.e. the backend) gone unexpectedly
|
||||
if (window.handleClientSessionError) {
|
||||
window.handleClientSessionError();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (window.handleClientSessionCloseOrError) {
|
||||
window.handleClientSessionCloseOrError();
|
||||
}
|
||||
}
|
||||
window.unsetClientSessionCloseOrErrorFlag();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -258,17 +243,17 @@ window.clearLocalStorageAndBackToLoginScene = function(shouldRetainBoundRoomIdIn
|
||||
window.mapIns.musicEffectManagerScriptIns.stopAllMusic();
|
||||
}
|
||||
/**
|
||||
* Here I deliberately removed the callback in the "common `handleClientSessionCloseOrError` callback"
|
||||
* Here I deliberately removed the callback in the "common `handleClientSessionError` callback"
|
||||
* within which another invocation to `clearLocalStorageAndBackToLoginScene` will be made.
|
||||
*
|
||||
* It'll be re-assigned to the common one upon reentrance of `Map.onLoad`.
|
||||
*
|
||||
* -- YFLu 2019-04-06
|
||||
*/
|
||||
window.handleClientSessionCloseOrError = () => {
|
||||
console.warn("+++++++ Special handleClientSessionCloseOrError() assigned within `clearLocalStorageAndBackToLoginScene`");
|
||||
window.handleClientSessionError = () => {
|
||||
console.warn("+++++++ Special handleClientSessionError() assigned within `clearLocalStorageAndBackToLoginScene`");
|
||||
// TBD.
|
||||
window.handleClientSessionCloseOrError = null; // To ensure that it's called at most once.
|
||||
window.handleClientSessionError = null; // To ensure that it's called at most once.
|
||||
};
|
||||
window.closeWSConnection();
|
||||
window.clearSelfPlayer();
|
||||
|
39
frontend/assets/scripts/collision_test_nodejs.js
Normal file
39
frontend/assets/scripts/collision_test_nodejs.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const collisions = require('./modules/Collisions');
|
||||
|
||||
const collisionSys = new collisions.Collisions();
|
||||
|
||||
/*
|
||||
Backend result reference
|
||||
|
||||
2022-10-22T12:11:25.156+0800 INFO collider_visualizer/worldColliderDisplay.go:77 Collided: player.X=1257.665, player.Y=1415.335, oldDx=-2.98, oldDy=-50, playerShape=&{[[0 0] [64 0] [64 64] [0 64]] 1254.685 1365.335 true}, toCheckBarrier=&{[[628.626 54.254500000000064] [0 56.03250000000003] [0.42449999999999477 1.1229999999999905] [625.9715000000001 0]] 1289.039 1318.0805 true}, pushbackX=-0.15848054013127655, pushbackY=-56.03205175509715, result=&{56.03227587710039 -0.0028283794946841584 -0.9999960001267175 false false [0.9988052279193613 -0.04886836073527201]}
|
||||
*/
|
||||
function polygonStr(body) {
|
||||
let coords = [];
|
||||
let cnt = body._coords.length;
|
||||
for (let ix = 0, iy = 1; ix < cnt; ix += 2, iy += 2) {
|
||||
coords.push([body._coords[ix], body._coords[iy]]);
|
||||
}
|
||||
return JSON.stringify(coords);
|
||||
}
|
||||
|
||||
const playerCollider = collisionSys.createPolygon(1257.665, 1415.335, [[0, 0], [64, 0], [64, 64], [0, 64]]);
|
||||
const barrierCollider = collisionSys.createPolygon(1289.039, 1318.0805, [[628.626, 54.254500000000064], [0, 56.03250000000003], [0.42449999999999477, 1.1229999999999905], [625.9715000000001, 0]]);
|
||||
|
||||
const oldDx = -2.98;
|
||||
const oldDy = -50.0;
|
||||
|
||||
playerCollider.x += oldDx;
|
||||
playerCollider.y += oldDy;
|
||||
|
||||
collisionSys.update();
|
||||
const result = collisionSys.createResult();
|
||||
|
||||
const potentials = playerCollider.potentials();
|
||||
|
||||
let overlapCheckId = 0;
|
||||
for (const barrier of potentials) {
|
||||
if (!playerCollider.collides(barrier, result)) continue;
|
||||
const pushbackX = result.overlap * result.overlap_x;
|
||||
const pushbackY = result.overlap * result.overlap_y;
|
||||
console.log("For overlapCheckId=" + overlapCheckId + ", the overlap: a=", polygonStr(result.a), ", b=", polygonStr(result.b), ", pushbackX=", pushbackX, ", pushbackY=", pushbackY);
|
||||
}
|
9
frontend/assets/scripts/collision_test_nodejs.js.meta
Normal file
9
frontend/assets/scripts/collision_test_nodejs.js.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "fce86138-76fc-44d5-8eac-2731b3b0cefd",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user