Merge pull request #8 from genxium/character_attack

Added melee attack feature.
This commit is contained in:
Wing 2022-11-24 17:51:46 +08:00 committed by GitHub
commit 2290c57c1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 4014 additions and 2279 deletions

View File

@ -2,7 +2,6 @@ package models
import ( import (
. "battle_srv/protos" . "battle_srv/protos"
. "dnmshared/sharedprotos"
) )
func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) map[int32]*PlayerDownsync { func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) map[int32]*PlayerDownsync {
@ -13,13 +12,11 @@ func toPbPlayers(modelInstances map[int32]*Player, withMetaInfo bool) map[int32]
for k, last := range modelInstances { for k, last := range modelInstances {
toRet[k] = &PlayerDownsync{ toRet[k] = &PlayerDownsync{
Id: last.Id, Id: last.Id,
VirtualGridX: last.VirtualGridX, VirtualGridX: last.VirtualGridX,
VirtualGridY: last.VirtualGridY, VirtualGridY: last.VirtualGridY,
Dir: &Direction{ DirX: last.DirX,
Dx: last.Dir.Dx, DirY: last.DirY,
Dy: last.Dir.Dy,
},
ColliderRadius: last.ColliderRadius, ColliderRadius: last.ColliderRadius,
Speed: last.Speed, Speed: last.Speed,
BattleState: last.BattleState, BattleState: last.BattleState,

View File

@ -4,7 +4,6 @@ import (
. "battle_srv/protos" . "battle_srv/protos"
"battle_srv/storage" "battle_srv/storage"
. "dnmshared" . "dnmshared"
. "dnmshared/sharedprotos"
"fmt" "fmt"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -64,12 +63,7 @@ func GetPlayerById(id int) (*Player, error) {
func getPlayer(cond sq.Eq) (*Player, error) { func getPlayer(cond sq.Eq) (*Player, error) {
p := Player{} p := Player{}
pd := PlayerDownsync{ pd := PlayerDownsync{}
Dir: &Direction{
Dx: 0,
Dy: 0,
},
}
query, args, err := sq.Select("*").From("player").Where(cond).Limit(1).ToSql() query, args, err := sq.Select("*").From("player").Where(cond).Limit(1).ToSql()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -54,6 +54,7 @@ const (
COLLISION_PLAYER_INDEX_PREFIX = (1 << 17) COLLISION_PLAYER_INDEX_PREFIX = (1 << 17)
COLLISION_BARRIER_INDEX_PREFIX = (1 << 16) COLLISION_BARRIER_INDEX_PREFIX = (1 << 16)
COLLISION_BULLET_INDEX_PREFIX = (1 << 15)
) )
const ( const (
@ -61,6 +62,13 @@ const (
MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2 MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED = -2
) )
const (
ATK_CHARACTER_STATE_IDLE1 = 0
ATK_CHARACTER_STATE_WALKING = 1
ATK_CHARACTER_STATE_ATK1 = 2
ATK_CHARACTER_STATE_ATKED1 = 3
)
// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged. // These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged.
var DIRECTION_DECODER = [][]int32{ var DIRECTION_DECODER = [][]int32{
{0, 0}, {0, 0},
@ -165,7 +173,8 @@ type Room struct {
LastRenderFrameIdTriggeredAt int64 LastRenderFrameIdTriggeredAt int64
PlayerDefaultSpeed int32 PlayerDefaultSpeed int32
BattleColliderInfo // Compositing to send centralized magic numbers BulletBattleLocalIdCounter int32
BattleColliderInfo // Compositing to send centralized magic numbers
} }
func (pR *Room) updateScore() { func (pR *Room) updateScore() {
@ -266,8 +275,8 @@ func (pR *Room) ChooseStage() error {
panic(err) panic(err)
} }
// Obtain the content of `gidBoundariesMapInB2World`. // Obtain the content of `gidBoundariesMap`.
gidBoundariesMapInB2World := make(map[int]StrToPolygon2DListMap, 0) gidBoundariesMap := make(map[int]StrToPolygon2DListMap, 0)
for _, tileset := range pTmxMapIns.Tilesets { for _, tileset := range pTmxMapIns.Tilesets {
relativeTsxFilePath := fmt.Sprintf("%s/%s", filepath.Join(pwd, relativePathForChosenStage), tileset.Source) // Note that "TmxTileset.Source" can be a string of "relative path". relativeTsxFilePath := fmt.Sprintf("%s/%s", filepath.Join(pwd, relativePathForChosenStage), tileset.Source) // Note that "TmxTileset.Source" can be a string of "relative path".
absTsxFilePath, err := filepath.Abs(relativeTsxFilePath) absTsxFilePath, err := filepath.Abs(relativeTsxFilePath)
@ -283,10 +292,10 @@ func (pR *Room) ChooseStage() error {
panic(err) panic(err)
} }
DeserializeTsxToColliderDict(pTmxMapIns, byteArrOfTsxFile, int(tileset.FirstGid), gidBoundariesMapInB2World) DeserializeTsxToColliderDict(pTmxMapIns, byteArrOfTsxFile, int(tileset.FirstGid), gidBoundariesMap)
} }
stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, strToVec2DListMap, strToPolygon2DListMap, err := ParseTmxLayersAndGroups(pTmxMapIns, gidBoundariesMapInB2World) stageDiscreteW, stageDiscreteH, stageTileW, stageTileH, strToVec2DListMap, strToPolygon2DListMap, err := ParseTmxLayersAndGroups(pTmxMapIns, gidBoundariesMap)
if nil != err { if nil != err {
panic(err) panic(err)
} }
@ -352,6 +361,7 @@ func (pR *Room) InputsBufferString(allDetails bool) string {
break break
} }
f := tmp.(*InputFrameDownsync) f := tmp.(*InputFrameDownsync)
//s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, &inputList: %p, confirmedList: %v}", f.InputFrameId, f.InputList, &(f.InputList), f.ConfirmedList))
s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList)) s = append(s, fmt.Sprintf("{inputFrameId: %v, inputList: %v, confirmedList: %v}", f.InputFrameId, f.InputList, f.ConfirmedList))
} }
@ -617,7 +627,6 @@ func (pR *Room) OnBattleCmdReceived(pReq *WsReq) {
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false))) Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
continue continue
} }
bufIndex := pR.toDiscreteInputsBufferIndex(clientInputFrameId, pReq.JoinIndex) bufIndex := pR.toDiscreteInputsBufferIndex(clientInputFrameId, pReq.JoinIndex)
pR.DiscreteInputsBuffer.Store(bufIndex, inputFrameUpsync) pR.DiscreteInputsBuffer.Store(bufIndex, inputFrameUpsync)
@ -762,6 +771,7 @@ func (pR *Room) Dismiss() {
func (pR *Room) OnDismissed() { func (pR *Room) OnDismissed() {
// Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference. // Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference.
pR.BulletBattleLocalIdCounter = 0
pR.WorldToVirtualGridRatio = float64(1000) pR.WorldToVirtualGridRatio = float64(1000)
pR.VirtualGridToWorldRatio = float64(1.0) / pR.WorldToVirtualGridRatio // this is a one-off computation, should avoid division in iterations pR.VirtualGridToWorldRatio = float64(1.0) / pR.WorldToVirtualGridRatio // this is a one-off computation, should avoid division in iterations
pR.SpAtkLookupFrames = 5 pR.SpAtkLookupFrames = 5
@ -773,9 +783,10 @@ func (pR *Room) OnDismissed() {
pR.PlayerSignalToCloseDict = make(map[int32]SignalToCloseConnCbType) pR.PlayerSignalToCloseDict = make(map[int32]SignalToCloseConnCbType)
pR.JoinIndexBooleanArr = make([]bool, pR.Capacity) pR.JoinIndexBooleanArr = make([]bool, pR.Capacity)
pR.Barriers = make(map[int32]*Barrier) pR.Barriers = make(map[int32]*Barrier)
pR.InputsBuffer = NewRingBuffer(1024) pR.RenderCacheSize = 512
pR.RenderFrameBuffer = NewRingBuffer(pR.RenderCacheSize)
pR.DiscreteInputsBuffer = sync.Map{} pR.DiscreteInputsBuffer = sync.Map{}
pR.RenderFrameBuffer = NewRingBuffer(1024) pR.InputsBuffer = NewRingBuffer((pR.RenderCacheSize >> 2) + 1)
pR.LastAllConfirmedInputFrameId = -1 pR.LastAllConfirmedInputFrameId = -1
pR.LastAllConfirmedInputFrameIdWithChange = -1 pR.LastAllConfirmedInputFrameIdWithChange = -1
@ -795,6 +806,34 @@ func (pR *Room) OnDismissed() {
pR.MaxChasingRenderFramesPerUpdate = 8 pR.MaxChasingRenderFramesPerUpdate = 8
pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work! pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work!
punchSkillId := int32(1)
if _, existent := pR.MeleeSkillConfig[punchSkillId]; !existent {
pR.MeleeSkillConfig = make(map[int32]*MeleeBullet, 0)
pR.MeleeSkillConfig[punchSkillId] = &MeleeBullet{
// for offender
StartupFrames: int32(18),
ActiveFrames: int32(42),
RecoveryFrames: int32(61), // usually but not always "startupFrames+activeFrames", I hereby set it to be 1 frame more than the actual animation to avoid critical transition, i.e. when the animation is 1 frame from ending but "rdfPlayer.framesToRecover" is already counted 0 and the player triggers an other same attack, making an effective bullet trigger but no animation is played due to same animName is still playing
RecoveryFramesOnBlock: int32(61),
RecoveryFramesOnHit: int32(61),
Moveforward: &Vec2D{
X: 0,
Y: 0,
},
HitboxOffset: float64(12.0), // should be about the radius of the PlayerCollider
HitboxSize: &Vec2D{
X: float64(45.0),
Y: float64(32.0),
},
// for defender
HitStunFrames: int32(18),
BlockStunFrames: int32(9),
Pushback: float64(22.0),
ReleaseTriggerType: int32(1), // 1: rising-edge, 2: falling-edge
Damage: int32(5),
}
}
pR.ChooseStage() pR.ChooseStage()
pR.EffectivePlayerCount = 0 pR.EffectivePlayerCount = 0
@ -1037,7 +1076,10 @@ func (pR *Room) prefabInputFrameDownsync(inputFrameId int32) *InputFrameDownsync
panic(fmt.Sprintf("Error prefabbing inputFrameDownsync: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false))) panic(fmt.Sprintf("Error prefabbing inputFrameDownsync: roomId=%v, InputsBuffer=%v", pR.Id, pR.InputsBufferString(false)))
} }
prevInputFrameDownsync := tmp.(*InputFrameDownsync) prevInputFrameDownsync := tmp.(*InputFrameDownsync)
currInputList := prevInputFrameDownsync.InputList // Would be a clone of the values currInputList := make([]uint64, pR.Capacity) // Would be a clone of the values
for i, _ := range currInputList {
currInputList[i] = (prevInputFrameDownsync.InputList[i] & uint64(15)) // Don't predict attack input
}
currInputFrameDownsync = &InputFrameDownsync{ currInputFrameDownsync = &InputFrameDownsync{
InputFrameId: inputFrameId, InputFrameId: inputFrameId,
InputList: currInputList, InputList: currInputList,
@ -1146,13 +1188,6 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
} }
nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap) nextRenderFrame := pR.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, pR.CollisionSysMap)
// Update in the latest player pointers
for playerId, playerDownsync := range nextRenderFrame.Players {
pR.Players[playerId].VirtualGridX = playerDownsync.VirtualGridX
pR.Players[playerId].VirtualGridY = playerDownsync.VirtualGridY
pR.Players[playerId].Dir.Dx = playerDownsync.Dir.Dx
pR.Players[playerId].Dir.Dy = playerDownsync.Dir.Dy
}
pR.RenderFrameBuffer.Put(nextRenderFrame) pR.RenderFrameBuffer.Put(nextRenderFrame)
pR.CurDynamicsRenderFrameId++ pR.CurDynamicsRenderFrameId++
} }
@ -1160,22 +1195,28 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
// TODO: Write unit-test for this function to compare with its frontend counter part // TODO: Write unit-test for this function to compare with its frontend counter part
func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame { func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame {
// TODO: Derive "nextRenderFramePlayers[*].CharacterState" as the frontend counter-part!
nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity) nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity)
// Make a copy first // Make a copy first
for playerId, currPlayerDownsync := range currRenderFrame.Players { for playerId, currPlayerDownsync := range currRenderFrame.Players {
nextRenderFramePlayers[playerId] = &PlayerDownsync{ nextRenderFramePlayers[playerId] = &PlayerDownsync{
Id: playerId, Id: playerId,
VirtualGridX: currPlayerDownsync.VirtualGridX, VirtualGridX: currPlayerDownsync.VirtualGridX,
VirtualGridY: currPlayerDownsync.VirtualGridY, VirtualGridY: currPlayerDownsync.VirtualGridY,
Dir: &Direction{ DirX: currPlayerDownsync.DirX,
Dx: currPlayerDownsync.Dir.Dx, DirY: currPlayerDownsync.DirY,
Dy: currPlayerDownsync.Dir.Dy, CharacterState: currPlayerDownsync.CharacterState,
}, Speed: currPlayerDownsync.Speed,
Speed: currPlayerDownsync.Speed, BattleState: currPlayerDownsync.BattleState,
BattleState: currPlayerDownsync.BattleState, Score: currPlayerDownsync.Score,
Score: currPlayerDownsync.Score, Removed: currPlayerDownsync.Removed,
Removed: currPlayerDownsync.Removed, JoinIndex: currPlayerDownsync.JoinIndex,
JoinIndex: currPlayerDownsync.JoinIndex, FramesToRecover: currPlayerDownsync.FramesToRecover - 1,
Hp: currPlayerDownsync.Hp,
MaxHp: currPlayerDownsync.MaxHp,
}
if nextRenderFramePlayers[playerId].FramesToRecover < 0 {
nextRenderFramePlayers[playerId].FramesToRecover = 0
} }
} }
@ -1183,29 +1224,159 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
Id: currRenderFrame.Id + 1, Id: currRenderFrame.Id + 1,
Players: nextRenderFramePlayers, Players: nextRenderFramePlayers,
CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos), CountdownNanos: (pR.BattleDurationNanos - int64(currRenderFrame.Id)*pR.RollbackEstimatedDtNanos),
MeleeBullets: make([]*MeleeBullet, 0), // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation?
}
bulletPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order
effPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order
// Reset playerCollider position from the "virtual grid position"
for playerId, player := range pR.Players {
joinIndex := player.JoinIndex
bulletPushbacks[joinIndex-1].X, bulletPushbacks[joinIndex-1].Y = float64(0), float64(0)
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
currPlayerDownsync := currRenderFrame.Players[playerId]
newVx, newVy := currPlayerDownsync.VirtualGridX, currPlayerDownsync.VirtualGridY
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio)
}
// Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision
bulletColliders := make(map[int32]*resolv.Object, 0) // Will all be removed at the end of `applyInputFrameDownsyncDynamicsOnSingleRenderFrame` due to the need for being rollback-compatible
removedBulletsAtCurrFrame := make(map[int32]int32, 0)
for _, meleeBullet := range currRenderFrame.MeleeBullets {
if (meleeBullet.OriginatedRenderFrameId+meleeBullet.StartupFrames <= currRenderFrame.Id) && (meleeBullet.OriginatedRenderFrameId+meleeBullet.StartupFrames+meleeBullet.ActiveFrames > currRenderFrame.Id) {
collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId
collisionOffenderIndex := COLLISION_PLAYER_INDEX_PREFIX + meleeBullet.OffenderJoinIndex
offenderCollider := collisionSysMap[collisionOffenderIndex]
offender := currRenderFrame.Players[meleeBullet.OffenderPlayerId]
xfac, yfac := float64(1.0), float64(0) // By now, straight Punch offset doesn't respect "y-axis"
if 0 > offender.DirX {
xfac = float64(-1.0)
}
bulletCx, bulletCy := offenderCollider.X+xfac*meleeBullet.HitboxOffset, offenderCollider.Y+yfac*meleeBullet.HitboxOffset
newBulletCollider := GenerateRectCollider(bulletCx, bulletCy, xfac*meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "MeleeBullet")
newBulletCollider.Data = meleeBullet
collisionSysMap[collisionBulletIndex] = newBulletCollider
bulletColliders[collisionBulletIndex] = newBulletCollider
newBulletCollider.Update()
}
}
for _, bulletCollider := range bulletColliders {
shouldRemove := false
meleeBullet := bulletCollider.Data.(*MeleeBullet)
collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId
if collision := bulletCollider.Check(0, 0); collision != nil {
offender := currRenderFrame.Players[meleeBullet.OffenderPlayerId]
bulletShape := bulletCollider.Shape.(*resolv.ConvexPolygon)
for _, obj := range collision.Objects {
switch t := obj.Data.(type) {
case Player:
defenderShape := obj.Shape.(*resolv.ConvexPolygon)
if meleeBullet.OffenderPlayerId != t.Id {
if overlapped, _, _, _ := CalcPushbacks(0, 0, bulletShape, defenderShape); overlapped {
xfac := float64(1.0) // By now, straight Punch offset doesn't respect "y-axis"
if 0 > offender.DirX {
xfac = float64(-1.0)
}
bulletPushbacks[t.JoinIndex-1].X += xfac * meleeBullet.Pushback
nextRenderFramePlayers[t.Id].CharacterState = ATK_CHARACTER_STATE_ATKED1
oldFramesToRecover := nextRenderFramePlayers[t.Id].FramesToRecover
if meleeBullet.HitStunFrames > oldFramesToRecover {
nextRenderFramePlayers[t.Id].FramesToRecover = meleeBullet.HitStunFrames
}
}
}
default:
Logger.Debug(fmt.Sprintf("Bullet collided with non-player: roomId=%v, currRenderFrame.Id=%v, delayedInputFrame.Id=%v", pR.Id, currRenderFrame.Id, delayedInputFrame.InputFrameId))
}
}
shouldRemove = true
}
if shouldRemove {
removedBulletsAtCurrFrame[collisionBulletIndex] = 1
}
}
for _, meleeBullet := range currRenderFrame.MeleeBullets {
collisionBulletIndex := COLLISION_BULLET_INDEX_PREFIX + meleeBullet.BattleLocalId
if bulletCollider, existent := collisionSysMap[collisionBulletIndex]; existent {
bulletCollider.Space.Remove(bulletCollider)
delete(collisionSysMap, collisionBulletIndex)
}
if _, existent := removedBulletsAtCurrFrame[collisionBulletIndex]; existent {
continue
}
toRet.MeleeBullets = append(toRet.MeleeBullets, meleeBullet)
} }
if nil != delayedInputFrame { if nil != delayedInputFrame {
var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil
tmp := pR.InputsBuffer.GetByFrameId(pR.ConvertToInputFrameId(currRenderFrame.Id-1, pR.InputDelayFrames))
if nil != tmp {
delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync)
}
inputList := delayedInputFrame.InputList inputList := delayedInputFrame.InputList
effPushbacks := make([]Vec2D, pR.Capacity) // Guaranteed determinism regardless of traversal order // Process player inputs
for playerId, player := range pR.Players { for playerId, player := range pR.Players {
joinIndex := player.JoinIndex joinIndex := player.JoinIndex
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
currPlayerDownsync := currRenderFrame.Players[playerId]
decodedInput := pR.decodeInput(inputList[joinIndex-1])
proposedVirtualGridDx, proposedVirtualGridDy := (decodedInput.Dx + decodedInput.Dx*currPlayerDownsync.Speed), (decodedInput.Dy + decodedInput.Dy*currPlayerDownsync.Speed)
newVx, newVy := (currPlayerDownsync.VirtualGridX + proposedVirtualGridDx), (currPlayerDownsync.VirtualGridY + proposedVirtualGridDy)
// Reset playerCollider position from the "virtual grid position"
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex] playerCollider := collisionSysMap[collisionPlayerIndex]
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderAnchorPos(newVx, newVy, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.VirtualGridToWorldRatio) thatPlayerInNextFrame := nextRenderFramePlayers[playerId]
if 0 < thatPlayerInNextFrame.FramesToRecover {
// No need to process inputs for this player, but there might be bullet pushbacks on this player
playerCollider.X += bulletPushbacks[joinIndex-1].X
playerCollider.Y += bulletPushbacks[joinIndex-1].Y
// Update in the collision system
playerCollider.Update()
continue
}
currPlayerDownsync := currRenderFrame.Players[playerId]
decodedInput := pR.decodeInput(inputList[joinIndex-1])
prevBtnALevel := int32(0)
if nil != delayedInputFrameForPrevRenderFrame {
prevDecodedInput := pR.decodeInput(delayedInputFrameForPrevRenderFrame.InputList[joinIndex-1])
prevBtnALevel = prevDecodedInput.BtnALevel
}
if decodedInput.BtnALevel > prevBtnALevel {
punchSkillId := int32(1)
punchConfig := pR.MeleeSkillConfig[punchSkillId]
var newMeleeBullet MeleeBullet = *punchConfig
newMeleeBullet.BattleLocalId = pR.BulletBattleLocalIdCounter
pR.BulletBattleLocalIdCounter += 1
newMeleeBullet.OffenderJoinIndex = joinIndex
newMeleeBullet.OffenderPlayerId = playerId
newMeleeBullet.OriginatedRenderFrameId = currRenderFrame.Id
toRet.MeleeBullets = append(toRet.MeleeBullets, &newMeleeBullet)
thatPlayerInNextFrame.FramesToRecover = newMeleeBullet.RecoveryFrames
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_ATK1
Logger.Info(fmt.Sprintf("roomId=%v, playerId=%v triggered a rising-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId))
} else if decodedInput.BtnALevel < prevBtnALevel {
Logger.Info(fmt.Sprintf("roomId=%v, playerId=%v triggered a falling-edge of btnA at currRenderFrame.id=%v, delayedInputFrame.id=%v", pR.Id, playerId, currRenderFrame.Id, delayedInputFrame.InputFrameId))
} else {
// No bullet trigger, process movement inputs
if 0 != decodedInput.Dx || 0 != decodedInput.Dy {
thatPlayerInNextFrame.DirX = decodedInput.Dx
thatPlayerInNextFrame.DirY = decodedInput.Dy
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING
} else {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
}
}
movementX, movementY := VirtualGridToWorldPos(decodedInput.Dx+decodedInput.Dx*currPlayerDownsync.Speed, decodedInput.Dy+decodedInput.Dy*currPlayerDownsync.Speed, pR.VirtualGridToWorldRatio)
playerCollider.X += movementX
playerCollider.Y += movementY
// Update in the collision system // Update in the collision system
playerCollider.Update() playerCollider.Update()
if 0 != decodedInput.Dx || 0 != decodedInput.Dy {
nextRenderFramePlayers[playerId].Dir.Dx = decodedInput.Dx
nextRenderFramePlayers[playerId].Dir.Dy = decodedInput.Dy
}
} }
// handle pushbacks upon collision after all movements treated as simultaneous // handle pushbacks upon collision after all movements treated as simultaneous
@ -1235,8 +1406,8 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF
// Update "virtual grid position" // Update "virtual grid position"
newVx, newVy := PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio) newVx, newVy := PolygonColliderAnchorToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, player.ColliderRadius, player.ColliderRadius, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, pR.WorldToVirtualGridRatio)
nextRenderFramePlayers[playerId].VirtualGridX = newVx thatPlayerInNextFrame := nextRenderFramePlayers[playerId]
nextRenderFramePlayers[playerId].VirtualGridY = newVy thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = newVx, newVy
} }
Logger.Debug(fmt.Sprintf("After applyInputFrameDownsyncDynamicsOnSingleRenderFrame: currRenderFrame.Id=%v, inputList=%v, currRenderFrame.Players=%v, nextRenderFramePlayers=%v", currRenderFrame.Id, inputList, currRenderFrame.Players, nextRenderFramePlayers)) Logger.Debug(fmt.Sprintf("After applyInputFrameDownsyncDynamicsOnSingleRenderFrame: currRenderFrame.Id=%v, inputList=%v, currRenderFrame.Players=%v, nextRenderFramePlayers=%v", currRenderFrame.Id, inputList, currRenderFrame.Players, nextRenderFramePlayers))
@ -1267,6 +1438,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) {
for _, player := range pR.Players { for _, player := range pR.Players {
wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio) wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio)
playerCollider := GenerateRectCollider(wx, wy, player.ColliderRadius*2, player.ColliderRadius*2, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player") playerCollider := GenerateRectCollider(wx, wy, player.ColliderRadius*2, player.ColliderRadius*2, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player")
playerCollider.Data = player
space.Add(playerCollider) space.Add(playerCollider)
// Keep track of the collider in "pR.CollisionSysMap" // Keep track of the collider in "pR.CollisionSysMap"
joinIndex := player.JoinIndex joinIndex := player.JoinIndex

File diff suppressed because it is too large Load Diff

View File

@ -271,6 +271,7 @@ func Serve(c *gin.Context) {
VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio, VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio,
SpAtkLookupFrames: pRoom.SpAtkLookupFrames, SpAtkLookupFrames: pRoom.SpAtkLookupFrames,
RenderCacheSize: pRoom.RenderCacheSize,
} }
resp := &pb.WsResp{ resp := &pb.WsResp{

View File

@ -20,6 +20,10 @@ func ConvexPolygonStr(body *resolv.ConvexPolygon) string {
func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { func GenerateRectCollider(origX, origY, w, h, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object {
cx, cy := WorldToPolygonColliderAnchorPos(origX, origY, w*0.5, h*0.5, spaceOffsetX, spaceOffsetY) cx, cy := WorldToPolygonColliderAnchorPos(origX, origY, w*0.5, h*0.5, spaceOffsetX, spaceOffsetY)
return GenerateRectColliderInCollisionSpace(cx, cy, w, h, tag)
}
func GenerateRectColliderInCollisionSpace(cx, cy, w, h float64, tag string) *resolv.Object {
collider := resolv.NewObject(cx, cy, w, h, tag) collider := resolv.NewObject(cx, cy, w, h, tag)
shape := resolv.NewRectangle(0, 0, w, h) shape := resolv.NewRectangle(0, 0, w, h)
collider.SetShape(shape) collider.SetShape(shape)

Binary file not shown.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "8f2f76c7-649c-414a-80be-b2daef4ed580",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"SubTexture":[{"y":50,"frameX":-2,"frameY":-2,"width":19,"frameWidth":23,"height":19,"name":"biu","frameHeight":22,"x":1},{"width":9,"y":50,"height":14,"name":"rightArm","x":42},{"y":34,"frameX":-6,"frameY":0,"width":20,"frameWidth":29,"height":32,"name":"yinmoqe00","frameHeight":32,"x":88},{"y":1,"frameX":0,"frameY":0,"width":33,"frameWidth":34,"height":39,"name":"body","frameHeight":41,"x":53},{"width":9,"y":56,"height":13,"name":"rightShoulder","x":74},{"y":50,"frameX":0,"frameY":0,"width":18,"frameWidth":19,"height":17,"name":"rightFrontArm","frameHeight":18,"x":22},{"width":14,"y":50,"height":14,"name":"rightHand","x":110},{"width":12,"y":42,"height":12,"name":"leftArm","x":74},{"width":13,"y":66,"height":12,"name":"leftShoulder","x":110},{"y":42,"frameX":-1,"frameY":0,"width":19,"frameWidth":20,"height":21,"name":"leftFrontArm","frameHeight":21,"x":53},{"width":50,"y":1,"height":47,"name":"head2","x":1},{"y":1,"frameX":-1,"frameY":0,"width":32,"frameWidth":33,"height":31,"name":"head","frameHeight":32,"x":88},{"width":16,"y":34,"height":14,"name":"leftHand","x":110},{"y":1,"frameX":-2,"frameY":-3,"width":2,"frameWidth":8,"height":2,"name":"huomiao01","frameHeight":8,"x":122}],"width":128,"height":128,"name":"SoldierWaterGhost","imagePath":"SoldierWaterGhost_tex.png"}

View File

@ -0,0 +1,7 @@
{
"ver": "1.0.0",
"uuid": "2f5ba371-9006-4e2d-8dd4-df91fcea1df2",
"atlasJson": "{\"SubTexture\":[{\"y\":50,\"frameX\":-2,\"frameY\":-2,\"width\":19,\"frameWidth\":23,\"height\":19,\"name\":\"biu\",\"frameHeight\":22,\"x\":1},{\"width\":9,\"y\":50,\"height\":14,\"name\":\"rightArm\",\"x\":42},{\"y\":34,\"frameX\":-6,\"frameY\":0,\"width\":20,\"frameWidth\":29,\"height\":32,\"name\":\"yinmoqe00\",\"frameHeight\":32,\"x\":88},{\"y\":1,\"frameX\":0,\"frameY\":0,\"width\":33,\"frameWidth\":34,\"height\":39,\"name\":\"body\",\"frameHeight\":41,\"x\":53},{\"width\":9,\"y\":56,\"height\":13,\"name\":\"rightShoulder\",\"x\":74},{\"y\":50,\"frameX\":0,\"frameY\":0,\"width\":18,\"frameWidth\":19,\"height\":17,\"name\":\"rightFrontArm\",\"frameHeight\":18,\"x\":22},{\"width\":14,\"y\":50,\"height\":14,\"name\":\"rightHand\",\"x\":110},{\"width\":12,\"y\":42,\"height\":12,\"name\":\"leftArm\",\"x\":74},{\"width\":13,\"y\":66,\"height\":12,\"name\":\"leftShoulder\",\"x\":110},{\"y\":42,\"frameX\":-1,\"frameY\":0,\"width\":19,\"frameWidth\":20,\"height\":21,\"name\":\"leftFrontArm\",\"frameHeight\":21,\"x\":53},{\"width\":50,\"y\":1,\"height\":47,\"name\":\"head2\",\"x\":1},{\"y\":1,\"frameX\":-1,\"frameY\":0,\"width\":32,\"frameWidth\":33,\"height\":31,\"name\":\"head\",\"frameHeight\":32,\"x\":88},{\"width\":16,\"y\":34,\"height\":14,\"name\":\"leftHand\",\"x\":110},{\"y\":1,\"frameX\":-2,\"frameY\":-3,\"width\":2,\"frameWidth\":8,\"height\":2,\"name\":\"huomiao01\",\"frameHeight\":8,\"x\":122}],\"width\":128,\"height\":128,\"name\":\"SoldierWaterGhost\",\"imagePath\":\"SoldierWaterGhost_tex.png\"}",
"texture": "62ea1a78-b096-4b8c-afb4-8edd4be142f7",
"subMetas": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,34 @@
{
"ver": "2.3.3",
"uuid": "62ea1a78-b096-4b8c-afb4-8edd4be142f7",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"platformSettings": {},
"subMetas": {
"SoldierWaterGhost_tex": {
"ver": "1.0.4",
"uuid": "e7b96c0e-a9b8-40eb-8568-73bd2803cd31",
"rawTextureUuid": "62ea1a78-b096-4b8c-afb4-8edd4be142f7",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": -0.5,
"offsetY": 24.5,
"trimX": 1,
"trimY": 1,
"width": 125,
"height": 77,
"rawWidth": 128,
"rawHeight": 128,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}

View File

@ -1,17 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="64" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="12"> <map version="1.2" tiledversion="1.2.3" orientation="orthogonal" renderorder="right-down" width="128" height="128" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="39">
<tileset firstgid="1" source="tiles0.tsx"/> <tileset firstgid="1" source="tiles0.tsx"/>
<tileset firstgid="65" source="tiles1.tsx"/> <tileset firstgid="65" source="tiles1.tsx"/>
<layer id="1" name="Ground" width="64" height="64"> <tileset firstgid="129" source="tiles2.tsx"/>
<layer id="2" name="Ground" width="128" height="128">
<data encoding="base64" compression="zlib"> <data encoding="base64" compression="zlib">
eJzt0jEOwjAUREGLkiLmALRU3In7HwMKkFCUCCwiNrKnmCbVvh8fSikHAAAA/qruYEOr84Zd+vM9re0v+vXr1z9S/7f0l3KZ9dXZ91r67QeAXxyf0jv0Z/tvO9iS7F+T3ucG+Xuk96RvkN4CMJLaIL0VttLy7r19euHdA+zX6YP0vlR3z/3ax2tfarw+TAO0L/VPb3ruXvv3o7z7lv70zuQN0vtgz+6DDSsZ eJzt2q1u22AYhuEo1UhBtYFWKh/bmRRMRYNFPZPxwcKi7jznSInkWk7teG5ex88FLhQD570d5/PP1WazuQIAAAAAAAAAAAAAAAAAgAmu96r3g/N5bjn0fw5U3aGy/3bv0P+p8XDEdoX0f9//GP3Xp93/YaD/9QJa6f95/fvov26773671zeb7jmhupX+5+2fQH/9qztU9v8yQXUz/fXXX3/9a/ovgf7666+//vrrf1n9/4zc7tue/uvq/9Z4PfLZ7vnz38ZP/cv7T53p4Z7i7/+kv/7667/bl/sO/dff/77T3+9ff/3r+38dMGf/Nv1r+w917+v/0vh1okN/67/l9B/b/nFj/X8pxvYf236r/0UZ6t/X+Efj5kj7bv+uxxPpX9e/r/1NS7f7Z/S/a3zXfzH9Pzrvt/vPTf/l9p+yRjyV/ufvP+YY6M5yzr5D5uy/XUCLJfafa8YfmdJe//X0r6Z/9jGgv/7661/doqp/9fyrpV//Vc+/mv7Z9M+mfzb9s+mfTf9s+mfTP5v+2fTPpn82/bPpn03/bPpn0z+b/tn0z6Z/Nv2z6Z/N+9/Z9M+mfzb//9n0z6Z/Nv2z6Z9N/2z6Z9M/m/7Z0vtT3wEAAAAAAAAAAAAAAAA4v39IY4NC
</data> </data>
</layer> </layer>
<objectgroup id="1" name="PlayerStartingPos"> <objectgroup id="1" name="PlayerStartingPos">
<object id="135" x="512" y="512"> <object id="135" x="1090" y="833.667">
<point/> <point/>
</object> </object>
<object id="137" x="640" y="640"> <object id="137" x="1215" y="830.5">
<point/> <point/>
</object> </object>
</objectgroup> </objectgroup>
@ -19,71 +20,149 @@
<properties> <properties>
<property name="type" value="barrier_and_shelter"/> <property name="type" value="barrier_and_shelter"/>
</properties> </properties>
<object id="1" x="400" y="224.5"> <object id="8" x="648.242" y="480.606">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,141.5 17,141.5 17,-1"/>
</object>
<object id="2" x="559.5" y="207.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 1,158.5 17,158.5 17,0"/>
</object>
<object id="3" x="448.5" y="353">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,14.5 -49,14.5 -49,-1.5"/>
</object>
<object id="4" x="577.5" y="351.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -98,1 -98,16 -1,15.5"/>
</object>
<object id="5" x="449.333" y="654.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -178.667,0.666667 -178.667,17.3333 -0.666667,17.3333"/>
</object>
<object id="6" x="432.5" y="703.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -191.667,0.666667 -191.667,17.3333 -0.715174,17.3333"/>
</object>
<object id="7" x="400" y="751">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -191.667,0.666667 -191.667,17.3333 -0.715174,17.3333"/>
</object>
<object id="8" x="-9.33333" y="-13.3333">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/> <polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object> </object>
<object id="9" x="-9.33333" y="1014.67"> <object id="9" x="650.667" y="1604.67">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/> <polyline points="0,0 0,18.6667 1041.33,21.3333 1041.33,-1.33333"/>
</object> </object>
<object id="10" x="-14" y="-40"> <object id="10" x="634.485" y="505.455">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 4,1110 24,1110 24,-8"/> <polyline points="0,0 4,1110 24,1110 24,-8"/>
</object> </object>
<object id="11" x="1014" y="-42"> <object id="11" x="1677.64" y="501.333">
<properties> <properties>
<property name="boundary_type" value="barrier"/> <property name="boundary_type" value="barrier"/>
</properties> </properties>
<polyline points="0,0 4,1110 24,1110 24,-8"/> <polyline points="0,0 4,1110 24,1110 24,-8"/>
</object> </object>
<object id="14" x="688.667" y="464">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -0.666667,78 33.3333,78 32,-0.666667"/>
</object>
<object id="15" x="833.333" y="495.333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -112,1.33333 -111.333,44.6667 -1.33333,44.6667"/>
</object>
<object id="17" x="832" y="574">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -67.3333,0 -67.3333,-76.6667 0.666667,-76"/>
</object>
<object id="18" x="865.333" y="606.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -210,-0.666667 -210,143.333 0,142.667"/>
</object>
<object id="19" x="754.667" y="1055.33">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="-2,1.33333 -100,0.666667 -97.3333,-454 -9.33333,-451.333"/>
</object>
<object id="20" x="769.333" y="747.333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -115.333,0.666667 -114.667,160.667 -2,162"/>
</object>
<object id="21" x="768" y="960.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -18,0.666667 -18.6667,95.3333 0,95.3333"/>
</object>
<object id="23" x="786" y="1058.67">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,-52.6667 -20,-52 -19.3333,-1.33333"/>
</object>
<object id="24" x="1118.67" y="749.333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -0.666667,-94 -254,-93.3333 -256.667,1.33333"/>
</object>
<object id="25" x="1168" y="975.333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,16.6667 224.667,17.3333 225.333,-2"/>
</object>
<object id="28" x="1394.67" y="958">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -210.667,1.33333 -210.667,17.3333 -2,18"/>
</object>
<object id="29" x="1119" y="654.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,63.5 272.5,65 273,-1"/>
</object>
<object id="30" x="1136.5" y="717.5">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -0.5,17 255,17.5 255,1.5"/>
</object>
<object id="31" x="1152" y="735.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,15 80.3333,15.3333 80.3333,-2"/>
</object>
<object id="32" x="1280.67" y="734.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -0.666667,64.3333 48,65 48,0"/>
</object>
<object id="34" x="1329" y="783">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 63.3333,0.333333 63,-48.3333 -0.666667,-48.3333"/>
</object>
<object id="35" x="1296.67" y="799">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -0.666667,31.3333 31.3333,31.6667 31.3333,-0.333333"/>
</object>
<object id="36" x="1280" y="848">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0.333333,62 112,63 111.667,-1.33333"/>
</object>
<object id="37" x="1392.33" y="911.333">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 -81.3333,-0.666667 -81,45 -0.666667,46.3333"/>
</object>
<object id="38" x="1344.33" y="800.667">
<properties>
<property name="boundary_type" value="barrier"/>
</properties>
<polyline points="0,0 0,46.3333 47,46.3333 47,-1"/>
</object>
</objectgroup> </objectgroup>
</map> </map>

View File

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

View File

@ -0,0 +1,5 @@
{
"ver": "2.0.0",
"uuid": "2e53500f-f9c8-4b5b-b74c-f2adbc2ec34d",
"subMetas": {}
}

View File

@ -4,6 +4,98 @@ option go_package = "battle_srv/protos"; // here "./" corresponds to the "--go_o
package protos; package protos;
import "geometry.proto"; // The import path here is only w.r.t. the proto file, not the Go package. import "geometry.proto"; // The import path here is only w.r.t. the proto file, not the Go package.
message PlayerDownsync {
int32 id = 1;
int32 virtualGridX = 2;
int32 virtualGridY = 3;
int32 dirX = 4;
int32 dirY = 5;
int32 speed = 6; // in terms of virtual grid units
int32 battleState = 7;
int32 joinIndex = 8;
double colliderRadius = 9;
bool removed = 10;
int32 score = 11;
int32 lastMoveGmtMillis = 12;
int32 framesToRecover = 13;
int32 hp = 14;
int32 maxHp = 15;
int32 characterState = 16;
string name = 17;
string displayName = 18;
string avatar = 19;
}
message InputFrameDecoded {
int32 dx = 1;
int32 dy = 2;
int32 btnALevel = 3;
}
message InputFrameUpsync {
int32 inputFrameId = 1;
uint64 encoded = 2;
}
message InputFrameDownsync {
int32 inputFrameId = 1;
repeated uint64 inputList = 2; // Indexed by "joinIndex", we try to compress the "single player input" into 1 word (64-bit for 64-bit Golang runtime) because atomic compare-and-swap only works on 1 word. Although CAS on custom struct is possible in Golang 1.19 https://pkg.go.dev/sync/atomic@go1.19.1#Value.CompareAndSwap, using a single word is still faster whenever possible.
uint64 confirmedList = 3; // Indexed by "joinIndex", same compression concern as above
}
message HeartbeatUpsync {
int64 clientTimestamp = 1;
}
message WsReq {
int32 msgId = 1;
int32 playerId = 2;
int32 act = 3;
int32 joinIndex = 4;
int32 ackingFrameId = 5;
int32 ackingInputFrameId = 6;
repeated InputFrameUpsync inputFrameUpsyncBatch = 7;
HeartbeatUpsync hb = 8;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
}
message MeleeBullet {
// Jargon reference https://www.thegamer.com/fighting-games-frame-data-explained/
// ALL lengths are in world coordinate
// for offender
int32 battleLocalId = 1;
int32 startupFrames = 2;
int32 activeFrames = 3;
int32 recoveryFrames = 4;
int32 recoveryFramesOnBlock = 5;
int32 recoveryFramesOnHit = 6;
sharedprotos.Vec2D moveforward = 7;
double hitboxOffset = 8;
sharedprotos.Vec2D hitboxSize = 9;
int32 originatedRenderFrameId = 10;
// for defender
int32 hitStunFrames = 11;
int32 blockStunFrames = 12;
double pushback = 13;
int32 releaseTriggerType = 14; // 1: rising-edge, 2: falling-edge
int32 damage = 15;
int32 offenderJoinIndex = 16;
int32 offenderPlayerId = 17;
}
message BattleColliderInfo { message BattleColliderInfo {
string stageName = 1; string stageName = 1;
map<string, sharedprotos.Vec2DList> strToVec2DListMap = 2; map<string, sharedprotos.Vec2DList> strToVec2DListMap = 2;
@ -31,69 +123,14 @@ message BattleColliderInfo {
double virtualGridToWorldRatio = 22; double virtualGridToWorldRatio = 22;
int32 spAtkLookupFrames = 23; int32 spAtkLookupFrames = 23;
} int32 renderCacheSize = 24;
message PlayerDownsync { map<int32, MeleeBullet> meleeSkillConfig = 25; // skillId -> skill
int32 id = 1;
int32 virtualGridX = 2;
int32 virtualGridY = 3;
sharedprotos.Direction dir = 4;
int32 speed = 5; // in terms of virtual grid units
int32 battleState = 6;
int32 joinIndex = 7;
double colliderRadius = 8;
bool removed = 9;
int32 score = 10;
int32 lastMoveGmtMillis = 11;
string name = 12;
string displayName = 13;
string avatar = 14;
}
message InputFrameDecoded {
int32 dx = 1;
int32 dy = 2;
int32 btnALevel = 3;
}
message InputFrameUpsync {
int32 inputFrameId = 1;
uint64 encoded = 2;
}
message InputFrameDownsync {
int32 inputFrameId = 1;
repeated uint64 inputList = 2; // Indexed by "joinIndex", we try to compress the "single player input" into 1 word (64-bit for 64-bit Golang runtime) because atomic compare-and-swap only works on 1 word. Although CAS on custom struct is possible in Golang 1.19 https://pkg.go.dev/sync/atomic@go1.19.1#Value.CompareAndSwap, using a single word is still faster whenever possible.
uint64 confirmedList = 3; // Indexed by "joinIndex", same compression concern as above
}
message HeartbeatUpsync {
int64 clientTimestamp = 1;
} }
message RoomDownsyncFrame { message RoomDownsyncFrame {
int32 id = 1; int32 id = 1;
map<int32, PlayerDownsync> players = 2; map<int32, PlayerDownsync> players = 2;
int64 countdownNanos = 3; int64 countdownNanos = 3;
} repeated MeleeBullet meleeBullets = 4; // I don't know how to mimic inheritance/composition in protobuf by far, thus using an array for each type of bullet as a compromise
message WsReq {
int32 msgId = 1;
int32 playerId = 2;
int32 act = 3;
int32 joinIndex = 4;
int32 ackingFrameId = 5;
int32 ackingInputFrameId = 6;
repeated InputFrameUpsync inputFrameUpsyncBatch = 7;
HeartbeatUpsync hb = 8;
}
message WsResp {
int32 ret = 1;
int32 echoedMsgId = 2;
int32 act = 3;
RoomDownsyncFrame rdf = 4;
repeated InputFrameDownsync inputFrameDownsyncBatch = 5;
BattleColliderInfo bciFrame = 6;
} }

View File

@ -33,14 +33,14 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 19 "__id__": 22
}, },
{ {
"__id__": 20 "__id__": 23
} }
], ],
"_prefab": { "_prefab": {
"__id__": 21 "__id__": 24
}, },
"_opacity": 255, "_opacity": 255,
"_color": { "_color": {
@ -490,12 +490,15 @@
}, },
{ {
"__id__": 15 "__id__": 15
},
{
"__id__": 18
} }
], ],
"_active": true, "_active": true,
"_components": [], "_components": [],
"_prefab": { "_prefab": {
"__id__": 18 "__id__": 21
}, },
"_opacity": 255, "_opacity": 255,
"_color": { "_color": {
@ -774,6 +777,121 @@
"fileId": "a8ZUEyoP1Ec5azSkL7Z/9h", "fileId": "a8ZUEyoP1Ec5azSkL7Z/9h",
"sync": false "sync": false
}, },
{
"__type__": "cc.Node",
"_name": "SoldierWaterGhost",
"_objFlags": 0,
"_parent": {
"__id__": 11
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 19
}
],
"_prefab": {
"__id__": 20
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 0,
"height": 0
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "dragonBones.ArmatureDisplay",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 18
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_armatureName": "SoldierWaterGhost",
"_animationName": "Idle1",
"_preCacheMode": 0,
"_cacheMode": 0,
"playTimes": -1,
"premultipliedAlpha": false,
"_armatureKey": "73b7e7d4-6ad0-41ea-9d17-1104c463aad2#2f5ba371-9006-4e2d-8dd4-df91fcea1df2",
"_accTime": 0,
"_playCount": 0,
"_frameCache": null,
"_curFrame": null,
"_playing": false,
"_armatureCache": null,
"_N$dragonAsset": {
"__uuid__": "73b7e7d4-6ad0-41ea-9d17-1104c463aad2"
},
"_N$dragonAtlasAsset": {
"__uuid__": "2f5ba371-9006-4e2d-8dd4-df91fcea1df2"
},
"_N$_defaultArmatureIndex": 0,
"_N$_animationIndex": 8,
"_N$_defaultCacheMode": 0,
"_N$timeScale": 1,
"_N$debugBones": false,
"_N$enableBatch": false,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "59bff7a2-23e1-4d69-bce7-afb37eae196a"
},
"fileId": "42Rmp/YOdMOYWzJwr3ET1h",
"sync": false
},
{ {
"__type__": "cc.PrefabInfo", "__type__": "cc.PrefabInfo",
"root": { "root": {

View File

@ -343,6 +343,7 @@
"forceBigEndianFloatingNumDecoding": false, "forceBigEndianFloatingNumDecoding": false,
"renderFrameIdLagTolerance": 4, "renderFrameIdLagTolerance": 4,
"jigglingEps1D": 0.001, "jigglingEps1D": 0.001,
"bulletTriggerEnabled": true,
"_id": "d12gkAmppNlIzqcRDELa91" "_id": "d12gkAmppNlIzqcRDELa91"
}, },
{ {
@ -522,7 +523,7 @@
"array": [ "array": [
0, 0,
0, 0,
239.32248305180272, 210.23252687912068,
0, 0,
0, 0,
0, 0,
@ -1482,7 +1483,6 @@
"zoomingListenerNode": { "zoomingListenerNode": {
"__id__": 5 "__id__": 5
}, },
"actionBtnListenerNode": null,
"stickhead": { "stickhead": {
"__id__": 25 "__id__": 25
}, },

View File

@ -440,7 +440,7 @@
"array": [ "array": [
0, 0,
0, 0,
210.4441731196186, 210.23252687912068,
0, 0,
0, 0,
0, 0,

View File

@ -277,6 +277,7 @@
"forceBigEndianFloatingNumDecoding": false, "forceBigEndianFloatingNumDecoding": false,
"renderFrameIdLagTolerance": 4, "renderFrameIdLagTolerance": 4,
"jigglingEps1D": 0.001, "jigglingEps1D": 0.001,
"bulletTriggerEnabled": true,
"_id": "4b+kZ46VhC0LCBixXEK2dk" "_id": "4b+kZ46VhC0LCBixXEK2dk"
}, },
{ {
@ -453,7 +454,7 @@
"array": [ "array": [
0, 0,
0, 0,
216.50635094610968, 210.23252687912068,
0, 0,
0, 0,
0, 0,

View File

@ -4,6 +4,7 @@ window.ATK_CHARACTER_STATE = {
Idle1: [0, "Idle1"], Idle1: [0, "Idle1"],
Walking: [1, "Walking"], Walking: [1, "Walking"],
Atk1: [2, "Atk1"], Atk1: [2, "Atk1"],
Atked1: [3, "Atked1"],
}; };
window.ATK_CHARACTER_STATE_ARR = []; window.ATK_CHARACTER_STATE_ARR = [];
@ -22,6 +23,9 @@ cc.Class({
ctor() { ctor() {
this.speciesName = null; this.speciesName = null;
this.hp = 100;
this.maxHp = 100;
this.framesToRecover = 0;
}, },
setSpecies(speciesName) { setSpecies(speciesName) {
@ -34,28 +38,29 @@ cc.Class({
onLoad() { onLoad() {
BaseCharacter.prototype.onLoad.call(this); BaseCharacter.prototype.onLoad.call(this);
this.characterState = ATK_CHARACTER_STATE.Idle1[0];
}, },
scheduleNewDirection(newScheduledDirection, forceAnimSwitch) { updateCharacterAnim(newScheduledDirection, rdfPlayer, forceAnimSwitch) {
const oldDx = this.activeDirection.dx, oldDy = this.activeDirection.dy; if (0 == rdfPlayer.framesToRecover) {
BaseCharacter.prototype.scheduleNewDirection.call(this, newScheduledDirection, forceAnimSwitch); // Update directions
if (ATK_CHARACTER_STATE.Atk1[0] == this.characterState) { if (forceAnimSwitch || null == this.activeDirection || (null != newScheduledDirection && (newScheduledDirection.dx != this.activeDirection.dx || newScheduledDirection.dy != this.activeDirection.dy))) {
return; this.activeDirection = newScheduledDirection;
} if (this.animComp && this.animComp.node) {
if (0 > newScheduledDirection.dx) {
let newCharacterState = ATK_CHARACTER_STATE.Idle1[0]; this.animComp.node.scaleX = (-1.0);
if (0 != newScheduledDirection.dx || 0 != newScheduledDirection.dy) { } else if (0 < newScheduledDirection.dx) {
newCharacterState = ATK_CHARACTER_STATE.Walking[0]; this.animComp.node.scaleX = (1.0);
} }
}
if (newCharacterState != this.characterState) {
this.characterState = newCharacterState;
const newAnimName = window.ATK_CHARACTER_STATE_ARR[newCharacterState][1];
if (newAnimName != this.animComp.animationName) {
this.animComp.playAnimation(newAnimName);
// console.log(`JoinIndex=${this.joinIndex}, Resetting anim to ${newAnimName}, dir changed: (${oldDx}, ${oldDy}) -> (${newScheduledDirection.dx}, ${newScheduledDirection.dy})`);
} }
} }
// Update per character state
let newCharacterState = rdfPlayer.characterState;
const newAnimName = window.ATK_CHARACTER_STATE_ARR[newCharacterState][1];
if (newAnimName != this.animComp.animationName) {
this.animComp.playAnimation(newAnimName);
// console.log(`JoinIndex=${this.joinIndex}, Resetting anim to ${newAnimName}, dir changed: (${oldDx}, ${oldDy}) -> (${newScheduledDirection.dx}, ${newScheduledDirection.dy})`);
}
}, },
}); });

View File

@ -23,23 +23,6 @@ module.export = cc.Class({
self.ctrl = joystickInputControllerScriptIns; self.ctrl = joystickInputControllerScriptIns;
}, },
scheduleNewDirection(newScheduledDirection, forceAnimSwitch) {
if (!newScheduledDirection) {
return;
}
if (forceAnimSwitch || null == this.activeDirection || (newScheduledDirection.dx != this.activeDirection.dx || newScheduledDirection.dy != this.activeDirection.dy)) {
this.activeDirection = newScheduledDirection;
if (this.animComp && this.animComp.node) {
if (0 > newScheduledDirection.dx) {
this.animComp.node.scaleX = (-1.0);
} else if (0 < newScheduledDirection.dx) {
this.animComp.node.scaleX = (1.0);
}
}
}
},
update(dt) {}, update(dt) {},
lateUpdate(dt) {}, lateUpdate(dt) {},

View File

@ -95,6 +95,9 @@ cc.Class({
type: cc.Float, type: cc.Float,
default: 1e-3 default: 1e-3
}, },
bulletTriggerEnabled: {
default: false
},
}, },
_inputFrameIdDebuggable(inputFrameId) { _inputFrameIdDebuggable(inputFrameId) {
@ -300,29 +303,31 @@ cc.Class({
// Clearing previous info of all players. [ENDS] // Clearing previous info of all players. [ENDS]
self.renderFrameId = 0; // After battle started self.renderFrameId = 0; // After battle started
self.bulletBattleLocalIdCounter = 0;
self.lastAllConfirmedRenderFrameId = -1; self.lastAllConfirmedRenderFrameId = -1;
self.lastAllConfirmedInputFrameId = -1; self.lastAllConfirmedInputFrameId = -1;
self.lastUpsyncInputFrameId = -1; self.lastUpsyncInputFrameId = -1;
self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch" self.chaserRenderFrameId = -1; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch"
self.recentRenderCache = new RingBuffer(1024); self.recentRenderCache = new RingBuffer(self.renderCacheSize);
self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others". self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others".
self.recentInputCache = new RingBuffer(1024); self.recentInputCache = new RingBuffer((self.renderCacheSize >> 2) + 1);
self.collisionSys = new collisions.Collisions(); self.collisionSys = new collisions.Collisions();
self.collisionBarrierIndexPrefix = (1 << 16); // For tracking the movements of barriers, though not yet actually used self.collisionBarrierIndexPrefix = (1 << 16); // For tracking the movements of barriers, though not yet actually used
self.collisionBulletIndexPrefix = (1 << 15); // For tracking the movements of bullets
self.collisionSysMap = new Map(); self.collisionSysMap = new Map();
self.transitToState(ALL_MAP_STATES.VISUAL); self.transitToState(ALL_MAP_STATES.VISUAL);
self.battleState = ALL_BATTLE_STATES.WAITING; self.battleState = ALL_BATTLE_STATES.WAITING;
self.countdownNanos = null;
if (self.countdownLabel) { if (self.countdownLabel) {
self.countdownLabel.string = ""; self.countdownLabel.string = "";
} }
self.countdownNanos = null;
if (self.findingPlayerNode) { if (self.findingPlayerNode) {
const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer"); const findingPlayerScriptIns = self.findingPlayerNode.getComponent("FindingPlayer");
findingPlayerScriptIns.init(); findingPlayerScriptIns.init();
@ -408,19 +413,7 @@ cc.Class({
/** Init required prefab ended. */ /** Init required prefab ended. */
window.handleBattleColliderInfo = function(parsedBattleColliderInfo) { window.handleBattleColliderInfo = function(parsedBattleColliderInfo) {
self.inputDelayFrames = parsedBattleColliderInfo.inputDelayFrames; Object.assign(self, parsedBattleColliderInfo);
self.inputScaleFrames = parsedBattleColliderInfo.inputScaleFrames;
self.inputFrameUpsyncDelayTolerance = parsedBattleColliderInfo.inputFrameUpsyncDelayTolerance;
self.battleDurationNanos = parsedBattleColliderInfo.battleDurationNanos;
self.rollbackEstimatedDt = parsedBattleColliderInfo.rollbackEstimatedDt;
self.rollbackEstimatedDtMillis = parsedBattleColliderInfo.rollbackEstimatedDtMillis;
self.rollbackEstimatedDtNanos = parsedBattleColliderInfo.rollbackEstimatedDtNanos;
self.maxChasingRenderFramesPerUpdate = parsedBattleColliderInfo.maxChasingRenderFramesPerUpdate;
self.spAtkLookupFrames = parsedBattleColliderInfo.spAtkLookupFrames;
self.worldToVirtualGridRatio = parsedBattleColliderInfo.worldToVirtualGridRatio;
self.virtualGridToWorldRatio = parsedBattleColliderInfo.virtualGridToWorldRatio;
const tiledMapIns = self.node.getComponent(cc.TiledMap); const tiledMapIns = self.node.getComponent(cc.TiledMap);
@ -751,17 +744,15 @@ cc.Class({
self.playersInfoNode.getComponent("PlayersInfo").clearInfo(); self.playersInfoNode.getComponent("PlayersInfo").clearInfo();
}, },
spawnPlayerNode(joinIndex, vx, vy, playerRichInfo) { spawnPlayerNode(joinIndex, vx, vy, playerDownsyncInfo) {
const self = this; const self = this;
const newPlayerNode = cc.instantiate(self.controlledCharacterPrefab) const newPlayerNode = cc.instantiate(self.controlledCharacterPrefab)
const playerScriptIns = newPlayerNode.getComponent("ControlledCharacter"); const playerScriptIns = newPlayerNode.getComponent("ControlledCharacter");
playerScriptIns.joinIndex = joinIndex;
if (1 == joinIndex) { if (1 == joinIndex) {
playerScriptIns.setSpecies("SoldierElf"); playerScriptIns.setSpecies("SoldierWaterGhost");
} else if (2 == joinIndex) { } else if (2 == joinIndex) {
playerScriptIns.setSpecies("SoldierFireGhost"); playerScriptIns.setSpecies("SoldierFireGhost");
if (0 == playerRichInfo.dir.dx && 0 == playerRichInfo.dir.dy) { if (0 == playerDownsyncInfo.dirX && 0 == playerDownsyncInfo.dirY) {
playerScriptIns.animComp.node.scaleX = (-1.0); playerScriptIns.animComp.node.scaleX = (-1.0);
} }
} }
@ -770,24 +761,25 @@ cc.Class({
newPlayerNode.setPosition(cc.v2(wpos[0], wpos[1])); newPlayerNode.setPosition(cc.v2(wpos[0], wpos[1]));
playerScriptIns.mapNode = self.node; playerScriptIns.mapNode = self.node;
const cpos = self.virtualGridToPlayerColliderPos(vx, vy, playerRichInfo); const cpos = self.virtualGridToPlayerColliderPos(vx, vy, playerDownsyncInfo);
const d = playerRichInfo.colliderRadius * 2, const d = playerDownsyncInfo.colliderRadius * 2,
x0 = cpos[0], x0 = cpos[0],
y0 = cpos[1]; y0 = cpos[1];
let pts = [[0, 0], [d, 0], [d, d], [0, d]]; let pts = [[0, 0], [d, 0], [d, d], [0, d]];
const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts); const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts);
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
newPlayerCollider.data = playerDownsyncInfo;
self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider); self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider);
safelyAddChild(self.node, newPlayerNode); safelyAddChild(self.node, newPlayerNode);
setLocalZOrder(newPlayerNode, 5); setLocalZOrder(newPlayerNode, 5);
newPlayerNode.active = true; newPlayerNode.active = true;
playerScriptIns.scheduleNewDirection({ playerScriptIns.updateCharacterAnim({
dx: playerRichInfo.dir.dx, dx: playerDownsyncInfo.dirX,
dy: playerRichInfo.dir.dy dy: playerDownsyncInfo.dirY,
}, true); }, playerDownsyncInfo, true);
return [newPlayerNode, playerScriptIns]; return [newPlayerNode, playerScriptIns];
}, },
@ -981,11 +973,12 @@ cc.Class({
const dy = (wpos[1] - playerRichInfo.node.y); const dy = (wpos[1] - playerRichInfo.node.y);
//const justJiggling = (self.jigglingEps1D >= Math.abs(dx) && self.jigglingEps1D >= Math.abs(dy)); //const justJiggling = (self.jigglingEps1D >= Math.abs(dx) && self.jigglingEps1D >= Math.abs(dy));
playerRichInfo.node.setPosition(wpos[0], wpos[1]); playerRichInfo.node.setPosition(wpos[0], wpos[1]);
playerRichInfo.virtualGridX = immediatePlayerInfo.virtualGridX; // TODO: check "rdf.players[playerId].characterState" instead, might have to play Atk/Atked anim!
playerRichInfo.virtualGridY = immediatePlayerInfo.virtualGridY;
if (null != delayedInputFrameForPrevRenderFrame) { if (null != delayedInputFrameForPrevRenderFrame) {
const decodedInput = self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[playerRichInfo.joinIndex - 1]); const decodedInput = self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[playerRichInfo.joinIndex - 1]);
playerRichInfo.scriptIns.scheduleNewDirection(decodedInput, false); playerRichInfo.scriptIns.updateCharacterAnim(decodedInput, immediatePlayerInfo, false);
} else {
playerRichInfo.scriptIns.updateCharacterAnim(null, immediatePlayerInfo, false);
} }
playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed);
}); });
@ -1008,61 +1001,190 @@ cc.Class({
// TODO: Write unit-test for this function to compare with its backend counter part // TODO: Write unit-test for this function to compare with its backend counter part
applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap) { applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap) {
const self = this; const self = this;
const nextRenderFramePlayers = {} const nextRenderFramePlayers = {};
for (let playerId in currRenderFrame.players) { for (let playerId in currRenderFrame.players) {
const currPlayerDownsync = currRenderFrame.players[playerId]; const currPlayerDownsync = currRenderFrame.players[playerId];
nextRenderFramePlayers[playerId] = { nextRenderFramePlayers[playerId] = {
id: playerId, id: playerId,
virtualGridX: currPlayerDownsync.virtualGridX, virtualGridX: currPlayerDownsync.virtualGridX,
virtualGridY: currPlayerDownsync.virtualGridY, virtualGridY: currPlayerDownsync.virtualGridY,
dir: { dirX: currPlayerDownsync.dirX,
dx: currPlayerDownsync.dir.dx, dirY: currPlayerDownsync.dirY,
dy: currPlayerDownsync.dir.dy, characterState: currPlayerDownsync.characterState,
},
speed: currPlayerDownsync.speed, speed: currPlayerDownsync.speed,
battleState: currPlayerDownsync.battleState, battleState: currPlayerDownsync.battleState,
score: currPlayerDownsync.score, score: currPlayerDownsync.score,
removed: currPlayerDownsync.removed, removed: currPlayerDownsync.removed,
joinIndex: currPlayerDownsync.joinIndex, joinIndex: currPlayerDownsync.joinIndex,
framesToRecover: (0 < currPlayerDownsync.framesToRecover ? currPlayerDownsync.framesToRecover - 1 : 0),
hp: currPlayerDownsync.hp,
maxHp: currPlayerDownsync.maxHp,
}; };
} }
const toRet = { const toRet = {
id: currRenderFrame.id + 1, id: currRenderFrame.id + 1,
players: nextRenderFramePlayers, players: nextRenderFramePlayers,
meleeBullets: []
}; };
const bulletPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
// Reset playerCollider position from the "virtual grid position"
for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1;
bulletPushbacks[joinIndex - 1] = [0.0, 0.0];
effPushbacks[joinIndex - 1] = [0.0, 0.0];
const playerRichInfo = self.playerRichInfoArr[j];
const playerId = playerRichInfo.id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const currPlayerDownsync = currRenderFrame.players[playerId];
const newVx = currPlayerDownsync.virtualGridX;
const newVy = currPlayerDownsync.virtualGridY;
const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]);
playerCollider.x = newCpos[0];
playerCollider.y = newCpos[1];
}
// Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision
const bulletColliders = new Map(); // Will all be removed at the end of `applyInputFrameDownsyncDynamicsOnSingleRenderFrame` due to the need for being rollback-compatible
const removedBulletsAtCurrFrame = new Set();
for (let k in currRenderFrame.meleeBullets) {
const meleeBullet = currRenderFrame.meleeBullets[k];
if (
meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames <= currRenderFrame.id
&&
meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames > currRenderFrame.id
) {
const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId;
const collisionOffenderIndex = self.collisionPlayerIndexPrefix + meleeBullet.offenderJoinIndex;
const offenderCollider = collisionSysMap.get(collisionOffenderIndex);
const offender = currRenderFrame.players[meleeBullet.offenderPlayerId];
let xfac = 1,
yfac = 0; // By now, straight Punch offset doesn't respect "y-axis"
if (0 > offender.dirX) {
xfac = -1;
}
const x0 = offenderCollider.x + xfac * meleeBullet.hitboxOffset,
y0 = offenderCollider.y + yfac * meleeBullet.hitboxOffset;
const pts = [[0, 0], [xfac * meleeBullet.hitboxSize.x, 0], [xfac * meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]];
const newBulletCollider = collisionSys.createPolygon(x0, y0, pts);
newBulletCollider.data = meleeBullet;
collisionSysMap.set(collisionBulletIndex, newBulletCollider);
bulletColliders.set(collisionBulletIndex, newBulletCollider);
// console.log(`A meleeBullet is added to collisionSys at renderFrame.id=${currRenderFrame.id} as start-up frames ended and active frame is not yet ended: ${JSON.stringify(meleeBullet)}`);
}
}
collisionSys.update();
const result1 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle?
bulletColliders.forEach((bulletCollider, collisionBulletIndex) => {
const potentials = bulletCollider.potentials();
const offender = currRenderFrame.players[bulletCollider.data.offenderPlayerId];
let shouldRemove = false;
for (const potential of potentials) {
if (null != potential.data && potential.data.joinIndex == bulletCollider.data.offenderJoinIndex) continue;
if (!bulletCollider.collides(potential, result1)) continue;
if (null != potential.data && null !== potential.data.joinIndex) {
const joinIndex = potential.data.joinIndex;
let xfac = 1;
if (0 > offender.dirX) {
xfac = -1;
}
bulletPushbacks[joinIndex - 1][0] += xfac * bulletCollider.data.pushback; // Only for straight punch, there's no y-pushback
bulletPushbacks[joinIndex - 1][1] += 0;
const thatAckedPlayerInNextFrame = nextRenderFramePlayers[potential.data.id];
thatAckedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0];
const oldFramesToRecover = thatAckedPlayerInNextFrame.framesToRecover;
thatAckedPlayerInNextFrame.framesToRecover = (oldFramesToRecover > bulletCollider.data.hitStunFrames ? oldFramesToRecover : bulletCollider.data.hitStunFrames); // In case the hit player is already stun, we extend it
}
shouldRemove = true;
}
if (shouldRemove) {
removedBulletsAtCurrFrame.add(collisionBulletIndex);
}
});
// [WARNING] Remove bullets from collisionSys ANYWAY for the convenience of rollback
for (let k in currRenderFrame.meleeBullets) {
const meleeBullet = currRenderFrame.meleeBullets[k];
const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId;
if (collisionSysMap.has(collisionBulletIndex)) {
const bulletCollider = collisionSysMap.get(collisionBulletIndex);
bulletCollider.remove();
collisionSysMap.delete(collisionBulletIndex);
}
if (removedBulletsAtCurrFrame.has(collisionBulletIndex)) continue;
toRet.meleeBullets.push(meleeBullet);
}
// Process player inputs
if (null != delayedInputFrame) { if (null != delayedInputFrame) {
const delayedInputFrameForPrevRenderFrame = self.getCachedInputFrameDownsyncWithPrediction(self._convertToInputFrameId(currRenderFrame.id - 1, self.inputDelayFrames));
const inputList = delayedInputFrame.inputList; const inputList = delayedInputFrame.inputList;
const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order
for (let j in self.playerRichInfoArr) { for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1; const joinIndex = parseInt(j) + 1;
effPushbacks[joinIndex - 1] = [0.0, 0.0]; effPushbacks[joinIndex - 1] = [0.0, 0.0];
const playerId = self.playerRichInfoArr[j].id; const playerRichInfo = self.playerRichInfoArr[j];
const playerId = playerRichInfo.id;
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex); const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const player = currRenderFrame.players[playerId]; const currPlayerDownsync = currRenderFrame.players[playerId];
const thatPlayerInNextFrame = nextRenderFramePlayers[playerId];
if (0 < thatPlayerInNextFrame.framesToRecover) {
// No need to process inputs for this player, but there might be bullet pushbacks on this player
playerCollider.x += bulletPushbacks[joinIndex - 1][0];
playerCollider.y += bulletPushbacks[joinIndex - 1][1];
if (0 != bulletPushbacks[joinIndex - 1][0] || 0 != bulletPushbacks[joinIndex - 1][1]) {
console.log(`playerId=${playerId}, joinIndex=${joinIndex} is pushbacked back by ${bulletPushbacks[joinIndex - 1]} by bullet impacts, now its framesToRecover is ${currPlayerDownsync.framesToRecover}`);
}
continue;
}
const decodedInput = self.ctrl.decodeInput(inputList[joinIndex - 1]); const decodedInput = self.ctrl.decodeInput(inputList[joinIndex - 1]);
// console.log(`Got non-zero inputs for playerId=${playerId}, decodedInput=${JSON.stringify(decodedInput)} @currRenderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.id}`); const prevDecodedInput = (null == delayedInputFrameForPrevRenderFrame ? null : self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[joinIndex - 1]));
/* const prevBtnALevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnALevel);
Reset "position" of players in "collisionSys" according to "virtual grid position". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics.
*/ if (1 == decodedInput.btnALevel && 0 == prevBtnALevel) {
const newVx = player.virtualGridX + (decodedInput.dx + player.speed * decodedInput.dx); // console.log(`playerId=${playerId} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
const newVy = player.virtualGridY + (decodedInput.dy + player.speed * decodedInput.dy); if (self.bulletTriggerEnabled) {
const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]); thatPlayerInNextFrame.framesToRecover = window.PunchAtkConfig.recoveryFrames;
playerCollider.x = newCpos[0]; const punch = window.pb.protos.MeleeBullet.create(window.PunchAtkConfig);
playerCollider.y = newCpos[1]; punch.battleLocalId = self.bulletBattleLocalIdCounter++;
if (0 != decodedInput.dx || 0 != decodedInput.dy) { punch.offenderJoinIndex = joinIndex;
// Update directions and thus would eventually update moving animation accordingly punch.offenderPlayerId = playerId;
nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx; punch.originatedRenderFrameId = currRenderFrame.id;
nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy; toRet.meleeBullets.push(punch);
console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}: ${self._stringifyRecentInputCache(true)}`);
thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0];
}
} else if (0 == decodedInput.btnALevel && 1 == prevBtnALevel) {
// console.log(`playerId=${playerId} triggered a falling-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
} else {
// No bullet trigger, process movement inputs
if (0 != decodedInput.dx || 0 != decodedInput.dy) {
// Update directions and thus would eventually update moving animation accordingly
thatPlayerInNextFrame.dirX = decodedInput.dx;
thatPlayerInNextFrame.dirY = decodedInput.dy;
thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Walking[0];
} else {
thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0];
}
const movement = self.virtualGridToWorldPos(decodedInput.dx + currPlayerDownsync.speed * decodedInput.dx, decodedInput.dy + currPlayerDownsync.speed * decodedInput.dy);
playerCollider.x += movement[0];
playerCollider.y += movement[1];
} }
} }
collisionSys.update(); collisionSys.update(); // by now all "bulletCollider"s are removed
const result = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? const result2 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle?
for (let j in self.playerRichInfoArr) { for (let j in self.playerRichInfoArr) {
const joinIndex = parseInt(j) + 1; const joinIndex = parseInt(j) + 1;
@ -1072,10 +1194,10 @@ cc.Class({
const potentials = playerCollider.potentials(); const potentials = playerCollider.potentials();
for (const potential of potentials) { for (const potential of potentials) {
// Test if the player collides with the wall // Test if the player collides with the wall
if (!playerCollider.collides(potential, result)) continue; if (!playerCollider.collides(potential, result2)) continue;
// Push the player out of the wall // Push the player out of the wall
effPushbacks[joinIndex - 1][0] += result.overlap * result.overlap_x; effPushbacks[joinIndex - 1][0] += result2.overlap * result2.overlap_x;
effPushbacks[joinIndex - 1][1] += result.overlap * result.overlap_y; effPushbacks[joinIndex - 1][1] += result2.overlap * result2.overlap_y;
} }
} }
@ -1085,9 +1207,11 @@ cc.Class({
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
const playerCollider = collisionSysMap.get(collisionPlayerIndex); const playerCollider = collisionSysMap.get(collisionPlayerIndex);
const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j]); const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j]);
nextRenderFramePlayers[playerId].virtualGridX = newVpos[0]; const thatPlayerInNextFrame = nextRenderFramePlayers[playerId];
nextRenderFramePlayers[playerId].virtualGridY = newVpos[1]; thatPlayerInNextFrame.virtualGridX = newVpos[0];
thatPlayerInNextFrame.virtualGridY = newVpos[1];
} }
} }
return toRet; return toRet;

View File

@ -3,6 +3,31 @@ i18n.init(window.language); // languageID should be equal to the one we input in
const OnlineMap = require('./Map'); const OnlineMap = require('./Map');
window.PunchAtkConfig = {
// for offender
startupFrames: 18,
activeFrames: 42,
recoveryFrames: 61, // usually but not always "startupFrames+activeFrames", I hereby set it to be 1 frame more than the actual animation to avoid critical transition, i.e. when the animation is 1 frame from ending but "rdfPlayer.framesToRecover" is already counted 0 and the player triggers an other same attack, making an effective bullet trigger but no animation is played due to same animName is still playing
recoveryFramesOnBlock: 61,
recoveryFramesOnHit: 61,
moveforward: {
x: 0,
y: 0,
},
hitboxOffset: 12.0, // should be about the radius of the PlayerCollider
hitboxSize: {
x: 45.0,
y: 32.0,
},
// for defender
hitStunFrames: 18,
blockStunFrames: 9,
pushback: 22.0,
releaseTriggerType: 1, // 1: rising-edge, 2: falling-edge
damage: 5
};
cc.Class({ cc.Class({
extends: OnlineMap, extends: OnlineMap,
@ -10,43 +35,6 @@ cc.Class({
console.warn("+++++++ Map onDestroy()"); console.warn("+++++++ Map onDestroy()");
}, },
spawnPlayerNode(joinIndex, vx, vy, playerRichInfo) {
const self = this;
const newPlayerNode = cc.instantiate(self.controlledCharacterPrefab)
const playerScriptIns = newPlayerNode.getComponent("ControlledCharacter");
if (1 == joinIndex) {
playerScriptIns.setSpecies("SoldierElf");
} else if (2 == joinIndex) {
playerScriptIns.setSpecies("SoldierFireGhost");
playerScriptIns.animComp.node.scaleX = (-1.0);
}
const wpos = self.virtualGridToWorldPos(vx, vy);
newPlayerNode.setPosition(cc.v2(wpos[0], wpos[1]));
playerScriptIns.mapNode = self.node;
const cpos = self.virtualGridToPlayerColliderPos(vx, vy, playerRichInfo);
const d = playerRichInfo.colliderRadius * 2,
x0 = cpos[0],
y0 = cpos[1];
let pts = [[0, 0], [d, 0], [d, d], [0, d]];
const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts);
const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex;
self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider);
safelyAddChild(self.node, newPlayerNode);
setLocalZOrder(newPlayerNode, 5);
newPlayerNode.active = true;
playerScriptIns.scheduleNewDirection({
dx: playerRichInfo.dir.dx,
dy: playerRichInfo.dir.dy
}, true);
return [newPlayerNode, playerScriptIns];
},
onLoad() { onLoad() {
const self = this; const self = this;
window.mapIns = self; window.mapIns = self;
@ -73,7 +61,6 @@ cc.Class({
self.rollbackEstimatedDt = 0.016667; self.rollbackEstimatedDt = 0.016667;
self.rollbackEstimatedDtMillis = 16.667; self.rollbackEstimatedDtMillis = 16.667;
self.rollbackEstimatedDtNanos = 16666666; self.rollbackEstimatedDtNanos = 16666666;
self.maxChasingRenderFramesPerUpdate = 5;
self.worldToVirtualGridRatio = 1000; self.worldToVirtualGridRatio = 1000;
self.virtualGridToWorldRatio = 1.0 / self.worldToVirtualGridRatio; self.virtualGridToWorldRatio = 1.0 / self.worldToVirtualGridRatio;
@ -153,22 +140,30 @@ cc.Class({
virtualGridX: 0, virtualGridX: 0,
virtualGridY: 0, virtualGridY: 0,
speed: 2 * self.worldToVirtualGridRatio, speed: 2 * self.worldToVirtualGridRatio,
dir: {
dx: 0,
dy: 0
}
},
},
playerMetas: {
10: {
colliderRadius: 12, colliderRadius: 12,
characterState: window.ATK_CHARACTER_STATE.Idle1[0],
framesToRecover: 0,
dirX: 0,
dirY: 0,
},
11: {
id: 11,
joinIndex: 2,
virtualGridX: 80 * self.worldToVirtualGridRatio,
virtualGridY: 40 * self.worldToVirtualGridRatio,
speed: 2 * self.worldToVirtualGridRatio,
colliderRadius: 12,
characterState: window.ATK_CHARACTER_STATE.Idle1[0],
framesToRecover: 0,
dirX: 0,
dirY: 0,
}, },
} }
}; };
self.selfPlayerInfo = { self.selfPlayerInfo = {
id: 10 id: 10
}; };
self._initPlayerRichInfoDict(startRdf.players, startRdf.playerMetas); self._initPlayerRichInfoDict(startRdf.players);
self.onRoomDownsyncFrame(startRdf); self.onRoomDownsyncFrame(startRdf);
self.battleState = ALL_BATTLE_STATES.IN_BATTLE; self.battleState = ALL_BATTLE_STATES.IN_BATTLE;