Archived
1
0
Fork 0

Player bounds checking, better scaling calc on client

This commit is contained in:
Sebastian Benjamin 2025-02-11 21:42:11 -08:00
parent 07f02d5a32
commit 901c4b773b
11 changed files with 209 additions and 118 deletions

View file

@ -0,0 +1,29 @@
[gd_scene load_steps=4 format=3 uid="uid://b1m2pclbncn68"]
[ext_resource type="Script" path="res://danmaku!/scaling.gd" id="1_mxxs1"]
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="2_d6n3g"]
[ext_resource type="Script" path="res://danmaku!/network_manager.gd" id="3_4fnyw"]
[node name="Board" type="AspectRatioContainer"]
anchors_preset = -1
anchor_right = 0.263889
anchor_bottom = 0.257716
offset_right = 14.0
offset_bottom = 27.0
ratio = 0.5625
script = ExtResource("1_mxxs1")
metadata/_edit_use_anchors_ = true
[node name="ReferenceRect" type="ReferenceRect" parent="."]
layout_mode = 2
border_color = Color(0.995542, 0.845388, 0.421763, 1)
editor_only = false
[node name="Container" type="Node2D" parent="ReferenceRect"]
scale = Vector2(1.2125, 1.2125)
[node name="Player" parent="ReferenceRect/Container" instance=ExtResource("2_d6n3g")]
[node name="NetworkManager" type="Node2D" parent="ReferenceRect/Container" node_paths=PackedStringArray("player")]
script = ExtResource("3_4fnyw")
player = NodePath("../Player")

21
client/danmaku!/Game.tscn Normal file
View file

@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3 uid="uid://3a8txh83qfu5"]
[ext_resource type="PackedScene" uid="uid://b1m2pclbncn68" path="res://danmaku!/Board.tscn" id="1_pv3ov"]
[node name="Game" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Board" parent="." instance=ExtResource("1_pv3ov")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 0.0
offset_bottom = 0.0
grow_horizontal = 2
grow_vertical = 2

View file

@ -0,0 +1,2 @@
class_name Globals
const SERVER_SIZE = Vector2(90.0, 160.0)

View file

@ -1,10 +1,9 @@
extends Node
extends Node2D
var nakama_client: NakamaClient
var nakama_session: NakamaSession
var nakama_socket: NakamaSocket
const SERVER_WIDTH = 90.0
const SERVER_HEIGHT = 160.0
@export var player: Player
var predicted_tick = 0
var delta_counter = 0
@ -18,7 +17,7 @@ func _ready() -> void:
print("Attempting to create debug match.")
await create_and_join_debug_match()
nakama_socket.received_match_state.connect(self._on_match_state)
func _process(delta: float) -> void:
if current_match_id == "":
return
@ -30,7 +29,7 @@ func _process(delta: float) -> void:
var next_pos = bullet.get_current_pos(predicted_tick + 1)
var interpolated_pos = prev_pos.lerp(next_pos, bullet_lerp_factor)
bullet.position = world_to_screen(interpolated_pos)
bullet.position = interpolated_pos
#var screen_size = get_viewport().size
#bullets = bullets.filter(func(bullet):
@ -40,22 +39,28 @@ func _process(delta: float) -> void:
func _on_match_state(p_state : NakamaRTAPI.MatchData):
match p_state.op_code:
2: # Server state update
2:
var data = JSON.parse_string(p_state.data)
var bullet = DanmakuBullet.new()
bullet.setup_bullet(
int(data["class"]),
int(data["tick"]),
data["x"],
data["y"],
data["vel_x"],
data["vel_y"])
bullet.position = world_to_screen(bullet.get_current_pos(int(data["tick"])))
bullet.texture = load("res://test-bullet.png")
add_child(bullet)
bullets.append(bullet)
#delta_counter = 0
#predicted_tick = int(data["tick"])
# Set player position given server bounds-checking
if data["forcePlayerPos"]:
player.position = Vector2(float(data["playerPos"]["x"]), float(data["playerPos"]["y"]))
# 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["vel_x"],
b["vel_y"])
bullet.texture = load("res://test-bullet.png")
bullet.position = bullet.get_current_pos(int(b["tick"]))
bullet.scale = Vector2(0.2, 0.2)
add_child(bullet)
bullets.append(bullet)
#predicted_tick = int(b["tick"])
func attempt_auth() -> void:
nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http")
@ -83,34 +88,14 @@ func create_and_join_debug_match() -> void:
else:
current_match_id = response.payload
func world_to_screen(server_pos: Vector2) -> Vector2:
var screen_size = get_viewport().size
var scale_x = screen_size.x / SERVER_WIDTH
var scale_y = screen_size.y / SERVER_HEIGHT
var client_x = server_pos.x * scale_x
var client_y = server_pos.y * scale_y
return Vector2(client_x, client_y)
func screen_to_world(client_pos: Vector2) -> Vector2:
var screen_size = get_viewport().size
var scale_x = SERVER_WIDTH / screen_size.x
var scale_y = SERVER_HEIGHT / screen_size.y
var server_x = client_pos.x * scale_x
var server_y = client_pos.y * scale_y
return Vector2(server_x, server_y)
func predict_tick_and_broadcast(delta):
delta_counter += delta
# New tick, broadcast player inputs
if delta_counter >= 0.05:
predicted_tick += 1
delta_counter = 0
var position = screen_to_world(get_node("../Player").position)
var json_string = JSON.stringify({"x": position.x, "y": position.y})
delta_counter -= 0.05
var pos = get_node("../Player").position
var json_string = JSON.stringify({"x": pos.x, "y": pos.y})
nakama_socket.send_match_state_async(current_match_id, 0, json_string)
bullet_lerp_factor = delta_counter / 0.05

View file

@ -1,17 +1,22 @@
class_name Player
extends Node2D
@export var speed = 400
var velocity = 0
@export var speed = 80
var velocity := Vector2.ZERO
func get_input():
if Input.is_action_pressed("Slow Mode"):
speed = 200
speed = 40
else:
speed = 400
speed = 80
var input_direction = Input.get_vector("Left", "Right", "Up", "Down")
velocity = input_direction * speed
velocity = Input.get_vector("Left", "Right", "Up", "Down") * speed
func _physics_process(delta):
func _physics_process(delta: float):
get_input()
position += velocity * delta
# Bounds checking
var attempted_position := position + (velocity * delta)
attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
position = attempted_position

View file

@ -7,4 +7,5 @@
script = ExtResource("1_l6typ")
[node name="Sprite2D" type="Sprite2D" parent="."]
scale = Vector2(0.09, 0.09)
texture = ExtResource("2_j7sx3")

View file

@ -0,0 +1,6 @@
@tool
extends AspectRatioContainer
func _process(delta: float) -> void:
var board_screen_size = $ReferenceRect.get_rect()
$ReferenceRect/Container.scale = Vector2(board_screen_size.size.x / Globals.SERVER_SIZE.x, board_screen_size.size.y / Globals.SERVER_SIZE.y)

View file

@ -1,11 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://b1m2pclbncn68"]
[ext_resource type="PackedScene" uid="uid://cd3tqt7hr5pqs" path="res://danmaku!/player.tscn" id="1_jeq34"]
[ext_resource type="Script" path="res://danmaku!/network_manager.gd" id="2_3453q"]
[node name="Testworld" type="Node2D"]
[node name="Player" parent="." instance=ExtResource("1_jeq34")]
[node name="NetworkManager" type="Node" parent="."]
script = ExtResource("2_3453q")

View file

@ -11,7 +11,7 @@ config_version=5
[application]
config/name="danmaku!"
run/main_scene="res://danmaku!/testworld.tscn"
run/main_scene="res://danmaku!/Game.tscn"
config/features=PackedStringArray("4.3", "Forward Plus")
config/icon="res://icon.svg"

View file

@ -1,11 +0,0 @@
package main
const (
BULLET_LINEAR = 0
)
const (
STAGE_WIDTH float64 = 90.0
STAGE_HEIGHT float64 = 160.0
BULLET_KILL_BUFFER_WIDTH float64 = 16.0
)

View file

@ -12,6 +12,7 @@ import (
"context"
"database/sql"
"encoding/json"
"math"
"math/rand"
"slices"
@ -26,26 +27,42 @@ const (
MATCH_END
)
const (
BULLET_LINEAR = 0
)
const (
STAGE_WIDTH float64 = 90.0
STAGE_HEIGHT float64 = 160.0
BULLET_KILL_BUFFER_WIDTH float64 = 16.0
)
// Interface for registering match handlers
type BattleRoyaleMatch struct{}
type PlayerStageState struct {
xPos float64
yPos float64
bullets []*C.Bullet
}
type PlayerMessageData struct {
type Position struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type PlayerStageState struct {
pos Position
bullets []*C.Bullet
updatePlayerPos bool
}
type GameTickUpdate struct {
Tick int64 `json:"tick"`
PlayerPos Position `json:"playerPos"`
NewBullets []map[string]interface{} `json:"newBullets"`
ForcePlayerPos bool `json:"forcePlayerPos"`
}
type PresenceState struct { // present time! hahahahahahahah!
presence runtime.Presence
stageState PlayerStageState
}
// In-memory game state
type BattleRoyaleMatchState struct {
tickRate int
currentMatchPhase int
@ -92,9 +109,12 @@ func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger
lobbyState.presences[presences[i].GetSessionId()] = &PresenceState{
presence: presences[i],
stageState: PlayerStageState{
xPos: STAGE_WIDTH * 0.5,
yPos: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
bullets: []*C.Bullet{},
pos: Position{
X: STAGE_WIDTH * 0.5,
Y: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
},
bullets: []*C.Bullet{},
updatePlayerPos: false,
},
}
}
@ -111,7 +131,16 @@ func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logge
}
for i := 0; i < len(presences); i++ {
delete(lobbyState.presences, presences[i].GetSessionId())
sessionID := presences[i].GetSessionId()
playerState, exists := lobbyState.presences[sessionID]
if exists {
for _, bullet := range playerState.stageState.bullets {
C.destroy_bullet(bullet)
}
delete(lobbyState.presences, sessionID)
}
}
return lobbyState
@ -158,9 +187,52 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
return nil
}
// Test bullet spawning
if tick%1 == 0 {
for _, v := range lobbyState.presences {
// Respond to player input
for _, msg := range messages {
// Validate player existence
_, exists := lobbyState.presences[msg.GetSessionId()]
if !exists {
logger.Warn("Received input for non-existent player session ID: %v", msg.GetSessionId())
continue
}
// Parse player message
var pos Position
if err := json.Unmarshal(msg.GetData(), &pos); err != nil {
logger.Warn("Failed to parse input: %v", err)
continue
}
// Player movement bounds detection
clampedX := pos.X < 0 || pos.X > STAGE_WIDTH
clampedY := pos.Y < 0 || pos.Y > STAGE_HEIGHT
pos.X = math.Max(0, math.Min(pos.X, STAGE_WIDTH))
pos.Y = math.Max(0, math.Min(pos.Y, STAGE_HEIGHT))
lobbyState.presences[msg.GetSessionId()].stageState.pos.X = pos.X
lobbyState.presences[msg.GetSessionId()].stageState.pos.Y = pos.Y
if clampedX || clampedY {
lobbyState.presences[msg.GetSessionId()].stageState.updatePlayerPos = true
}
}
// Compute and broadcast per-presence state
for _, v := range lobbyState.presences {
// Clean up bullets when they pass off the board
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
C.destroy_bullet(b)
return true
}
return false
})
var newBulletsToBroadcast = []map[string]interface{}{}
// Test bullet spawning
if tick%1 == 0 {
velx := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
vely := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
vel_x_sign := 2*rand.Intn(2) - 1
@ -175,6 +247,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
C.double(float64(vel_y_sign)*vely),
)
v.stageState.bullets = append(v.stageState.bullets, bullet)
var x, y C.double
C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
@ -187,38 +261,28 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger
"vel_y": float64(vel_y_sign) * vely,
}
data, err := json.Marshal(bulletData)
if err != nil {
logger.Error("Error marshalling bullet data", err)
} else {
v.stageState.bullets = append(v.stageState.bullets, bullet)
reliable := true
dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
}
}
}
// Respond to player input
for _, msg := range messages {
var pos PlayerMessageData
if err := json.Unmarshal(msg.GetData(), &pos); err != nil {
logger.Warn("Failed to parse input: %v", err)
continue
newBulletsToBroadcast = append(newBulletsToBroadcast, bulletData)
}
lobbyState.presences[msg.GetSessionId()].stageState.xPos = pos.X
lobbyState.presences[msg.GetSessionId()].stageState.yPos = pos.Y
}
var tickData = GameTickUpdate{
Tick: tick,
PlayerPos: Position{
X: v.stageState.pos.X,
Y: v.stageState.pos.Y,
},
NewBullets: newBulletsToBroadcast,
ForcePlayerPos: v.stageState.updatePlayerPos,
}
// Bullet cleanup when off board
for _, v := range lobbyState.presences {
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
C.destroy_bullet(b)
return true
}
return false
})
v.stageState.updatePlayerPos = false
data, err := json.Marshal(tickData)
if err != nil {
logger.Error("Error marshalling bullet data", err)
} else {
reliable := true
dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable)
}
}
return lobbyState