From 901c4b773b971cf6fe32a06a3055986558979ba0 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Tue, 11 Feb 2025 21:42:11 -0800 Subject: [PATCH] Player bounds checking, better scaling calc on client --- client/danmaku!/Board.tscn | 29 ++++++ client/danmaku!/Game.tscn | 21 ++++ client/danmaku!/globals.gd | 2 + client/danmaku!/network_manager.gd | 71 ++++++-------- client/danmaku!/player.gd | 21 ++-- client/danmaku!/player.tscn | 1 + client/danmaku!/scaling.gd | 6 ++ client/danmaku!/testworld.tscn | 11 --- client/project.godot | 2 +- server/consts.go | 11 --- server/main.go | 152 ++++++++++++++++++++--------- 11 files changed, 209 insertions(+), 118 deletions(-) create mode 100644 client/danmaku!/Board.tscn create mode 100644 client/danmaku!/Game.tscn create mode 100644 client/danmaku!/globals.gd create mode 100644 client/danmaku!/scaling.gd delete mode 100644 client/danmaku!/testworld.tscn delete mode 100644 server/consts.go diff --git a/client/danmaku!/Board.tscn b/client/danmaku!/Board.tscn new file mode 100644 index 0000000..5af4920 --- /dev/null +++ b/client/danmaku!/Board.tscn @@ -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") diff --git a/client/danmaku!/Game.tscn b/client/danmaku!/Game.tscn new file mode 100644 index 0000000..9d9e522 --- /dev/null +++ b/client/danmaku!/Game.tscn @@ -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 diff --git a/client/danmaku!/globals.gd b/client/danmaku!/globals.gd new file mode 100644 index 0000000..e23bf41 --- /dev/null +++ b/client/danmaku!/globals.gd @@ -0,0 +1,2 @@ +class_name Globals +const SERVER_SIZE = Vector2(90.0, 160.0) diff --git a/client/danmaku!/network_manager.gd b/client/danmaku!/network_manager.gd index bcea7fe..5d6508d 100644 --- a/client/danmaku!/network_manager.gd +++ b/client/danmaku!/network_manager.gd @@ -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 diff --git a/client/danmaku!/player.gd b/client/danmaku!/player.gd index 4a89c7a..86da5c5 100644 --- a/client/danmaku!/player.gd +++ b/client/danmaku!/player.gd @@ -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 diff --git a/client/danmaku!/player.tscn b/client/danmaku!/player.tscn index 7ba7dea..4f47bee 100644 --- a/client/danmaku!/player.tscn +++ b/client/danmaku!/player.tscn @@ -7,4 +7,5 @@ script = ExtResource("1_l6typ") [node name="Sprite2D" type="Sprite2D" parent="."] +scale = Vector2(0.09, 0.09) texture = ExtResource("2_j7sx3") diff --git a/client/danmaku!/scaling.gd b/client/danmaku!/scaling.gd new file mode 100644 index 0000000..6cc98d6 --- /dev/null +++ b/client/danmaku!/scaling.gd @@ -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) diff --git a/client/danmaku!/testworld.tscn b/client/danmaku!/testworld.tscn deleted file mode 100644 index 68778fa..0000000 --- a/client/danmaku!/testworld.tscn +++ /dev/null @@ -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") diff --git a/client/project.godot b/client/project.godot index 97df6d5..6da05b0 100644 --- a/client/project.godot +++ b/client/project.godot @@ -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" diff --git a/server/consts.go b/server/consts.go deleted file mode 100644 index ed3ca32..0000000 --- a/server/consts.go +++ /dev/null @@ -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 -) diff --git a/server/main.go b/server/main.go index 6b493d6..18fc7d8 100644 --- a/server/main.go +++ b/server/main.go @@ -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