From 51842836ce485346b7330115684ecdcdf60ace4b Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 23 Jan 2025 20:24:42 +0100 Subject: [PATCH] First working multiplayer implementation --- project.godot | 4 ++ scenes/maps/el_test.gd | 25 +++++-- scenes/maps/el_test.tscn | 7 ++ scenes/utils/character.tscn | 16 ++++- scenes/utils/menu.gd | 112 +++++++++++++++++--------------- scenes/weapon/bullet.tscn | 2 + scripts/character_controller.gd | 16 +++-- scripts/game_server_manager.gd | 11 ++++ 8 files changed, 129 insertions(+), 64 deletions(-) create mode 100644 scripts/game_server_manager.gd diff --git a/project.godot b/project.godot index 423b03f..286dca5 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://scenes/utils/Menu.tscn" config/features=PackedStringArray("4.3", "Forward Plus") config/icon="res://icon.svg" +[autoload] + +GameServerManager="*res://scripts/game_server_manager.gd" + [display] window/size/viewport_width=1920 diff --git a/scenes/maps/el_test.gd b/scenes/maps/el_test.gd index 33746a8..b948931 100644 --- a/scenes/maps/el_test.gd +++ b/scenes/maps/el_test.gd @@ -6,16 +6,33 @@ var player_side: String @onready var intro_view_port = $Intro/CameraMount/IntroCamera/SubViewportContainer/SubViewport @onready var spawns = $Spawns @onready var root = $'.' +@onready var players = $Players # Called when the node enters the scene tree for the first time. func _ready() -> void: var char : Node3D = null var red_spawn: Node3D = $Spawns/Blue/SpawnArea var position := red_spawn.global_position - char = ResourceLoader.load("res://scenes/utils/character.tscn").instantiate() - char.global_position = position - root.add_child(char) - pass + if multiplayer.is_server(): + for i in GameServerManager.players: + char = ResourceLoader.load("res://scenes/utils/character.tscn").instantiate() + char.name = str(GameServerManager.players[i].name) + char.global_position = position + var my_random_number = RandomNumberGenerator.new().randf_range(-2.0, 2.0) + char.global_position = position + char.global_position.x += my_random_number + $MultiplayerSpawner.spawn(char) + players.add_child(char) +func spawn_player(id: int): + var red_spawn: Node3D = $Spawns/Blue/SpawnArea + var position := red_spawn.global_position + var char = ResourceLoader.load("res://scenes/utils/character.tscn").instantiate() + char.name = str(GameServerManager.players[id].name) + char.global_position = position + var my_random_number = RandomNumberGenerator.new().randf_range(-2.0, 2.0) + char.global_position = position + char.global_position.x += my_random_number + # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: diff --git a/scenes/maps/el_test.tscn b/scenes/maps/el_test.tscn index 68c113d..bf1d80f 100644 --- a/scenes/maps/el_test.tscn +++ b/scenes/maps/el_test.tscn @@ -24,3 +24,10 @@ size = Vector3(0.100647, 1, 6.02112) [node name="SpawnArea" type="CSGBox3D" parent="Spawns/Blue"] transform = Transform3D(5.70162, 0, 0, 0, 1, 0, 0, 0, 7.97817, 21.2099, 1.78438, 1.23551) size = Vector3(0.484497, 1, 5.99213) + +[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://scenes/utils/character.tscn") +spawn_path = NodePath("../Players") +spawn_limit = 4 + +[node name="Players" type="Node3D" parent="."] diff --git a/scenes/utils/character.tscn b/scenes/utils/character.tscn index 140a6b1..85f0e21 100644 --- a/scenes/utils/character.tscn +++ b/scenes/utils/character.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=6 format=3 uid="uid://ccm77j5rkh21w"] +[gd_scene load_steps=7 format=3 uid="uid://ccm77j5rkh21w"] [ext_resource type="Script" path="res://scripts/character_controller.gd" id="1_sue4n"] [ext_resource type="PackedScene" uid="uid://bmqutwuj28san" path="res://scenes/utils/view_model_camera.tscn" id="4_al83x"] @@ -9,6 +9,14 @@ radius = 0.368364 height = 1.8 +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_csl3n"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath("CharacterBody3D:position") +properties/1/spawn = true +properties/1/replication_mode = 1 + [node name="Character" type="Node3D"] [node name="CharacterBody3D" type="CharacterBody3D" parent="."] @@ -23,10 +31,12 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.71312, 0) radius = 0.001 [node name="ViewModelCamera" parent="CharacterBody3D/UpperTorso" instance=ExtResource("4_al83x")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00122696, 0.093623, -0.179357) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00122696, 0.093623, -0.463804) script = ExtResource("4_uwcjh") [node name="Pistol" parent="CharacterBody3D/UpperTorso/ViewModelCamera" instance=ExtResource("5_6k7rq")] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, -0.0287516, -0.136104, -0.276055) -visible = false script = null + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_csl3n") diff --git a/scenes/utils/menu.gd b/scenes/utils/menu.gd index 21260a0..afe50cd 100644 --- a/scenes/utils/menu.gd +++ b/scenes/utils/menu.gd @@ -1,22 +1,8 @@ extends Control +# Autoload named Lobby -# Called when the node enters the scene tree for the first time. -func _ready() -> void: - multiplayer.peer_connected.connect(_on_player_connected) - multiplayer.peer_disconnected.connect(_on_player_disconnected) - multiplayer.connected_to_server.connect(_on_connected_ok) - multiplayer.connection_failed.connect(_on_connected_fail) - multiplayer.server_disconnected.connect(_on_server_disconnected) - pass # Replace with function body. - - -# Called every frame. 'delta' is the elapsed time since the previous frame. -func _process(delta: float) -> void: - pass - -var peer = ENetMultiplayerPeer.new() - +# These signals can be connected to by a UI lobby scene or the game scene. signal player_connected(peer_id, player_info) signal player_disconnected(peer_id) signal server_disconnected @@ -25,20 +11,37 @@ const PORT = 7000 const DEFAULT_SERVER_IP = "127.0.0.1" # IPv4 localhost const MAX_CONNECTIONS = 20 +var players = {} +var my_random_number = RandomNumberGenerator.new().randf_range(-2.0, 2.0) +var player_info = {"name": str(my_random_number)} var players_loaded = 0 -var player_info = {"name": "Name"} -var players = {} +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + multiplayer.peer_connected.connect(_on_player_connected) + multiplayer.peer_disconnected.connect(_on_player_disconnected) + multiplayer.connected_to_server.connect(_on_connected_ok) + multiplayer.connection_failed.connect(_on_connected_fail) + multiplayer.server_disconnected.connect(_on_server_disconnected) -func _on_host_pressed() -> void: +func join_game(address = ""): + if address.is_empty(): + address = DEFAULT_SERVER_IP + var peer = ENetMultiplayerPeer.new() + var error = peer.create_client(address, PORT) + if error: + return error + multiplayer.multiplayer_peer = peer + + +func create_game(): var peer = ENetMultiplayerPeer.new() var error = peer.create_server(PORT, MAX_CONNECTIONS) if error: - print(error) + return error multiplayer.multiplayer_peer = peer - players[1] = player_info - player_connected.emit(1, player_info) + player_connected.emit(peer.get_unique_id(), player_info) func remove_multiplayer_peer(): @@ -47,7 +50,7 @@ func remove_multiplayer_peer(): # When the server decides to start the game from a UI scene, # do Lobby.load_game.rpc(filepath) @rpc("call_local", "reliable") -func load_game(game_scene_path): +func load_game(): get_tree().change_scene_to_file("res://scenes/maps/el_test.tscn") # Every peer will call this when they have loaded the game scene. @@ -55,36 +58,35 @@ func load_game(game_scene_path): func player_loaded(): if multiplayer.is_server(): players_loaded += 1 + players_loaded = players.size() if players_loaded == players.size(): - + #$/root/Game.start_game() players_loaded = 0 - -func _on_join_pressed() -> void: - var address = '127.0.0.1' - var peer = ENetMultiplayerPeer.new() - var error = peer.create_client(address, PORT) - if error: - print(error) - multiplayer.multiplayer_peer = peer - - - -func _on_player_connected(id: Variant, player_info: Variant) -> void: - print(player_info) - _register_player.rpc_id(id, player_info) + +# When a peer connects, send them my player info. +# This allows transfer of all desired data for each player, not only the unique ID. +func _on_player_connected(id): + player_info = {"name": multiplayer.get_unique_id()} + _register_player.rpc_id(1, multiplayer.get_unique_id(), player_info) @rpc("any_peer", "reliable") -func _register_player(new_player_info): - var new_player_id = multiplayer.get_remote_sender_id() - players[new_player_id] = new_player_info - player_connected.emit(new_player_id, new_player_info) +func _register_player(id: int, new_player_info): + var new_player_id: int = 1 + if not multiplayer.is_server(): + new_player_id = 1 + else: + new_player_id = multiplayer.get_remote_sender_id() + players[id] = new_player_info + GameServerManager.players[id] = new_player_info + #player_info = {"name": str(multiplayer.get)} + player_connected.emit(id, new_player_info) - -func _on_player_disconnected(id: Variant) -> void: - pass + +func _on_player_disconnected(id): players.erase(id) player_disconnected.emit(id) + func _on_connected_ok(): var peer_id = multiplayer.get_unique_id() players[peer_id] = player_info @@ -93,17 +95,23 @@ func _on_connected_ok(): func _on_connected_fail(): multiplayer.multiplayer_peer = null - -func _on_server_disconnected() -> void: + + +func _on_server_disconnected(): multiplayer.multiplayer_peer = null players.clear() server_disconnected.emit() +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _on_host_pressed() -> void: + player_info = {"name": str(1)} + _register_player(1, player_info) + create_game() - -@rpc("call_local", "reliable", "any_peer") -func start_game(): - get_tree().change_scene_to_file("res://scenes/maps/el_test.tscn") +func _on_join_pressed() -> void: + join_game() func _on_start_pressed() -> void: - start_game.rpc() + load_game.rpc() +func _process(delta: float) -> void: + pass diff --git a/scenes/weapon/bullet.tscn b/scenes/weapon/bullet.tscn index d2bbc63..1b61697 100644 --- a/scenes/weapon/bullet.tscn +++ b/scenes/weapon/bullet.tscn @@ -37,6 +37,8 @@ surface_material_override/0 = SubResource("StandardMaterial3D_p8o05") [node name="RayCast3D" type="RayCast3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.100459) target_position = Vector3(0, 0, -0.5) +hit_back_faces = false +collide_with_areas = true [node name="GPUParticles3D" type="GPUParticles3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.18476) diff --git a/scripts/character_controller.gd b/scripts/character_controller.gd index e6780c4..28824c0 100644 --- a/scripts/character_controller.gd +++ b/scripts/character_controller.gd @@ -16,7 +16,6 @@ class_name Player extends CharacterBody3D @onready var body: Node3D = $RealBody var jumping: bool = false var mouse_captured: bool = false - var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity") var move_dir: Vector2 # Input direction for movement @@ -32,7 +31,11 @@ var is_crouch: bool = false @onready var character: Node3D = $"." func _ready() -> void: + $"../MultiplayerSynchronizer".set_multiplayer_authority(str($"..".name).to_int()) + enable_camera() capture_mouse() + #print("I am " + str(multiplayer.get_unique_id()) + "I'm controling " + str($"../MultiplayerSynchronizer".get_multiplayer_authority())) + func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion: look_dir = event.relative * 0.001 @@ -44,12 +47,15 @@ func _unhandled_input(event: InputEvent) -> void: if Input.is_action_just_pressed("crouch"): crouch() if Input.is_action_just_released("crouch"): uncrouch() +func enable_camera(): + if str($"..".name).to_int() == multiplayer.get_unique_id(): + $UpperTorso/ViewModelCamera.make_current() func _physics_process(delta: float) -> void: - if mouse_captured: _handle_joypad_camera_rotation(delta) - velocity = _walk(delta) + _gravity(delta) + _jump(delta) - - move_and_slide() + if str($"..".name).to_int() == multiplayer.get_unique_id(): + if mouse_captured: _handle_joypad_camera_rotation(delta) + velocity = _walk(delta) + _gravity(delta) + _jump(delta) + move_and_slide() func capture_mouse() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) diff --git a/scripts/game_server_manager.gd b/scripts/game_server_manager.gd new file mode 100644 index 0000000..16f1dcb --- /dev/null +++ b/scripts/game_server_manager.gd @@ -0,0 +1,11 @@ +extends Node + +var players = {} +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + pass # Replace with function body. + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta: float) -> void: + pass