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:
|
if !controlled_world:
|
||||||
return
|
return
|
||||||
|
|
||||||
var pos = controlled_world.player.position
|
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, "x": pos.x, "y": pos.y})
|
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)
|
#print("SEND: ", json_string)
|
||||||
network.nakama_socket.send_match_state_async(network.current_match_id, 0, json_string)
|
network.nakama_socket.send_match_state_async(network.current_match_id, 0, json_string)
|
||||||
|
|
||||||
func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
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:
|
match p_state.op_code:
|
||||||
2:
|
2:
|
||||||
var data = JSON.parse_string(p_state.data)
|
var data = JSON.parse_string(p_state.data)
|
||||||
print("RECV: ", data)
|
|
||||||
|
|
||||||
# Set player position if server demands a forced position
|
# Set player position if server demands a forced position
|
||||||
if data["forcePlayerPos"]:
|
if true:#data["forcePlayerPos"]:
|
||||||
controlled_world.player.set_position_data(
|
controlled_world.player.set_position_data(
|
||||||
Vector2(
|
Vector2(
|
||||||
float(data["playerHitPos"]["x"]),
|
float(data["playerHitPos"]["x"]),
|
||||||
|
|
@ -38,31 +37,31 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
|
||||||
controlled_world.ui.text = "Graze: " + str(data["graze"])
|
controlled_world.ui.text = "Graze: " + str(data["graze"])
|
||||||
|
|
||||||
# Handle player death if there is an ongoing death timer
|
# Handle player death if there is an ongoing death timer
|
||||||
if int(data["deathTimer"]) > 0:
|
#if int(data["deathTimer"]) > 0:
|
||||||
controlled_world.player.kill()
|
# controlled_world.player.kill()
|
||||||
elif int(data["deathTimer"]) == 0:
|
#elif int(data["deathTimer"]) == 0:
|
||||||
controlled_world.player.resurrect()
|
# controlled_world.player.resurrect()
|
||||||
|
|
||||||
# Spawn new bullets
|
# Spawn new bullets
|
||||||
for b in data["newBullets"]:
|
#for b in data["serverEvents"]:
|
||||||
var bullet = DanmakuBullet.new()
|
#var bullet = DanmakuBullet.new()
|
||||||
bullet.setup_bullet(
|
#bullet.setup_bullet(
|
||||||
int(b["class"]),
|
#int(b["class"]),
|
||||||
int(b["tick"]),
|
#int(b["tick"]),
|
||||||
b["x"],
|
#b["x"],
|
||||||
b["y"],
|
#b["y"],
|
||||||
b["radius"],
|
#b["radius"],
|
||||||
b["vel_x"],
|
#b["vel_x"],
|
||||||
b["vel_y"])
|
#b["vel_y"])
|
||||||
bullet.texture = load("res://test-bullet.png")
|
#bullet.texture = load("res://test-bullet.png")
|
||||||
bullet.position = bullet.get_current_pos(int(b["tick"]))
|
#bullet.position = bullet.get_current_pos(int(b["tick"]))
|
||||||
|
#
|
||||||
# Reimplemented from ScalableSprite2D here atm
|
## Reimplemented from ScalableSprite2D here atm
|
||||||
var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
|
#var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
|
||||||
bullet.scale = Vector2(scale_ratio, scale_ratio)
|
#bullet.scale = Vector2(scale_ratio, scale_ratio)
|
||||||
bullet.z_index = 4
|
#bullet.z_index = 4
|
||||||
|
#
|
||||||
controlled_world.bullet_manager.add_child(bullet)
|
#controlled_world.bullet_manager.add_child(bullet)
|
||||||
controlled_world.bullet_manager.bullets.append(bullet)
|
#controlled_world.bullet_manager.bullets.append(bullet)
|
||||||
LocalTimer.predicted_tick = int(b["tick"])
|
#LocalTimer.predicted_tick = int(b["tick"])
|
||||||
LocalTimer.delta_counter = 0
|
#LocalTimer.delta_counter = 0
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
class_name Player
|
class_name Player
|
||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
@export var speed = 80
|
|
||||||
@export var controlled = false
|
@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 hurt_collision: DanmakuCircle = DanmakuCircle.new()
|
||||||
var graze_collision: DanmakuCircle = DanmakuCircle.new()
|
var graze_collision: DanmakuCircle = DanmakuCircle.new()
|
||||||
const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18
|
const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18
|
||||||
|
|
@ -12,25 +14,30 @@ var alive: bool = true
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
$BodySprite.scale_sprite(PLAYER_BODY_WIDTH_MULTIPLIER * Globals.SERVER_SIZE.x)
|
$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"):
|
if Input.is_action_pressed("Slow Mode"):
|
||||||
speed = 30
|
slow = true
|
||||||
else:
|
|
||||||
speed = 80
|
|
||||||
|
|
||||||
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):
|
func _physics_process(delta: float):
|
||||||
get_input()
|
get_input(delta)
|
||||||
|
|
||||||
if !alive:
|
#if !alive:
|
||||||
return
|
#return
|
||||||
|
|
||||||
# Bounds checking
|
# Bounds checking
|
||||||
var attempted_position := position + (velocity * delta)
|
#var attempted_position := position + (velocity * delta)
|
||||||
attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
|
#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):
|
func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
|
||||||
position = pos
|
position = pos
|
||||||
|
|
@ -45,7 +52,6 @@ func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
|
||||||
graze_collision.set_radius(grazecircle_radius)
|
graze_collision.set_radius(grazecircle_radius)
|
||||||
$GrazecircleSprite.scale_sprite(grazecircle_radius*2)
|
$GrazecircleSprite.scale_sprite(grazecircle_radius*2)
|
||||||
|
|
||||||
|
|
||||||
func kill():
|
func kill():
|
||||||
if alive == false:
|
if alive == false:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,17 @@ ENV CGO_ENABLED 1
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
COPY . .
|
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/backend.so /nakama/data/modules
|
||||||
#COPY --from=builder /backend/local.yml /nakama/data/
|
#COPY --from=builder /backend/local.yml /nakama/data/
|
||||||
COPY --from=builder /backend/*.json /nakama/data/modules
|
COPY --from=builder /backend/*.json /nakama/data/modules
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/bash" ]
|
||||||
|
|
@ -24,7 +24,7 @@ services:
|
||||||
- "-ecx"
|
- "-ecx"
|
||||||
- >
|
- >
|
||||||
/nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
|
/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"
|
restart: "no"
|
||||||
links:
|
links:
|
||||||
- "cockroachdb:db"
|
- "cockroachdb:db"
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ package ffi
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
// Circles
|
// Circles
|
||||||
type Circle struct {
|
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) {
|
func (c *Circle) UpdatePos(x float64, y float64) {
|
||||||
c.cptr.x = C.double(x)
|
c.cptr.x = C.double(x)
|
||||||
c.cptr.y = C.double(y)
|
c.cptr.y = C.double(y)
|
||||||
|
|
@ -30,6 +39,7 @@ func DestroyCircle(circle *Circle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
C.destroy_circle(circle.cptr)
|
C.destroy_circle(circle.cptr)
|
||||||
|
circle.cptr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Circle) GetPos() (float64, float64) {
|
func (c *Circle) GetPos() (float64, float64) {
|
||||||
|
|
@ -38,20 +48,18 @@ func (c *Circle) GetPos() (float64, float64) {
|
||||||
|
|
||||||
// Bullets
|
// Bullets
|
||||||
type Bullet struct {
|
type Bullet struct {
|
||||||
cptr *C.Bullet
|
cptr *C.Bullet
|
||||||
DeletionTick int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Values for selecting bullet paths
|
// Values for selecting bullet paths
|
||||||
const (
|
const (
|
||||||
BULLET_LINEAR = 0
|
BULLET_LINEAR = 0
|
||||||
ACTIVE_BULLET = -1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, velX float64, velY float64) *Bullet {
|
func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, velX float64, velY float64) *Bullet {
|
||||||
return &Bullet{
|
return &Bullet{
|
||||||
cptr: C.new_bullet(
|
cptr: C.new_bullet(
|
||||||
C.uint8_t(0),
|
C.uint8_t(BULLET_LINEAR),
|
||||||
C.int64_t(tick),
|
C.int64_t(tick),
|
||||||
C.double(spawnX),
|
C.double(spawnX),
|
||||||
C.double(spawnY),
|
C.double(spawnY),
|
||||||
|
|
@ -59,7 +67,29 @@ func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64,
|
||||||
C.double(velX),
|
C.double(velX),
|
||||||
C.double(velY),
|
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)
|
C.destroy_bullet(bullet.cptr)
|
||||||
|
bullet.cptr = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bullet) BeyondKillBoundary(tick int64) bool {
|
func (b *Bullet) BeyondKillBoundary(tick int64) bool {
|
||||||
|
|
@ -94,13 +125,17 @@ func (b *Bullet) GetRadius() float64 {
|
||||||
return float64(b.cptr.radius)
|
return float64(b.cptr.radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bullet) Serialize(tick int64) map[string]any {
|
func (b *Bullet) GetSpawnTick() int64 {
|
||||||
x, y := b.GetPos(tick)
|
return int64(b.cptr.spawn_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bullet) Serialize( /*tick int64*/ ) map[string]any {
|
||||||
|
//x, y := b.GetPos(tick)
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
"class": b.GetType(),
|
"class": b.GetType(),
|
||||||
"tick": tick,
|
"tick": b.GetSpawnTick(),
|
||||||
"x": x,
|
//"x": x,
|
||||||
"y": y,
|
//"y": y,
|
||||||
"radius": b.GetRadius(),
|
"radius": b.GetRadius(),
|
||||||
"vel_x": float64(b.cptr.parameters[0]),
|
"vel_x": float64(b.cptr.parameters[0]),
|
||||||
"vel_y": float64(b.cptr.parameters[1]),
|
"vel_y": float64(b.cptr.parameters[1]),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,18 @@ const (
|
||||||
FINAL_PHASE
|
FINAL_PHASE
|
||||||
MATCH_END
|
MATCH_END
|
||||||
)
|
)
|
||||||
|
const (
|
||||||
|
LEFT = iota
|
||||||
|
RIGHT
|
||||||
|
UP
|
||||||
|
DOWN
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ALIVE = iota
|
||||||
|
DEATH_QUEUED
|
||||||
|
DEAD
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TICK_RATE = 60
|
TICK_RATE = 60
|
||||||
|
|
|
||||||
|
|
@ -31,20 +31,22 @@ func StorePlayerInputs(lobbyState *MatchState, messages []runtime.MatchData, log
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var update ClientUpdate
|
var newInput ClientUpdate
|
||||||
if err := json.Unmarshal(msg.GetData(), &update); err != nil {
|
if err := json.Unmarshal(msg.GetData(), &newInput); err != nil {
|
||||||
(*logger).Warn("Failed to parse input: %v", err)
|
(*logger).Warn("Failed to parse input: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the input in the player's stage state
|
// Store the input in the player's stage state if it arrived in time
|
||||||
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &update)
|
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) {
|
func BroadcastToPresences(tick int64, lobbyState *MatchState, logger *runtime.Logger, dispatcher *runtime.MatchDispatcher) {
|
||||||
for _, v := range lobbyState.presences {
|
for _, v := range lobbyState.presences {
|
||||||
var tickData = v.stageState.MakeServerTick(tick)
|
var tickData = v.stageState.SimulateAndMakeServerTick(tick)
|
||||||
|
|
||||||
data, err := json.Marshal(tickData)
|
data, err := json.Marshal(tickData)
|
||||||
if err != nil {
|
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)
|
StorePlayerInputs(lobbyState, messages, &logger, tick)
|
||||||
|
|
||||||
for _, playerState := range lobbyState.presences {
|
for _, playerState := range lobbyState.presences {
|
||||||
playerState.stageState.MarkBulletsBeyondKillBoundary(tick)
|
playerState.stageState = playerState.stageState.ResolveLockedInState(tick)
|
||||||
playerState.stageState.ProcessPlayerInputs(tick)
|
|
||||||
playerState.stageState.HandleDeath(tick)
|
|
||||||
playerState.stageState.CleanupOldBullets(tick)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
|
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)
|
||||||
|
|
|
||||||
|
|
@ -5,189 +5,186 @@ import (
|
||||||
"danmaku/ffi"
|
"danmaku/ffi"
|
||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlayerStageState struct {
|
type PlayerStageState struct {
|
||||||
hitCol *ffi.Circle
|
lastLockedInTick int64
|
||||||
grazeCol *ffi.Circle
|
hitCol *ffi.Circle
|
||||||
bullets []*ffi.Bullet
|
grazeCol *ffi.Circle
|
||||||
updatePlayerPos bool
|
bullets []*ffi.Bullet
|
||||||
health int
|
playerInputs []*ClientUpdate
|
||||||
graze int
|
deathState int
|
||||||
grazePerTick map[int64]int
|
health int
|
||||||
deathTick int64
|
graze int
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlayerStage() *PlayerStageState {
|
func NewPlayerStage() *PlayerStageState {
|
||||||
return &PlayerStageState{
|
return &PlayerStageState{
|
||||||
hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER),
|
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),
|
grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER),
|
||||||
bullets: []*ffi.Bullet{},
|
bullets: []*ffi.Bullet{},
|
||||||
updatePlayerPos: true,
|
lastLockedInTick: 0,
|
||||||
grazePerTick: make(map[int64]int),
|
health: 3,
|
||||||
health: 3,
|
graze: 0,
|
||||||
deathTick: PLAYER_ALIVE,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
func (s *PlayerStageState) Delete() {
|
||||||
for _, bullet := range s.bullets {
|
for _, bullet := range s.bullets {
|
||||||
ffi.DestroyBullet(bullet)
|
ffi.DestroyBullet(bullet)
|
||||||
}
|
}
|
||||||
|
s.bullets = nil
|
||||||
|
|
||||||
ffi.DestroyCircle(s.hitCol)
|
ffi.DestroyCircle(s.hitCol)
|
||||||
ffi.DestroyCircle(s.grazeCol)
|
ffi.DestroyCircle(s.grazeCol)
|
||||||
|
s.hitCol = nil
|
||||||
|
s.grazeCol = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PlayerStageState) clampedMove(x, y float64) bool {
|
func (s *PlayerStageState) ResolveLockedInState(tick int64) *PlayerStageState {
|
||||||
clampedX := x < 0 || x > STAGE_WIDTH
|
// Rotate the player's input queue and lock in the results of the oldest inputs if they're outside the grace window
|
||||||
clampedY := y < 0 || y > STAGE_HEIGHT
|
// 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))
|
newX := math.Max(0, math.Min(x, STAGE_WIDTH))
|
||||||
newY := math.Max(0, math.Min(y, STAGE_HEIGHT))
|
newY := math.Max(0, math.Min(y, STAGE_HEIGHT))
|
||||||
|
|
||||||
|
clamped := (newX != x) || (newY != y)
|
||||||
|
|
||||||
s.hitCol.UpdatePos(newX, newY)
|
s.hitCol.UpdatePos(newX, newY)
|
||||||
s.grazeCol.UpdatePos(newX, newY)
|
s.grazeCol.UpdatePos(newX, newY)
|
||||||
|
|
||||||
return clampedX || clampedY
|
return clamped
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PlayerStageState) AddBullet(b *ffi.Bullet) {
|
func (s *PlayerStageState) AddBullet(b *ffi.Bullet) {
|
||||||
|
|
@ -228,27 +225,32 @@ func MakeTestFireBullets(tick int64) []*ffi.Bullet {
|
||||||
return bullets
|
return bullets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PlayerStageState) GetBoardStateDiff(tick int64) []map[string]any {
|
// Contains most timed server-side game logic
|
||||||
var newBulletsToBroadcast = []map[string]any{}
|
func (s *PlayerStageState) MakeServerEvents(tick int64) {
|
||||||
if !s.dead {
|
if s.deathState == ALIVE {
|
||||||
newBullets := MakeTestFireBullets(tick)
|
newBullets := MakeTestFireBullets(tick)
|
||||||
|
|
||||||
for _, bullet := range newBullets {
|
for _, bullet := range newBullets {
|
||||||
s.AddBullet(bullet)
|
s.AddBullet(bullet)
|
||||||
newBulletsToBroadcast = append(newBulletsToBroadcast, bullet.Serialize(tick))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newBulletsToBroadcast
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
|
func (s *PlayerStageState) SerializeServerEventsWithinGraceWindow(tick int64) []map[string]any {
|
||||||
hitPosX, hitPosY := s.hitCol.GetPos()
|
var bulletsWithinGraceWindow = []map[string]any{}
|
||||||
grazePosX, grazePosY := s.grazeCol.GetPos()
|
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{
|
var tickData = ServerTickUpdate{
|
||||||
Tick: tick,
|
Tick: tick,
|
||||||
|
|
@ -262,17 +264,11 @@ func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
|
||||||
"y": grazePosY,
|
"y": grazePosY,
|
||||||
"radius": STAGE_WIDTH * PLAYER_GRAZE_COL_RADIUS_MULTIPLIER,
|
"radius": STAGE_WIDTH * PLAYER_GRAZE_COL_RADIUS_MULTIPLIER,
|
||||||
},
|
},
|
||||||
StageStateDiff: s.GetBoardStateDiff(tick),
|
ServerEvents: s.SerializeServerEventsWithinGraceWindow(tick),
|
||||||
ForcePlayerPos: s.updatePlayerPos,
|
Graze: transientCurrentState.graze,
|
||||||
Graze: s.graze + prospectiveGraze,
|
DeathState: transientCurrentState.deathState,
|
||||||
Dead: s.dead,
|
|
||||||
CancelDeath: s.cancelDeath,
|
|
||||||
DeathTick: s.deathTick,
|
|
||||||
UTCTime: float64(time.Now().UnixMilli()) / 1000.0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.cancelDeath = false
|
transientCurrentState.Delete()
|
||||||
s.updatePlayerPos = false
|
|
||||||
|
|
||||||
return &tickData
|
return &tickData
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ type PresenceState struct { // present time! hahahahahahahah!
|
||||||
|
|
||||||
// Struct to serialize client->server updates
|
// Struct to serialize client->server updates
|
||||||
type ClientUpdate struct {
|
type ClientUpdate struct {
|
||||||
X float64 `json:"x"`
|
VelX float64 `json:"vel_x"`
|
||||||
Y float64 `json:"y"`
|
VelY float64 `json:"vel_y"`
|
||||||
Tick int64 `json:"tick"`
|
Tick int64 `json:"tick"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,11 +33,7 @@ type ServerTickUpdate struct {
|
||||||
Tick int64 `json:"tick"`
|
Tick int64 `json:"tick"`
|
||||||
PlayerHitPos map[string]any `json:"playerHitPos"`
|
PlayerHitPos map[string]any `json:"playerHitPos"`
|
||||||
PlayerGrazePos map[string]any `json:"playerGrazePos"`
|
PlayerGrazePos map[string]any `json:"playerGrazePos"`
|
||||||
StageStateDiff []map[string]any `json:"stageStateDiff"`
|
ServerEvents []map[string]any `json:"serverEvents"`
|
||||||
ForcePlayerPos bool `json:"forcePlayerPos"`
|
DeathState int `json:"deathState"`
|
||||||
Dead bool `json:"dead"`
|
|
||||||
CancelDeath bool `json:"cancelDeath"`
|
|
||||||
DeathTick int64 `json:"deathTick"`
|
|
||||||
Graze int `json:"graze"`
|
Graze int `json:"graze"`
|
||||||
UTCTime float64 `json:"utctime"`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in a new issue