WIP
This commit is contained in:
parent
122590d10b
commit
8fe7bff891
9 changed files with 306 additions and 256 deletions
|
|
@ -11,9 +11,9 @@ func _broadcast() -> void:
|
|||
if !controlled_world:
|
||||
return
|
||||
|
||||
var pos = controlled_world.player.position
|
||||
var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "x": pos.x, "y": pos.y})
|
||||
print("SEND: ", json_string)
|
||||
var delta_vel = controlled_world.player.get_and_reset_delta_vel()
|
||||
var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "vel_x": delta_vel.x, "vel_y": delta_vel.y})
|
||||
#print("SEND: ", json_string)
|
||||
network.nakama_socket.send_match_state_async(network.current_match_id, 0, json_string)
|
||||
|
||||
func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
||||
|
|
@ -23,10 +23,9 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
|||
match p_state.op_code:
|
||||
2:
|
||||
var data = JSON.parse_string(p_state.data)
|
||||
print("RECV: ", data)
|
||||
|
||||
# Set player position if server demands a forced position
|
||||
if data["forcePlayerPos"]:
|
||||
if true:#data["forcePlayerPos"]:
|
||||
controlled_world.player.set_position_data(
|
||||
Vector2(
|
||||
float(data["playerHitPos"]["x"]),
|
||||
|
|
@ -38,31 +37,31 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
|||
controlled_world.ui.text = "Graze: " + str(data["graze"])
|
||||
|
||||
# Handle player death if there is an ongoing death timer
|
||||
if int(data["deathTimer"]) > 0:
|
||||
controlled_world.player.kill()
|
||||
elif int(data["deathTimer"]) == 0:
|
||||
controlled_world.player.resurrect()
|
||||
#if int(data["deathTimer"]) > 0:
|
||||
# controlled_world.player.kill()
|
||||
#elif int(data["deathTimer"]) == 0:
|
||||
# controlled_world.player.resurrect()
|
||||
|
||||
# Spawn new bullets
|
||||
for b in data["newBullets"]:
|
||||
var bullet = DanmakuBullet.new()
|
||||
bullet.setup_bullet(
|
||||
int(b["class"]),
|
||||
int(b["tick"]),
|
||||
b["x"],
|
||||
b["y"],
|
||||
b["radius"],
|
||||
b["vel_x"],
|
||||
b["vel_y"])
|
||||
bullet.texture = load("res://test-bullet.png")
|
||||
bullet.position = bullet.get_current_pos(int(b["tick"]))
|
||||
|
||||
# Reimplemented from ScalableSprite2D here atm
|
||||
var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
|
||||
bullet.scale = Vector2(scale_ratio, scale_ratio)
|
||||
bullet.z_index = 4
|
||||
|
||||
controlled_world.bullet_manager.add_child(bullet)
|
||||
controlled_world.bullet_manager.bullets.append(bullet)
|
||||
LocalTimer.predicted_tick = int(b["tick"])
|
||||
LocalTimer.delta_counter = 0
|
||||
#for b in data["serverEvents"]:
|
||||
#var bullet = DanmakuBullet.new()
|
||||
#bullet.setup_bullet(
|
||||
#int(b["class"]),
|
||||
#int(b["tick"]),
|
||||
#b["x"],
|
||||
#b["y"],
|
||||
#b["radius"],
|
||||
#b["vel_x"],
|
||||
#b["vel_y"])
|
||||
#bullet.texture = load("res://test-bullet.png")
|
||||
#bullet.position = bullet.get_current_pos(int(b["tick"]))
|
||||
#
|
||||
## Reimplemented from ScalableSprite2D here atm
|
||||
#var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
|
||||
#bullet.scale = Vector2(scale_ratio, scale_ratio)
|
||||
#bullet.z_index = 4
|
||||
#
|
||||
#controlled_world.bullet_manager.add_child(bullet)
|
||||
#controlled_world.bullet_manager.bullets.append(bullet)
|
||||
#LocalTimer.predicted_tick = int(b["tick"])
|
||||
#LocalTimer.delta_counter = 0
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
class_name Player
|
||||
extends Node2D
|
||||
|
||||
@export var speed = 80
|
||||
@export var controlled = false
|
||||
var velocity := Vector2.ZERO
|
||||
const SPEED = 80
|
||||
var slow = false
|
||||
var tick_vel := Vector2.ZERO
|
||||
var delta_vel_since_last_broadcast := Vector2.ZERO
|
||||
var hurt_collision: DanmakuCircle = DanmakuCircle.new()
|
||||
var graze_collision: DanmakuCircle = DanmakuCircle.new()
|
||||
const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18
|
||||
|
|
@ -12,25 +14,30 @@ var alive: bool = true
|
|||
func _ready() -> void:
|
||||
$BodySprite.scale_sprite(PLAYER_BODY_WIDTH_MULTIPLIER * Globals.SERVER_SIZE.x)
|
||||
|
||||
func get_input():
|
||||
func get_input(delta):
|
||||
slow = false
|
||||
if Input.is_action_pressed("Slow Mode"):
|
||||
speed = 30
|
||||
else:
|
||||
speed = 80
|
||||
slow = true
|
||||
|
||||
velocity = Input.get_vector("Left", "Right", "Up", "Down") * speed
|
||||
tick_vel = Input.get_vector("Left", "Right", "Up", "Down") * ((SPEED / 3) if slow else SPEED) * delta
|
||||
delta_vel_since_last_broadcast += tick_vel
|
||||
|
||||
func _physics_process(delta: float):
|
||||
get_input()
|
||||
get_input(delta)
|
||||
|
||||
if !alive:
|
||||
return
|
||||
#if !alive:
|
||||
#return
|
||||
|
||||
# Bounds checking
|
||||
var attempted_position := position + (velocity * delta)
|
||||
attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
|
||||
#var attempted_position := position + (velocity * delta)
|
||||
#attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
|
||||
|
||||
set_position_data(attempted_position, null, null)
|
||||
#set_position_data(attempted_position, null, null)
|
||||
|
||||
func get_and_reset_delta_vel():
|
||||
var ret = delta_vel_since_last_broadcast
|
||||
delta_vel_since_last_broadcast = Vector2.ZERO
|
||||
return ret
|
||||
|
||||
func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
|
||||
position = pos
|
||||
|
|
@ -45,7 +52,6 @@ func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
|
|||
graze_collision.set_radius(grazecircle_radius)
|
||||
$GrazecircleSprite.scale_sprite(grazecircle_radius*2)
|
||||
|
||||
|
||||
func kill():
|
||||
if alive == false:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -6,10 +6,17 @@ ENV CGO_ENABLED 1
|
|||
WORKDIR /backend
|
||||
COPY . .
|
||||
|
||||
RUN go build --trimpath --buildmode=plugin -o ./backend.so
|
||||
RUN apt-get update && \
|
||||
apt-get -y upgrade && \
|
||||
apt-get install -y --no-install-recommends gcc libc6-dev
|
||||
|
||||
FROM heroiclabs/nakama:3.26.0
|
||||
RUN go build --trimpath --gcflags "all=-N -l" --mod=vendor --buildmode=plugin -o ./backend.so
|
||||
RUN go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
|
||||
FROM heroiclabs/nakama-dsym:3.26.0
|
||||
|
||||
COPY --from=builder /backend/backend.so /nakama/data/modules
|
||||
#COPY --from=builder /backend/local.yml /nakama/data/
|
||||
COPY --from=builder /backend/*.json /nakama/data/modules
|
||||
|
||||
ENTRYPOINT [ "/bin/bash" ]
|
||||
|
|
@ -24,7 +24,7 @@ services:
|
|||
- "-ecx"
|
||||
- >
|
||||
/nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
|
||||
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
|
||||
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level ERROR --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
|
||||
restart: "no"
|
||||
links:
|
||||
- "cockroachdb:db"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package ffi
|
|||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import "strconv"
|
||||
|
||||
// Circles
|
||||
type Circle struct {
|
||||
|
|
@ -19,6 +20,14 @@ func NewCircle(x float64, y float64, rad float64) *Circle {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *Circle) Clone() *Circle {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
x, y := c.GetPos()
|
||||
return NewCircle(x, y, float64(c.cptr.radius))
|
||||
}
|
||||
|
||||
func (c *Circle) UpdatePos(x float64, y float64) {
|
||||
c.cptr.x = C.double(x)
|
||||
c.cptr.y = C.double(y)
|
||||
|
|
@ -30,6 +39,7 @@ func DestroyCircle(circle *Circle) {
|
|||
}
|
||||
|
||||
C.destroy_circle(circle.cptr)
|
||||
circle.cptr = nil
|
||||
}
|
||||
|
||||
func (c *Circle) GetPos() (float64, float64) {
|
||||
|
|
@ -38,20 +48,18 @@ func (c *Circle) GetPos() (float64, float64) {
|
|||
|
||||
// Bullets
|
||||
type Bullet struct {
|
||||
cptr *C.Bullet
|
||||
DeletionTick int64
|
||||
cptr *C.Bullet
|
||||
}
|
||||
|
||||
// Values for selecting bullet paths
|
||||
const (
|
||||
BULLET_LINEAR = 0
|
||||
ACTIVE_BULLET = -1
|
||||
)
|
||||
|
||||
func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, velX float64, velY float64) *Bullet {
|
||||
return &Bullet{
|
||||
cptr: C.new_bullet(
|
||||
C.uint8_t(0),
|
||||
C.uint8_t(BULLET_LINEAR),
|
||||
C.int64_t(tick),
|
||||
C.double(spawnX),
|
||||
C.double(spawnY),
|
||||
|
|
@ -59,7 +67,29 @@ func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64,
|
|||
C.double(velX),
|
||||
C.double(velY),
|
||||
),
|
||||
DeletionTick: ACTIVE_BULLET,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bullet) Clone() *Bullet {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch int(b.cptr.class_) {
|
||||
case BULLET_LINEAR:
|
||||
nb := NewLinearBullet(
|
||||
int64(b.cptr.spawn_time),
|
||||
float64(b.cptr.spawn_x),
|
||||
float64(b.cptr.spawn_y),
|
||||
float64(b.cptr.radius),
|
||||
float64(b.cptr.parameters[0]), // vel_x
|
||||
float64(b.cptr.parameters[1]), // vel_y
|
||||
)
|
||||
return nb
|
||||
|
||||
default:
|
||||
// Mirror other bullet classes here with their exact constructors.
|
||||
panic("ffi.Bullet.Clone: unsupported bullet class: " + strconv.Itoa(int(b.cptr.class_)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +99,7 @@ func DestroyBullet(bullet *Bullet) {
|
|||
}
|
||||
|
||||
C.destroy_bullet(bullet.cptr)
|
||||
bullet.cptr = nil
|
||||
}
|
||||
|
||||
func (b *Bullet) BeyondKillBoundary(tick int64) bool {
|
||||
|
|
@ -94,13 +125,17 @@ func (b *Bullet) GetRadius() float64 {
|
|||
return float64(b.cptr.radius)
|
||||
}
|
||||
|
||||
func (b *Bullet) Serialize(tick int64) map[string]any {
|
||||
x, y := b.GetPos(tick)
|
||||
func (b *Bullet) GetSpawnTick() int64 {
|
||||
return int64(b.cptr.spawn_time)
|
||||
}
|
||||
|
||||
func (b *Bullet) Serialize( /*tick int64*/ ) map[string]any {
|
||||
//x, y := b.GetPos(tick)
|
||||
return map[string]any{
|
||||
"class": b.GetType(),
|
||||
"tick": tick,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"class": b.GetType(),
|
||||
"tick": b.GetSpawnTick(),
|
||||
//"x": x,
|
||||
//"y": y,
|
||||
"radius": b.GetRadius(),
|
||||
"vel_x": float64(b.cptr.parameters[0]),
|
||||
"vel_y": float64(b.cptr.parameters[1]),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ const (
|
|||
FINAL_PHASE
|
||||
MATCH_END
|
||||
)
|
||||
const (
|
||||
LEFT = iota
|
||||
RIGHT
|
||||
UP
|
||||
DOWN
|
||||
)
|
||||
|
||||
const (
|
||||
ALIVE = iota
|
||||
DEATH_QUEUED
|
||||
DEAD
|
||||
)
|
||||
|
||||
const (
|
||||
TICK_RATE = 60
|
||||
|
|
|
|||
|
|
@ -31,20 +31,22 @@ func StorePlayerInputs(lobbyState *MatchState, messages []runtime.MatchData, log
|
|||
continue
|
||||
}
|
||||
|
||||
var update ClientUpdate
|
||||
if err := json.Unmarshal(msg.GetData(), &update); err != nil {
|
||||
var newInput ClientUpdate
|
||||
if err := json.Unmarshal(msg.GetData(), &newInput); err != nil {
|
||||
(*logger).Warn("Failed to parse input: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Store the input in the player's stage state
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &update)
|
||||
// Store the input in the player's stage state if it arrived in time
|
||||
if tick-newInput.Tick < GRACE_WINDOW_TICKS {
|
||||
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &newInput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BroadcastToPresences(tick int64, lobbyState *MatchState, logger *runtime.Logger, dispatcher *runtime.MatchDispatcher) {
|
||||
for _, v := range lobbyState.presences {
|
||||
var tickData = v.stageState.MakeServerTick(tick)
|
||||
var tickData = v.stageState.SimulateAndMakeServerTick(tick)
|
||||
|
||||
data, err := json.Marshal(tickData)
|
||||
if err != nil {
|
||||
|
|
@ -71,10 +73,7 @@ func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB
|
|||
StorePlayerInputs(lobbyState, messages, &logger, tick)
|
||||
|
||||
for _, playerState := range lobbyState.presences {
|
||||
playerState.stageState.MarkBulletsBeyondKillBoundary(tick)
|
||||
playerState.stageState.ProcessPlayerInputs(tick)
|
||||
playerState.stageState.HandleDeath(tick)
|
||||
playerState.stageState.CleanupOldBullets(tick)
|
||||
playerState.stageState = playerState.stageState.ResolveLockedInState(tick)
|
||||
}
|
||||
|
||||
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
|
||||
|
|
|
|||
|
|
@ -5,189 +5,186 @@ import (
|
|||
"danmaku/ffi"
|
||||
"math"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PlayerStageState struct {
|
||||
hitCol *ffi.Circle
|
||||
grazeCol *ffi.Circle
|
||||
bullets []*ffi.Bullet
|
||||
updatePlayerPos bool
|
||||
health int
|
||||
graze int
|
||||
grazePerTick map[int64]int
|
||||
deathTick int64
|
||||
dead bool
|
||||
cancelDeath bool
|
||||
survivedGraceWindow bool
|
||||
playerInputs []*ClientUpdate
|
||||
lastInput *ClientUpdate
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) ProcessPlayerInputs(tick int64) {
|
||||
// Lock-in the earned graze
|
||||
cutoffTick := tick - GRACE_WINDOW_TICKS
|
||||
for t, v := range s.grazePerTick {
|
||||
if t < cutoffTick {
|
||||
s.graze += v
|
||||
delete(s.grazePerTick, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up inputs outside the grace window
|
||||
s.playerInputs = slices.DeleteFunc(s.playerInputs, func(input *ClientUpdate) bool {
|
||||
return tick-input.Tick > GRACE_WINDOW_TICKS
|
||||
})
|
||||
|
||||
// Sort inputs by tick
|
||||
slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int {
|
||||
return cmp.Compare(a.Tick, b.Tick)
|
||||
})
|
||||
|
||||
// Replay each tick within the grace window
|
||||
s.survivedGraceWindow = true
|
||||
for t := tick - GRACE_WINDOW_TICKS; t <= tick; t++ {
|
||||
// Find the input for the current tick
|
||||
var currentInput *ClientUpdate
|
||||
for _, input := range s.playerInputs {
|
||||
if input.Tick == t {
|
||||
currentInput = input
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the correct movement or no movement
|
||||
if currentInput != nil {
|
||||
s.clampedMove(currentInput.X, currentInput.Y)
|
||||
s.lastInput = currentInput
|
||||
} else if s.lastInput != nil {
|
||||
s.clampedMove(s.lastInput.X, s.lastInput.Y)
|
||||
}
|
||||
|
||||
// If the player dies in the grace window, don't cancel their death
|
||||
if s.CheckPlayerDeadOnTick(t) {
|
||||
s.survivedGraceWindow = false
|
||||
|
||||
for grazeT := range s.grazePerTick {
|
||||
if grazeT > t {
|
||||
delete(s.grazePerTick, grazeT)
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
s.grazePerTick[t] = s.CalculateGrazeDelta(t)
|
||||
}
|
||||
}
|
||||
lastLockedInTick int64
|
||||
hitCol *ffi.Circle
|
||||
grazeCol *ffi.Circle
|
||||
bullets []*ffi.Bullet
|
||||
playerInputs []*ClientUpdate
|
||||
deathState int
|
||||
health int
|
||||
graze int
|
||||
}
|
||||
|
||||
func NewPlayerStage() *PlayerStageState {
|
||||
return &PlayerStageState{
|
||||
hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER),
|
||||
grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER),
|
||||
bullets: []*ffi.Bullet{},
|
||||
updatePlayerPos: true,
|
||||
grazePerTick: make(map[int64]int),
|
||||
health: 3,
|
||||
deathTick: PLAYER_ALIVE,
|
||||
hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER),
|
||||
grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER),
|
||||
bullets: []*ffi.Bullet{},
|
||||
lastLockedInTick: 0,
|
||||
health: 3,
|
||||
graze: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) DeepClone() *PlayerStageState {
|
||||
clone := &PlayerStageState{
|
||||
lastLockedInTick: s.lastLockedInTick,
|
||||
health: s.health,
|
||||
graze: s.graze,
|
||||
}
|
||||
|
||||
// Circles
|
||||
if s.hitCol != nil {
|
||||
clone.hitCol = s.hitCol.Clone()
|
||||
}
|
||||
if s.grazeCol != nil {
|
||||
clone.grazeCol = s.grazeCol.Clone()
|
||||
}
|
||||
|
||||
// Bullets
|
||||
if s.bullets != nil {
|
||||
clone.bullets = make([]*ffi.Bullet, len(s.bullets))
|
||||
for i, b := range s.bullets {
|
||||
clone.bullets[i] = b.Clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Player inputs
|
||||
if s.playerInputs != nil {
|
||||
clone.playerInputs = make([]*ClientUpdate, len(s.playerInputs))
|
||||
for i, u := range s.playerInputs {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
clone.playerInputs[i] = &ClientUpdate{
|
||||
VelX: u.VelX,
|
||||
VelY: u.VelY,
|
||||
Tick: u.Tick,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) Delete() {
|
||||
for _, bullet := range s.bullets {
|
||||
ffi.DestroyBullet(bullet)
|
||||
}
|
||||
s.bullets = nil
|
||||
|
||||
ffi.DestroyCircle(s.hitCol)
|
||||
ffi.DestroyCircle(s.grazeCol)
|
||||
s.hitCol = nil
|
||||
s.grazeCol = nil
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) clampedMove(x, y float64) bool {
|
||||
clampedX := x < 0 || x > STAGE_WIDTH
|
||||
clampedY := y < 0 || y > STAGE_HEIGHT
|
||||
func (s *PlayerStageState) ResolveLockedInState(tick int64) *PlayerStageState {
|
||||
// Rotate the player's input queue and lock in the results of the oldest inputs if they're outside the grace window
|
||||
// Sort inputs by tick
|
||||
slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int {
|
||||
return cmp.Compare(a.Tick, b.Tick)
|
||||
})
|
||||
|
||||
// Clean up inputs outside the grace window and store them in a separate list
|
||||
var lockedInInputs, inputQueue []*ClientUpdate
|
||||
for _, input := range s.playerInputs {
|
||||
if tick-input.Tick < GRACE_WINDOW_TICKS {
|
||||
inputQueue = append(inputQueue, input)
|
||||
} else {
|
||||
lockedInInputs = append(lockedInInputs, input)
|
||||
}
|
||||
}
|
||||
|
||||
newLockedInState := s.NewStateFromInputs(lockedInInputs, tick-GRACE_WINDOW_TICKS)
|
||||
newLockedInState.playerInputs = inputQueue
|
||||
s.Delete()
|
||||
|
||||
return newLockedInState
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) NewStateFromInputs(inputs []*ClientUpdate, tickToSimulateTo int64) *PlayerStageState {
|
||||
stageStateCopy := s.DeepClone()
|
||||
|
||||
// Replay each tick between the last locked in tick and the tick to simulate to
|
||||
for t := stageStateCopy.lastLockedInTick; t < tickToSimulateTo; t++ {
|
||||
// Handle inputs for the current tick on the client
|
||||
for _, input := range inputs {
|
||||
if input.Tick == t {
|
||||
stageStateCopy.ApplyUpdate(input)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter bullets in place
|
||||
stageStateCopy.bullets = slices.DeleteFunc(stageStateCopy.bullets, func(b *ffi.Bullet) bool {
|
||||
if b.BeyondKillBoundary(t) {
|
||||
ffi.DestroyBullet(b)
|
||||
return true // remove from slice
|
||||
}
|
||||
|
||||
// Otherwise, handle collisions
|
||||
if b.CollidesWith(stageStateCopy.hitCol, t) && stageStateCopy.deathState == ALIVE {
|
||||
// TODO fix whatever this weird sliding issue is on the client
|
||||
// TODO handle the issue of the transient state being used to compute bullets
|
||||
// i.e. where do those bullets go? in the past? in the future?
|
||||
// TODO bullets are impacting the past
|
||||
// TODO Then replay, since bullets should be working again
|
||||
// TODO: Finally, handle death and revival using deathState
|
||||
|
||||
ffi.DestroyBullet(b)
|
||||
return true
|
||||
}
|
||||
|
||||
if b.CollidesWith(stageStateCopy.grazeCol, t) && stageStateCopy.deathState == ALIVE {
|
||||
stageStateCopy.graze += GRAZE_ADDITION_MULTIPLIER
|
||||
}
|
||||
return false // keep bullet
|
||||
})
|
||||
|
||||
// Handle server events
|
||||
stageStateCopy.MakeServerEvents(t)
|
||||
}
|
||||
|
||||
stageStateCopy.lastLockedInTick = tickToSimulateTo
|
||||
return stageStateCopy
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) ApplyUpdate(update *ClientUpdate) {
|
||||
dx := update.VelX
|
||||
dy := update.VelY
|
||||
|
||||
if dx == 0 && dy == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// clamp max movement distance to 100 units
|
||||
if dist := math.Hypot(dx, dy); dist > 100 {
|
||||
scale := 100 / dist
|
||||
dx *= scale
|
||||
dy *= scale
|
||||
}
|
||||
|
||||
s.clampedMoveDelta(dx, dy)
|
||||
}
|
||||
|
||||
// Move by a delta
|
||||
func (s *PlayerStageState) clampedMoveDelta(dx, dy float64) bool {
|
||||
posX, posY := s.hitCol.GetPos()
|
||||
x := posX + dx
|
||||
y := posY + dy
|
||||
|
||||
newX := math.Max(0, math.Min(x, STAGE_WIDTH))
|
||||
newY := math.Max(0, math.Min(y, STAGE_HEIGHT))
|
||||
|
||||
clamped := (newX != x) || (newY != y)
|
||||
|
||||
s.hitCol.UpdatePos(newX, newY)
|
||||
s.grazeCol.UpdatePos(newX, newY)
|
||||
|
||||
return clampedX || clampedY
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) MarkBulletsBeyondKillBoundary(tick int64) {
|
||||
for _, b := range s.bullets {
|
||||
if b.BeyondKillBoundary(tick) && b.DeletionTick == ACTIVE_BULLET {
|
||||
b.DeletionTick = tick
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) CleanupOldBullets(tick int64) {
|
||||
s.bullets = slices.DeleteFunc(s.bullets, func(b *ffi.Bullet) bool {
|
||||
if b.DeletionTick > ACTIVE_BULLET && tick-b.DeletionTick > GRACE_WINDOW_TICKS {
|
||||
ffi.DestroyBullet(b)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) Kill(tick int64) {
|
||||
s.deathTick = tick
|
||||
s.dead = true
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) Revive() {
|
||||
s.hitCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1)
|
||||
s.grazeCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1)
|
||||
s.updatePlayerPos = true
|
||||
s.playerInputs = nil
|
||||
s.dead = false
|
||||
s.deathTick = PLAYER_ALIVE
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) CancelDeath() {
|
||||
s.dead = false
|
||||
s.deathTick = PLAYER_ALIVE
|
||||
s.cancelDeath = true
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) HandleDeath(tick int64) {
|
||||
// If the player didn't survive the grace window and they're currently alive, kill them
|
||||
if !(s.survivedGraceWindow) && (s.dead == false) {
|
||||
s.Kill(tick)
|
||||
return
|
||||
}
|
||||
|
||||
// If the player is currently dead and they survived the grace window, cancel their death
|
||||
if (s.dead == true) && s.survivedGraceWindow {
|
||||
s.CancelDeath()
|
||||
return
|
||||
}
|
||||
|
||||
// If the player is currently dead and they have been dead for greater than PLAYER_DEATH_TIMER_MAX ticks, revive them
|
||||
if s.dead == true && ((tick - s.deathTick) > PLAYER_DEATH_TIMER_MAX) {
|
||||
s.Revive()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) CheckPlayerDeadOnTick(tick int64) bool {
|
||||
if slices.ContainsFunc(s.bullets, func(b *ffi.Bullet) bool {
|
||||
return b.CollidesWith(s.hitCol, tick)
|
||||
}) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) CalculateGrazeDelta(tick int64) int {
|
||||
count := 0
|
||||
for _, b := range s.bullets {
|
||||
if b.CollidesWith(s.grazeCol, tick) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count * GRAZE_ADDITION_MULTIPLIER
|
||||
return clamped
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) AddBullet(b *ffi.Bullet) {
|
||||
|
|
@ -228,27 +225,32 @@ func MakeTestFireBullets(tick int64) []*ffi.Bullet {
|
|||
return bullets
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) GetBoardStateDiff(tick int64) []map[string]any {
|
||||
var newBulletsToBroadcast = []map[string]any{}
|
||||
if !s.dead {
|
||||
// Contains most timed server-side game logic
|
||||
func (s *PlayerStageState) MakeServerEvents(tick int64) {
|
||||
if s.deathState == ALIVE {
|
||||
newBullets := MakeTestFireBullets(tick)
|
||||
|
||||
for _, bullet := range newBullets {
|
||||
s.AddBullet(bullet)
|
||||
newBulletsToBroadcast = append(newBulletsToBroadcast, bullet.Serialize(tick))
|
||||
}
|
||||
}
|
||||
return newBulletsToBroadcast
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
|
||||
hitPosX, hitPosY := s.hitCol.GetPos()
|
||||
grazePosX, grazePosY := s.grazeCol.GetPos()
|
||||
func (s *PlayerStageState) SerializeServerEventsWithinGraceWindow(tick int64) []map[string]any {
|
||||
var bulletsWithinGraceWindow = []map[string]any{}
|
||||
for _, bullet := range s.bullets {
|
||||
if bullet.GetSpawnTick() > tick-GRACE_WINDOW_TICKS {
|
||||
bulletsWithinGraceWindow = append(bulletsWithinGraceWindow, bullet.Serialize())
|
||||
}
|
||||
|
||||
prospectiveGraze := 0
|
||||
for _, v := range s.grazePerTick {
|
||||
prospectiveGraze += v
|
||||
}
|
||||
return bulletsWithinGraceWindow
|
||||
}
|
||||
|
||||
func (s *PlayerStageState) SimulateAndMakeServerTick(tick int64) *ServerTickUpdate {
|
||||
transientCurrentState := s.NewStateFromInputs(s.playerInputs, tick)
|
||||
hitPosX, hitPosY := transientCurrentState.hitCol.GetPos()
|
||||
grazePosX, grazePosY := transientCurrentState.grazeCol.GetPos()
|
||||
|
||||
var tickData = ServerTickUpdate{
|
||||
Tick: tick,
|
||||
|
|
@ -262,17 +264,11 @@ func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
|
|||
"y": grazePosY,
|
||||
"radius": STAGE_WIDTH * PLAYER_GRAZE_COL_RADIUS_MULTIPLIER,
|
||||
},
|
||||
StageStateDiff: s.GetBoardStateDiff(tick),
|
||||
ForcePlayerPos: s.updatePlayerPos,
|
||||
Graze: s.graze + prospectiveGraze,
|
||||
Dead: s.dead,
|
||||
CancelDeath: s.cancelDeath,
|
||||
DeathTick: s.deathTick,
|
||||
UTCTime: float64(time.Now().UnixMilli()) / 1000.0,
|
||||
ServerEvents: s.SerializeServerEventsWithinGraceWindow(tick),
|
||||
Graze: transientCurrentState.graze,
|
||||
DeathState: transientCurrentState.deathState,
|
||||
}
|
||||
|
||||
s.cancelDeath = false
|
||||
s.updatePlayerPos = false
|
||||
|
||||
transientCurrentState.Delete()
|
||||
return &tickData
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ type PresenceState struct { // present time! hahahahahahahah!
|
|||
|
||||
// Struct to serialize client->server updates
|
||||
type ClientUpdate struct {
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
VelX float64 `json:"vel_x"`
|
||||
VelY float64 `json:"vel_y"`
|
||||
Tick int64 `json:"tick"`
|
||||
}
|
||||
|
||||
|
|
@ -33,11 +33,7 @@ type ServerTickUpdate struct {
|
|||
Tick int64 `json:"tick"`
|
||||
PlayerHitPos map[string]any `json:"playerHitPos"`
|
||||
PlayerGrazePos map[string]any `json:"playerGrazePos"`
|
||||
StageStateDiff []map[string]any `json:"stageStateDiff"`
|
||||
ForcePlayerPos bool `json:"forcePlayerPos"`
|
||||
Dead bool `json:"dead"`
|
||||
CancelDeath bool `json:"cancelDeath"`
|
||||
DeathTick int64 `json:"deathTick"`
|
||||
ServerEvents []map[string]any `json:"serverEvents"`
|
||||
DeathState int `json:"deathState"`
|
||||
Graze int `json:"graze"`
|
||||
UTCTime float64 `json:"utctime"`
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue