mirror of
https://github.com/genxium/DelayNoMore
synced 2024-12-26 03:39:00 +00:00
316 lines
15 KiB
Go
316 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"github.com/gopherjs/gopherjs/js"
|
|
"github.com/solarlune/resolv"
|
|
"dnmshared"
|
|
. "dnmshared/sharedprotos"
|
|
. "jsexport/protos"
|
|
. "jsexport/models"
|
|
)
|
|
|
|
var DIRECTION_DECODER = [][]int32{
|
|
{0, 0},
|
|
{0, +2},
|
|
{0, -2},
|
|
{+2, 0},
|
|
{-2, 0},
|
|
{+1, +1},
|
|
{-1, -1},
|
|
{+1, -1},
|
|
{-1, +1},
|
|
}
|
|
|
|
func ConvertToInputFrameId(renderFrameId int32, inputDelayFrames int32, inputScaleFrames int32) int32 {
|
|
if renderFrameId < inputDelayFrames {
|
|
return 0
|
|
}
|
|
return ((renderFrameId - inputDelayFrames) >> inputScaleFrames)
|
|
}
|
|
|
|
func DecodeInput(encodedInput uint64) *InputFrameDecoded {
|
|
encodedDirection := (encodedInput & uint64(15))
|
|
btnALevel := int32((encodedInput >> 4) & 1)
|
|
btnBLevel := int32((encodedInput >> 5) & 1)
|
|
return &InputFrameDecoded{
|
|
Dx: DIRECTION_DECODER[encodedDirection][0],
|
|
Dy: DIRECTION_DECODER[encodedDirection][1],
|
|
BtnALevel: btnALevel,
|
|
BtnBLevel: btnBLevel,
|
|
}
|
|
}
|
|
|
|
func CalcHardPushbacksNorms(playerCollider *resolv.Object, playerShape *resolv.ConvexPolygon, snapIntoPlatformOverlap float64, pEffPushback *Vec2D) []Vec2D {
|
|
ret := make([]Vec2D, 0, 10) // no one would simultaneously have more than 5 hardPushbacks
|
|
collision := playerCollider.Check(0, 0)
|
|
if nil == collision {
|
|
return ret
|
|
}
|
|
for _, obj := range collision.Objects {
|
|
switch obj.Data.(type) {
|
|
case *Barrier:
|
|
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
|
|
overlapped, pushbackX, pushbackY, overlapResult := dnmshared.CalcPushbacks(0, 0, playerShape, barrierShape)
|
|
if !overlapped {
|
|
continue
|
|
}
|
|
// ALWAY snap into hardPushbacks!
|
|
// [OverlapX, OverlapY] is the unit vector that points into the platform
|
|
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY
|
|
ret = append(ret, Vec2D{X: overlapResult.OverlapX, Y: overlapResult.OverlapY})
|
|
pEffPushback.X += pushbackX
|
|
pEffPushback.Y += pushbackY
|
|
default:
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func NewRingBufferJs(n int32) *js.Object {
|
|
return js.MakeWrapper(dnmshared.NewRingBuffer(n));
|
|
}
|
|
|
|
func NewCollisionSpaceJs(spaceW, spaceH, minStepW, minStepH int) *js.Object {
|
|
return js.MakeWrapper(resolv.NewSpace(spaceW, spaceH, minStepW, minStepH))
|
|
}
|
|
|
|
func GenerateRectColliderJs(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, tag string) *js.Object {
|
|
/*
|
|
[WARNING] It's important to note that we don't need "js.MakeFullWrapper" for a call sequence as follows.
|
|
```
|
|
var space = gopkgs.NewCollisionSpaceJs(2048, 2048, 8, 8);
|
|
var a = gopkgs.GenerateRectColliderJs(189, 497, 48, 48, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, spaceOffsetX, spaceOffsetY, "Player");
|
|
space.Add(a);
|
|
```
|
|
The "space" variable doesn't need access to the field of "a" in JavaScript level to run "space.Add(...)" method, which is good.
|
|
*/
|
|
return js.MakeWrapper(dnmshared.GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, tag));
|
|
|
|
}
|
|
|
|
func CheckCollisionJs(obj *resolv.Object, dx, dy float64) *js.Object {
|
|
// TODO: Support multiple tags in the future
|
|
// Unfortunately I couldn't find a way to just call "var a = GenerateRectColliderJs(...); space.Add(a); a.Check(...)" to get the collision result, the unwrapped method will result in stack overflow. Need a better solution later.
|
|
return js.MakeFullWrapper(obj.Check(dx, dy));
|
|
}
|
|
|
|
|
|
/*
|
|
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object, topPadding, bottomPadding, leftPadding, rightPadding float64, roomCapacity int, jumpingInitVelY int32, playersArr []*Player, inputDelayFrames int32, inputScaleFrames int32, inputsBuffer *RingBuffer, collisionSpaceOffsetX, collisionSpaceOffsetY int32, snapIntoPlatformOverlap, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *RoomDownsyncFrame {
|
|
// [WARNING] This function MUST BE called while "InputsBufferLock" is locked!
|
|
nextRenderFramePlayers := make(map[int32]*PlayerDownsync, roomCapacity)
|
|
// Make a copy first
|
|
for playerId, currPlayerDownsync := range currRenderFrame.Players {
|
|
nextRenderFramePlayers[playerId] = &PlayerDownsync{
|
|
Id: playerId,
|
|
VirtualGridX: currPlayerDownsync.VirtualGridX,
|
|
VirtualGridY: currPlayerDownsync.VirtualGridY,
|
|
DirX: currPlayerDownsync.DirX,
|
|
DirY: currPlayerDownsync.DirY,
|
|
VelX: currPlayerDownsync.VelX,
|
|
VelY: currPlayerDownsync.VelY,
|
|
CharacterState: currPlayerDownsync.CharacterState,
|
|
InAir: true,
|
|
Speed: currPlayerDownsync.Speed,
|
|
BattleState: currPlayerDownsync.BattleState,
|
|
Score: currPlayerDownsync.Score,
|
|
Removed: currPlayerDownsync.Removed,
|
|
JoinIndex: currPlayerDownsync.JoinIndex,
|
|
FramesToRecover: currPlayerDownsync.FramesToRecover - 1,
|
|
Hp: currPlayerDownsync.Hp,
|
|
MaxHp: currPlayerDownsync.MaxHp,
|
|
}
|
|
if nextRenderFramePlayers[playerId].FramesToRecover < 0 {
|
|
nextRenderFramePlayers[playerId].FramesToRecover = 0
|
|
}
|
|
}
|
|
|
|
nextRenderFrameMeleeBullets := make([]*MeleeBullet, 0, len(currRenderFrame.MeleeBullets)) // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation?
|
|
effPushbacks := make([]Vec2D, roomCapacity)
|
|
hardPushbackNorms := make([][]Vec2D, roomCapacity)
|
|
|
|
// 1. Process player inputs
|
|
if nil != delayedInputFrame {
|
|
var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil
|
|
tmp := inputsBuffer.GetByFrameId(ConvertToInputFrameId(currRenderFrame.Id-1, inputDelayFrames, inputScaleFrames))
|
|
if nil != tmp {
|
|
delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync)
|
|
}
|
|
inputList := delayedInputFrame.InputList
|
|
for _, player := range playersArr {
|
|
playerId := player.Id
|
|
joinIndex := player.JoinIndex
|
|
currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId]
|
|
if 0 < thatPlayerInNextFrame.FramesToRecover {
|
|
continue
|
|
}
|
|
decodedInput := DecodeInput(inputList[joinIndex-1])
|
|
prevBtnALevel, prevBtnBLevel := int32(0), int32(0)
|
|
if nil != delayedInputFrameForPrevRenderFrame {
|
|
prevDecodedInput := DecodeInput(delayedInputFrameForPrevRenderFrame.InputList[joinIndex-1])
|
|
prevBtnALevel = prevDecodedInput.BtnALevel
|
|
prevBtnBLevel = prevDecodedInput.BtnBLevel
|
|
}
|
|
|
|
if decodedInput.BtnBLevel > prevBtnBLevel {
|
|
characStateAlreadyInAir := false
|
|
if ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATK1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATKED1 == thatPlayerInNextFrame.CharacterState {
|
|
characStateAlreadyInAir = true
|
|
}
|
|
characStateIsInterruptWaivable := false
|
|
if ATK_CHARACTER_STATE_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_WALKING == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState {
|
|
characStateIsInterruptWaivable = true
|
|
}
|
|
if !characStateAlreadyInAir && characStateIsInterruptWaivable {
|
|
thatPlayerInNextFrame.VelY = jumpingInitVelY
|
|
}
|
|
}
|
|
|
|
// Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs
|
|
if 0 != decodedInput.Dx || 0 != decodedInput.Dy {
|
|
thatPlayerInNextFrame.DirX = decodedInput.Dx
|
|
thatPlayerInNextFrame.DirY = decodedInput.Dy
|
|
thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING
|
|
} else {
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
|
|
thatPlayerInNextFrame.VelX = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Process player movement
|
|
for _, player := range playersArr {
|
|
playerId := player.Id
|
|
joinIndex := player.JoinIndex
|
|
effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0)
|
|
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
|
playerCollider := collisionSysMap[collisionPlayerIndex]
|
|
currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId]
|
|
// Reset playerCollider position from the "virtual grid position"
|
|
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
|
|
if thatPlayerInNextFrame.VelY == jumpingInitVelY {
|
|
newVy += thatPlayerInNextFrame.VelY
|
|
}
|
|
|
|
halfColliderWidth, halfColliderHeight := player.ColliderRadius, player.ColliderRadius+player.ColliderRadius // avoid multiplying
|
|
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderBLPos(newVx, newVy, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY, virtualGridToWorldRatio)
|
|
// Update in the collision system
|
|
playerCollider.Update()
|
|
|
|
if currPlayerDownsync.InAir {
|
|
thatPlayerInNextFrame.VelX += gravityX
|
|
thatPlayerInNextFrame.VelY += gravityY
|
|
}
|
|
}
|
|
|
|
// 3. Invoke collision system stepping (no-op for backend collision lib)
|
|
|
|
// 4. Calc pushbacks for each player (after its movement) w/o bullets
|
|
for _, player := range playersArr {
|
|
joinIndex := player.JoinIndex
|
|
playerId := player.Id
|
|
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
|
playerCollider := collisionSysMap[collisionPlayerIndex]
|
|
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
|
|
hardPushbackNorms[joinIndex-1] = CalcHardPushbacksNorms(playerCollider, playerShape, snapIntoPlatformOverlap, &(effPushbacks[joinIndex-1]))
|
|
currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId]
|
|
fallStopping := false
|
|
possiblyFallStoppedOnAnotherPlayer := false
|
|
if collision := playerCollider.Check(0, 0); nil != collision {
|
|
for _, obj := range collision.Objects {
|
|
isBarrier, isAnotherPlayer, isBullet := false, false, false
|
|
switch obj.Data.(type) {
|
|
case *Barrier:
|
|
isBarrier = true
|
|
case *Player:
|
|
isAnotherPlayer = true
|
|
case *MeleeBullet:
|
|
isBullet = true
|
|
}
|
|
if isBullet {
|
|
// ignore bullets for this step
|
|
continue
|
|
}
|
|
bShape := obj.Shape.(*resolv.ConvexPolygon)
|
|
overlapped, pushbackX, pushbackY, overlapResult := dnmshared.CalcPushbacks(0, 0, playerShape, bShape)
|
|
if !overlapped {
|
|
continue
|
|
}
|
|
normAlignmentWithGravity := (overlapResult.OverlapX*float64(0) + overlapResult.OverlapY*float64(-1.0))
|
|
landedOnGravityPushback := (snapIntoPlatformThreshold < normAlignmentWithGravity) // prevents false snapping on the lateral sides
|
|
if landedOnGravityPushback {
|
|
// kindly note that one player might land on top of another player, and snapping is also required in such case
|
|
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY
|
|
thatPlayerInNextFrame.InAir = false
|
|
}
|
|
if isAnotherPlayer {
|
|
// [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame.
|
|
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapY
|
|
}
|
|
for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] {
|
|
projectedMagnitude := pushbackX*hardPushbackNorm.X + pushbackY*hardPushbackNorm.Y
|
|
if isBarrier || (isAnotherPlayer && 0 > projectedMagnitude) {
|
|
pushbackX -= projectedMagnitude * hardPushbackNorm.X
|
|
pushbackY -= projectedMagnitude * hardPushbackNorm.Y
|
|
}
|
|
}
|
|
effPushbacks[joinIndex-1].X += pushbackX
|
|
effPushbacks[joinIndex-1].Y += pushbackY
|
|
if currPlayerDownsync.InAir && landedOnGravityPushback {
|
|
fallStopping = true
|
|
if isAnotherPlayer {
|
|
possiblyFallStoppedOnAnotherPlayer = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if fallStopping {
|
|
thatPlayerInNextFrame.VelX = 0
|
|
thatPlayerInNextFrame.VelY = 0
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
|
|
thatPlayerInNextFrame.FramesToRecover = 0
|
|
}
|
|
if currPlayerDownsync.InAir {
|
|
oldNextCharacterState := thatPlayerInNextFrame.CharacterState
|
|
switch oldNextCharacterState {
|
|
case ATK_CHARACTER_STATE_IDLE1, ATK_CHARACTER_STATE_WALKING:
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1
|
|
case ATK_CHARACTER_STATE_ATK1:
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATK1
|
|
case ATK_CHARACTER_STATE_ATKED1:
|
|
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1
|
|
}
|
|
}
|
|
}
|
|
|
|
// 7. Get players out of stuck barriers if there's any
|
|
for _, player := range playersArr {
|
|
joinIndex := player.JoinIndex
|
|
playerId := player.Id
|
|
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
|
|
playerCollider := collisionSysMap[collisionPlayerIndex]
|
|
// Update "virtual grid position"
|
|
currPlayerDownsync, thatPlayerInNextFrame := currRenderFrame.Players[playerId], nextRenderFramePlayers[playerId]
|
|
halfColliderWidth, halfColliderHeight := player.ColliderRadius, player.ColliderRadius+player.ColliderRadius // avoid multiplying
|
|
thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = PolygonColliderBLToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY, worldToVirtualGridRatio)
|
|
}
|
|
|
|
return &RoomDownsyncFrame{
|
|
Id: currRenderFrame.Id + 1,
|
|
Players: nextRenderFramePlayers,
|
|
MeleeBullets: nextRenderFrameMeleeBullets,
|
|
CountdownNanos: (BattleDurationNanos - int64(currRenderFrame.Id)*RollbackEstimatedDtNanos),
|
|
}
|
|
}
|
|
*/
|
|
|
|
func main() {
|
|
js.Global.Set("gopkgs", map[string]interface{}{
|
|
"NewRingBufferJs": NewRingBufferJs,
|
|
"NewCollisionSpaceJs": NewCollisionSpaceJs,
|
|
"GenerateRectColliderJs": GenerateRectColliderJs,
|
|
"CheckCollisionJs": CheckCollisionJs,
|
|
})
|
|
}
|