Compare commits

...

15 Commits
v0.4 ... v0.5.3

Author SHA1 Message Date
genxium
ec2a21dbe7 Minor fix for "false == Room.BackendDynamicsEnabled". 2022-10-31 09:02:14 +08:00
Wing
dc6402c2b7 Updated README. 2022-10-26 10:28:32 +08:00
yflu
8038b393e0 Formatted codes. 2022-10-25 23:36:55 +08:00
yflu
4e0f7b52d4 Fixed frontend ws session onclose handling. 2022-10-25 23:02:39 +08:00
genxium
486c46f608 Added backend dynamics toggle. 2022-10-25 10:42:36 +08:00
genxium
6d075877ec Fixed frontend reconnection on page refresh for Firefox and Safari. 2022-10-25 09:52:38 +08:00
yflu
fe826b393b Updated logs in frontend. 2022-10-25 00:05:38 +08:00
Wing
c69aa25353 Merge pull request #4 from genxium/backend_collision_pushback
Backend collision pushback synchronization.
2022-10-22 13:52:52 +08:00
yflu
0f4d067c06 Updated test cases for frontend-backend-collision-reconciliation. 2022-10-22 13:38:10 +08:00
yflu
cff31d295c Simplified frontend log. 2022-10-22 00:03:26 +08:00
yflu
150e30db2a Drafted backend collision with pushback calculations. 2022-10-21 22:39:08 +08:00
genxium
bc8989a0e6 Minor update. 2022-10-19 17:56:52 +08:00
genxium
1959a7fd9a Refactored use of SAT collision checking. 2022-10-19 17:32:18 +08:00
genxium
3baaf1d52c Updated collider utility encapsulation for visualization subproject. 2022-10-19 15:10:11 +08:00
genxium
62f10e0877 Added collision test in collider visualizer. 2022-10-19 09:48:52 +08:00
15 changed files with 544 additions and 240 deletions

View File

@@ -3,9 +3,12 @@
This project is a demo for a websocket-based input synchronization method inspired by [GGPO](https://www.ggpo.net/).
![screenshot-1](./screenshot-1.png)
Please checkout [this demo video](https://pan.baidu.com/s/1aM6e8IWaJszFCYAsRjt19g?pwd=z02c) to see whether the source codes are doing what you expect for synchronization.
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 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.
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

View File

@@ -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,21 @@ 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)
barrierShape := collision.Objects[0].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("Collider 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 +1260,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 +1273,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)
}
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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
View 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
}

View File

@@ -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
}

View File

@@ -539,7 +539,7 @@
"array": [
0,
0,
210.4441731196186,
342.9460598986377,
0,
0,
0,

View File

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

View File

@@ -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);

View File

@@ -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;
});
/*

View File

@@ -104,18 +104,6 @@ window.getExpectedRoomIdSync = function() {
return null;
};
window.unsetClientSessionCloseOrErrorFlag = function() {
cc.sys.localStorage.removeItem("ClientSessionCloseOrErrorFlag");
return;
}
window.setClientSessionCloseOrErrorFlag = function() {
const oldVal = cc.sys.localStorage.getItem("ClientSessionCloseOrErrorFlag");
if (true == oldVal) return false;
cc.sys.localStorage.setItem("ClientSessionCloseOrErrorFlag", true);
return true;
}
window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
if (window.clientSession && window.clientSession.readyState == WebSocket.OPEN) {
if (null != onopenCb) {
@@ -124,7 +112,9 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
return;
}
const intAuthToken = cc.sys.localStorage.getItem("selfPlayer") ? JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken : "";
const selfPlayerStr = cc.sys.localStorage.getItem("selfPlayer");
const selfPlayer = null == selfPlayerStr ? null : JSON.parse(selfPlayerStr);
const intAuthToken = null == selfPlayer ? "" : selfPlayer.intAuthToken;
let urlToConnect = backendAddress.PROTOCOL.replace('http', 'ws') + '://' + backendAddress.HOST + ":" + backendAddress.PORT + backendAddress.WS_PATH_PREFIX + "?intAuthToken=" + intAuthToken;
@@ -144,19 +134,19 @@ window.initPersistentSessionClient = function(onopenCb, expectedRoomId) {
const clientSession = new WebSocket(urlToConnect);
clientSession.binaryType = 'arraybuffer'; // Make 'event.data' of 'onmessage' an "ArrayBuffer" instead of a "Blob"
clientSession.onopen = function(event) {
console.log("The WS clientSession is opened.");
clientSession.onopen = function(evt) {
console.log("The WS clientSession is opened. clientSession.id=", clientSession.id);
window.clientSession = clientSession;
if (null == onopenCb) return;
onopenCb();
};
clientSession.onmessage = function(event) {
if (null == event || null == event.data) {
clientSession.onmessage = function(evt) {
if (null == evt || null == evt.data) {
return;
}
try {
const resp = window.WsResp.decode(new Uint8Array(event.data));
const resp = window.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();

View 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);
}

View 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