From 07f02d5a320843555928af6efaad4691688ec7c8 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Sat, 8 Feb 2025 23:01:29 -0800 Subject: [PATCH] Basic synced player movement --- .gitignore | 1 + client/danmaku!/network_manager.gd | 116 +++++++++++++++++++++++++++++ client/danmaku!/player.gd | 102 +++---------------------- client/danmaku!/player.tscn | 6 +- client/danmaku!/testworld.tscn | 6 +- client/project.godot | 28 +++++++ server/main.go | 46 +++++++----- 7 files changed, 193 insertions(+), 112 deletions(-) create mode 100644 client/danmaku!/network_manager.gd diff --git a/.gitignore b/.gitignore index 28d591c..0203e78 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ debug/ release/ target/ +*.tmp lib/ \ No newline at end of file diff --git a/client/danmaku!/network_manager.gd b/client/danmaku!/network_manager.gd new file mode 100644 index 0000000..bcea7fe --- /dev/null +++ b/client/danmaku!/network_manager.gd @@ -0,0 +1,116 @@ +extends Node +var nakama_client: NakamaClient +var nakama_session: NakamaSession +var nakama_socket: NakamaSocket + +const SERVER_WIDTH = 90.0 +const SERVER_HEIGHT = 160.0 + +var predicted_tick = 0 +var delta_counter = 0 +var bullet_lerp_factor := 0.0 +var bullets = [] +var current_match_id = "" + +func _ready() -> void: + print("Attempting auth.") + await attempt_auth() + 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 + + predict_tick_and_broadcast(delta) + + for bullet in bullets: + var prev_pos = bullet.get_current_pos(predicted_tick) + 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) + + #var screen_size = get_viewport().size + #bullets = bullets.filter(func(bullet): + # return bullet.position.x >= 0 and bullet.position.x <= screen_size.x and \ + # bullet.position.y >= 0 and bullet.position.y <= screen_size.y + #) + +func _on_match_state(p_state : NakamaRTAPI.MatchData): + match p_state.op_code: + 2: # Server state update + 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"]) + +func attempt_auth() -> void: + nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http") + nakama_session = await nakama_client.authenticate_device_async(OS.get_unique_id()) + nakama_socket = Nakama.create_socket_from(nakama_client) + + var connected: NakamaAsyncResult = await nakama_socket.connect_async(nakama_session) + if connected.is_exception(): + print("An error occured when creating nakama socket: %s" % connected) + return + print("Oh baby we're ready.") + +func create_and_join_debug_match() -> void: + var response: NakamaAPI.ApiRpc = await nakama_client.rpc_async(nakama_session, "manual_force_create_br_match_rpc") + + if response.is_exception(): + print("An error occurred when calling manual_force_create_br_match_rpc: %s" % response) + return + + var debug_br_match: NakamaRTAPI.Match = await nakama_socket.join_match_async(response.payload) + + if debug_br_match.is_exception(): + print("An error occurred when joining debug BR match: %s" % response) + return + 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}) + 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 a357ab9..4a89c7a 100644 --- a/client/danmaku!/player.gd +++ b/client/danmaku!/player.gd @@ -1,95 +1,17 @@ extends Node2D -var nakama_client: NakamaClient -var nakama_session: NakamaSession -var nakama_socket: NakamaSocket +@export var speed = 400 +var velocity = 0 -const SERVER_WIDTH = 90.0 -const SERVER_HEIGHT = 160.0 - -var predicted_tick = 0 -var delta_counter = 0 -var lerp_factor := 0.0 -var bullets = [] - -func _ready() -> void: - print("Attempting auth.") - await attempt_auth() - 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: - delta_counter += delta - - if delta_counter >= 0.05: - predicted_tick += 1 - delta_counter = 0 # Reset counter - lerp_factor = delta_counter / 0.05 # Normalize factor between 0 and 1 - - for bullet in bullets: - var prev_pos = bullet.get_current_pos(predicted_tick) - var next_pos = bullet.get_current_pos(predicted_tick + 1) - var interpolated_pos = prev_pos.lerp(next_pos, lerp_factor) - - bullet.position = world_to_screen(interpolated_pos) - - #var screen_size = get_viewport().size - #bullets = bullets.filter(func(bullet): - # return bullet.position.x >= 0 and bullet.position.x <= screen_size.x and \ - # bullet.position.y >= 0 and bullet.position.y <= screen_size.y - #) +func get_input(): + if Input.is_action_pressed("Slow Mode"): + speed = 200 + else: + speed = 400 + var input_direction = Input.get_vector("Left", "Right", "Up", "Down") + velocity = input_direction * speed -func _on_match_state(p_state : NakamaRTAPI.MatchData): - match p_state.op_code: - 2: # Spawn bullet - 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"]) - -func attempt_auth() -> void: - nakama_client = Nakama.create_client("defaultkey", "127.0.0.1", 7350, "http") - nakama_session = await nakama_client.authenticate_device_async(OS.get_unique_id()) - nakama_socket = Nakama.create_socket_from(nakama_client) - - var connected: NakamaAsyncResult = await nakama_socket.connect_async(nakama_session) - if connected.is_exception(): - print("An error occured when creating nakama socket: %s" % connected) - return - print("Oh baby we're ready.") - -func create_and_join_debug_match() -> void: - var response: NakamaAPI.ApiRpc = await nakama_client.rpc_async(nakama_session, "manual_force_create_br_match_rpc") - - if response.is_exception(): - print("An error occurred when calling manual_force_create_br_match_rpc: %s" % response) - return - - var debug_br_match: NakamaRTAPI.Match = await nakama_socket.join_match_async(response.payload) - - if debug_br_match.is_exception(): - print("An error occurred when joining debug BR match: %s" % response) - return - -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 _physics_process(delta): + get_input() + position += velocity * delta diff --git a/client/danmaku!/player.tscn b/client/danmaku!/player.tscn index 3f1fbcd..7ba7dea 100644 --- a/client/danmaku!/player.tscn +++ b/client/danmaku!/player.tscn @@ -1,6 +1,10 @@ -[gd_scene load_steps=2 format=3 uid="uid://cd3tqt7hr5pqs"] +[gd_scene load_steps=3 format=3 uid="uid://cd3tqt7hr5pqs"] [ext_resource type="Script" path="res://danmaku!/player.gd" id="1_l6typ"] +[ext_resource type="Texture2D" uid="uid://bs3fntlmlqpt2" path="res://icon.svg" id="2_j7sx3"] [node name="Player" type="Node2D"] script = ExtResource("1_l6typ") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("2_j7sx3") diff --git a/client/danmaku!/testworld.tscn b/client/danmaku!/testworld.tscn index 8cefde0..68778fa 100644 --- a/client/danmaku!/testworld.tscn +++ b/client/danmaku!/testworld.tscn @@ -1,7 +1,11 @@ -[gd_scene load_steps=2 format=3 uid="uid://b1m2pclbncn68"] +[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 5d3f579..97df6d5 100644 --- a/client/project.godot +++ b/client/project.godot @@ -24,3 +24,31 @@ Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd" animation_library={ "animation/fps": 120.0 } + +[input] + +Left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +Right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +Up={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +Down={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +"Slow Mode"={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} diff --git a/server/main.go b/server/main.go index c52d5b3..6b493d6 100644 --- a/server/main.go +++ b/server/main.go @@ -12,7 +12,6 @@ import ( "context" "database/sql" "encoding/json" - "fmt" "math/rand" "slices" @@ -22,7 +21,8 @@ import ( const ( MATCH_LOADING = iota MATCH_START - SPAWN_BULLET + STATE_UPDATE + FINAL_PHASE MATCH_END ) @@ -35,6 +35,11 @@ type PlayerStageState struct { bullets []*C.Bullet } +type PlayerMessageData struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + type PresenceState struct { // present time! hahahahahahahah! presence runtime.Presence stageState PlayerStageState @@ -156,7 +161,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger // Test bullet spawning if tick%1 == 0 { for _, v := range lobbyState.presences { - vel := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 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 vel_y_sign := 2*rand.Intn(2) - 1 @@ -165,8 +171,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger C.int64_t(tick), C.double(STAGE_WIDTH*rand.Float64()), C.double(STAGE_HEIGHT*rand.Float64()), - C.double(float64(vel_x_sign)*vel), - C.double(float64(vel_y_sign)*vel), + C.double(float64(vel_x_sign)*velx), + C.double(float64(vel_y_sign)*vely), ) var x, y C.double @@ -177,8 +183,8 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger "tick": tick, "x": float64(x), "y": float64(y), - "vel_x": float64(vel_x_sign) * vel, - "vel_y": float64(vel_y_sign) * vel, + "vel_x": float64(vel_x_sign) * velx, + "vel_y": float64(vel_y_sign) * vely, } data, err := json.Marshal(bulletData) @@ -187,19 +193,25 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger } else { v.stageState.bullets = append(v.stageState.bullets, bullet) reliable := true - dispatcher.BroadcastMessage(SPAWN_BULLET, data, nil, nil, reliable) + dispatcher.BroadcastMessage(STATE_UPDATE, data, nil, nil, reliable) } } } - // Bullet cleanup - for _, v := range lobbyState.presences { - for _, bullet := range v.stageState.bullets { - var x, y C.double - C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y) - fmt.Printf("Bullet at (%.2f, %.2f)\n", float64(x), float64(y)) + // 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 } + lobbyState.presences[msg.GetSessionId()].stageState.xPos = pos.X + lobbyState.presences[msg.GetSessionId()].stageState.yPos = pos.Y + } + + // 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) @@ -214,12 +226,6 @@ func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger // RPC for force-creating a match for debugging/development, separate from the matchmaking process func ManualForceCreateBRMatchRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { - /*params := make(map[string]interface{}) - - if err := json.Unmarshal([]byte(payload), ¶ms); err != nil { - return "", err - }*/ - modulename := "battle-royale" if matchId, err := nk.MatchCreate(ctx, modulename, make(map[string]interface{})); err != nil {