diff --git a/Cargo.toml b/Cargo.toml index 9a7f25210..503b41745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "itest/rust", "itest/repo-tweak", "examples/dodge-the-creeps/rust", - "examples/hot-reload/rust", + "examples/hot-reload/rust", "examples/multiplayer-lan/rust", ] # Note about Jetbrains IDEs: "IDE Sync" (Refresh Cargo projects) may cause static analysis errors such as diff --git a/examples/multiplayer-lan/godot/.godot/extension_list.cfg b/examples/multiplayer-lan/godot/.godot/extension_list.cfg new file mode 100644 index 000000000..e6cba9255 --- /dev/null +++ b/examples/multiplayer-lan/godot/.godot/extension_list.cfg @@ -0,0 +1 @@ +res://rust.gdextension diff --git a/examples/multiplayer-lan/godot/README.md b/examples/multiplayer-lan/godot/README.md new file mode 100644 index 000000000..c6ad6b820 --- /dev/null +++ b/examples/multiplayer-lan/godot/README.md @@ -0,0 +1,15 @@ +# Multiplayer Bomber + +A multiplayer implementation of the classic bomberman game. +One of the players should press **Host**, while other player(s) +should type in the host's IP address and press **Join**. + +Language: GDScript + +Renderer: Compatibility + +Check out this demo on the asset library: https://godotengine.org/asset-library/asset/139 + +## Screenshots + +![Screenshot](screenshots/bomber.png) diff --git a/examples/multiplayer-lan/godot/bomb.gd b/examples/multiplayer-lan/godot/bomb.gd new file mode 100644 index 000000000..c4d928bb2 --- /dev/null +++ b/examples/multiplayer-lan/godot/bomb.gd @@ -0,0 +1,34 @@ +extends Area2D + +var in_area: Array = [] +var from_player: int + +# Called from the animation. +func explode(): + if not is_multiplayer_authority(): + # Explode only on authority. + return + for p in in_area: + if p.has_method("exploded"): + # Checks if there is wall in between bomb and the object + var world_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state + var query := PhysicsRayQueryParameters2D.create(position, p.position) + query.hit_from_inside = true + var result: Dictionary = world_state.intersect_ray(query) + if not result.collider is TileMap: + # Exploded can only be called by the authority, but will also be called locally. + p.exploded.rpc(from_player) + + +func done(): + if is_multiplayer_authority(): + queue_free() + + +func _on_bomb_body_enter(body): + if not body in in_area: + in_area.append(body) + + +func _on_bomb_body_exit(body): + in_area.erase(body) diff --git a/examples/multiplayer-lan/godot/bomb.tscn b/examples/multiplayer-lan/godot/bomb.tscn new file mode 100644 index 000000000..c4f81eb57 --- /dev/null +++ b/examples/multiplayer-lan/godot/bomb.tscn @@ -0,0 +1,134 @@ +[gd_scene load_steps=9 format=3 uid="uid://enwoaqi0rnei"] + +[ext_resource type="Script" path="res://bomb.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://bdomqql6y50po" path="res://brickfloor.png" id="2"] +[ext_resource type="Texture2D" uid="uid://drfbkdqmj0gu2" path="res://explosion.png" id="3"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_1ih13"] +size = Vector2(16, 192) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_whso6"] +size = Vector2(192, 16) + +[sub_resource type="Curve" id="Curve_4yges"] +max_value = 2.0 +_data = [Vector2(0.00150494, 0.398437), 0.0, 0.0, 0, 0, Vector2(0.0152287, 1.42969), 0.0, 0.0, 0, 0, Vector2(0.478607, 1.30078), 0.0, 0.0, 0, 0, Vector2(1, 0.291016), 0.0, 0.0, 0, 0] +point_count = 4 + +[sub_resource type="Animation" id="Animation_21j5c"] +length = 4.0 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("Sprite:self_modulate") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.4, 0.6, 0.8, 1.1, 1.3, 1.5, 1.8, 1.9, 2, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 3), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 0, +"values": [Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)] +} +tracks/1/type = "method" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(2.8, 3.4), +"transitions": PackedFloat32Array(1, 1), +"values": [{ +"args": [], +"method": &"explode" +}, { +"args": [], +"method": &"done" +}] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("Explosion1:emitting") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 2.8), +"transitions": PackedFloat32Array(1, 1), +"update": 1, +"values": [false, true] +} +tracks/3/type = "value" +tracks/3/imported = false +tracks/3/enabled = true +tracks/3/path = NodePath("Explosion2:emitting") +tracks/3/interp = 1 +tracks/3/loop_wrap = true +tracks/3/keys = { +"times": PackedFloat32Array(0, 2.8), +"transitions": PackedFloat32Array(1, 1), +"update": 1, +"values": [false, true] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_h2w7m"] +_data = { +"anim": SubResource("Animation_21j5c") +} + +[node name="Bomb" type="Area2D"] +monitorable = false +script = ExtResource("1") + +[node name="Sprite" type="Sprite2D" parent="."] +self_modulate = Color(1, 1, 1, 0) +position = Vector2(-2.92606, -2.92606) +texture = ExtResource("2") +region_enabled = true +region_rect = Rect2(144, 0, 48, 48) + +[node name="Shape1" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_1ih13") + +[node name="Shape2" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_whso6") + +[node name="Explosion1" type="CPUParticles2D" parent="."] +emitting = false +lifetime = 0.5 +one_shot = true +explosiveness = 0.95 +texture = ExtResource("3") +emission_shape = 3 +emission_rect_extents = Vector2(80, 1) +gravity = Vector2(0, 0) +initial_velocity_min = 1.0 +initial_velocity_max = 1.0 +angular_velocity_min = 187.35 +angular_velocity_max = 188.35 +scale_amount_curve = SubResource("Curve_4yges") + +[node name="Explosion2" type="CPUParticles2D" parent="."] +rotation = 1.57162 +emitting = false +lifetime = 0.5 +one_shot = true +explosiveness = 0.95 +texture = ExtResource("3") +emission_shape = 3 +emission_rect_extents = Vector2(80, 1) +gravity = Vector2(0, 0) +initial_velocity_min = 1.0 +initial_velocity_max = 1.0 +angular_velocity_min = 187.35 +angular_velocity_max = 188.35 +scale_amount_curve = SubResource("Curve_4yges") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +autoplay = "anim" +libraries = { +"": SubResource("AnimationLibrary_h2w7m") +} + +[connection signal="body_entered" from="." to="." method="_on_bomb_body_enter"] +[connection signal="body_exited" from="." to="." method="_on_bomb_body_exit"] diff --git a/examples/multiplayer-lan/godot/bomb_spawner.gd b/examples/multiplayer-lan/godot/bomb_spawner.gd new file mode 100644 index 000000000..1f406709f --- /dev/null +++ b/examples/multiplayer-lan/godot/bomb_spawner.gd @@ -0,0 +1,14 @@ +extends MultiplayerSpawner + +func _init(): + spawn_function = _spawn_bomb + + +func _spawn_bomb(data): + print(data) + if data.size() != 2 or typeof(data[0]) != TYPE_VECTOR2 or typeof(data[1]) != TYPE_INT: + return null + var bomb = preload("res://bomb.tscn").instantiate() + bomb.position = data[0] + bomb.from_player = data[1] + return bomb diff --git a/examples/multiplayer-lan/godot/brickfloor.png b/examples/multiplayer-lan/godot/brickfloor.png new file mode 100644 index 000000000..20748c012 Binary files /dev/null and b/examples/multiplayer-lan/godot/brickfloor.png differ diff --git a/examples/multiplayer-lan/godot/charwalk.png b/examples/multiplayer-lan/godot/charwalk.png new file mode 100644 index 000000000..198d75f3e Binary files /dev/null and b/examples/multiplayer-lan/godot/charwalk.png differ diff --git a/examples/multiplayer-lan/godot/explosion.png b/examples/multiplayer-lan/godot/explosion.png new file mode 100644 index 000000000..31063cdb1 Binary files /dev/null and b/examples/multiplayer-lan/godot/explosion.png differ diff --git a/examples/multiplayer-lan/godot/gamestate.gd b/examples/multiplayer-lan/godot/gamestate.gd new file mode 100644 index 000000000..299eca44a --- /dev/null +++ b/examples/multiplayer-lan/godot/gamestate.gd @@ -0,0 +1,149 @@ +extends Node + +# Default game server port. Can be any number between 1024 and 49151. +# Not on the list of registered or common ports as of November 2020: +# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers +const DEFAULT_PORT = 10567 + +# Max number of players. +const MAX_PEERS = 12 + +var peer = null + +# Name for my player. +var player_name = "The Warrior" + +# Names for remote players in id:name format. +var players = {} +var players_ready = [] + +# Signals to let lobby GUI know what's going on. +signal player_list_changed() +signal connection_failed() +signal connection_succeeded() +signal game_ended() +signal game_error(what) + +# Callback from SceneTree. +func _player_connected(id): + # Registration of a client beings here, tell the connected player that we are here. + register_player.rpc_id(id, player_name) + + +# Callback from SceneTree. +func _player_disconnected(id): + if has_node("/root/World"): # Game is in progress. + if multiplayer.is_server(): + game_error.emit("Player " + players[id] + " disconnected") + end_game() + else: # Game is not in progress. + # Unregister this player. + unregister_player(id) + + +# Callback from SceneTree, only for clients (not server). +func _connected_ok(): + # We just connected to a server + connection_succeeded.emit() + + +# Callback from SceneTree, only for clients (not server). +func _server_disconnected(): + game_error.emit("Server disconnected") + end_game() + + +# Callback from SceneTree, only for clients (not server). +func _connected_fail(): + multiplayer.set_network_peer(null) # Remove peer + connection_failed.emit() + + +# Lobby management functions. +@rpc("any_peer") +func register_player(new_player_name): + var id = multiplayer.get_remote_sender_id() + players[id] = new_player_name + player_list_changed.emit() + + +func unregister_player(id): + players.erase(id) + player_list_changed.emit() + + +@rpc("call_local") +func load_world(): + # Change scene. + var world = load("res://world.tscn").instantiate() + get_tree().get_root().add_child(world) + get_tree().get_root().get_node("Lobby").hide() + + # Set up score. + world.get_node("Score").add_player(multiplayer.get_unique_id(), player_name) + for pn in players: + world.get_node("Score").add_player(pn, players[pn]) + get_tree().set_pause(false) # Unpause and unleash the game! + + +func host_game(new_player_name): + player_name = new_player_name + peer = ENetMultiplayerPeer.new() + peer.create_server(DEFAULT_PORT, MAX_PEERS) + multiplayer.set_multiplayer_peer(peer) + + +func join_game(ip, new_player_name): + player_name = new_player_name + peer = ENetMultiplayerPeer.new() + peer.create_client(ip, DEFAULT_PORT) + multiplayer.set_multiplayer_peer(peer) + + +func get_player_list(): + return players.values() + + +func get_player_name(): + return player_name + + +func begin_game(): + assert(multiplayer.is_server()) + load_world.rpc() + + var world = get_tree().get_root().get_node("World") + var player_scene = load("res://player.tscn") + + # Create a dictionary with peer id and respective spawn points, could be improved by randomizing. + var spawn_points = {} + spawn_points[1] = 0 # Server in spawn point 0. + var spawn_point_idx = 1 + for p in players: + spawn_points[p] = spawn_point_idx + spawn_point_idx += 1 + + for p_id in spawn_points: + var spawn_pos = world.get_node("SpawnPoints/" + str(spawn_points[p_id])).position + var player = player_scene.instantiate() + player.synced_position = spawn_pos + player.name = str(p_id) + player.set_player_name(player_name if p_id == multiplayer.get_unique_id() else players[p_id]) + world.get_node("Players").add_child(player) + + +func end_game(): + if has_node("/root/World"): # Game is in progress. + # End it + get_node("/root/World").queue_free() + + game_ended.emit() + players.clear() + + +func _ready(): + multiplayer.peer_connected.connect(_player_connected) + multiplayer.peer_disconnected.connect(_player_disconnected) + multiplayer.connected_to_server.connect(_connected_ok) + multiplayer.connection_failed.connect(_connected_fail) + multiplayer.server_disconnected.connect(_server_disconnected) diff --git a/examples/multiplayer-lan/godot/icon.webp b/examples/multiplayer-lan/godot/icon.webp new file mode 100644 index 000000000..b1f9e8bc4 Binary files /dev/null and b/examples/multiplayer-lan/godot/icon.webp differ diff --git a/examples/multiplayer-lan/godot/lobby.gd b/examples/multiplayer-lan/godot/lobby.gd new file mode 100644 index 000000000..90ed898f4 --- /dev/null +++ b/examples/multiplayer-lan/godot/lobby.gd @@ -0,0 +1,92 @@ +extends Control + +func _ready(): + # Called every time the node is added to the scene. + gamestate.connection_failed.connect(_on_connection_failed) + gamestate.connection_succeeded.connect(_on_connection_success) + gamestate.player_list_changed.connect(refresh_lobby) + gamestate.game_ended.connect(_on_game_ended) + gamestate.game_error.connect(_on_game_error) + # Set the player name according to the system username. Fallback to the path. + if OS.has_environment("USERNAME"): + $Connect/Name.text = OS.get_environment("USERNAME") + else: + var desktop_path = OS.get_system_dir(0).replace("\\", "/").split("/") + $Connect/Name.text = desktop_path[desktop_path.size() - 2] + + +func _on_host_pressed(): + if $Connect/Name.text == "": + $Connect/ErrorLabel.text = "Invalid name!" + return + + $Connect.hide() + $Players.show() + $Connect/ErrorLabel.text = "" + + var player_name = $Connect/Name.text + gamestate.host_game(player_name) + refresh_lobby() + + +func _on_join_pressed(): + if $Connect/Name.text == "": + $Connect/ErrorLabel.text = "Invalid name!" + return + + var ip = $Connect/IPAddress.text + if not ip.is_valid_ip_address(): + $Connect/ErrorLabel.text = "Invalid IP address!" + return + + $Connect/ErrorLabel.text = "" + $Connect/Host.disabled = true + $Connect/Join.disabled = true + + var player_name = $Connect/Name.text + gamestate.join_game(ip, player_name) + + +func _on_connection_success(): + $Connect.hide() + $Players.show() + + +func _on_connection_failed(): + $Connect/Host.disabled = false + $Connect/Join.disabled = false + $Connect/ErrorLabel.set_text("Connection failed.") + + +func _on_game_ended(): + show() + $Connect.show() + $Players.hide() + $Connect/Host.disabled = false + $Connect/Join.disabled = false + + +func _on_game_error(errtxt): + $ErrorDialog.dialog_text = errtxt + $ErrorDialog.popup_centered() + $Connect/Host.disabled = false + $Connect/Join.disabled = false + + +func refresh_lobby(): + var players = gamestate.get_player_list() + players.sort() + $Players/List.clear() + $Players/List.add_item(gamestate.get_player_name() + " (You)") + for p in players: + $Players/List.add_item(p) + + $Players/Start.disabled = not multiplayer.is_server() + + +func _on_start_pressed(): + gamestate.begin_game() + + +func _on_find_public_ip_pressed(): + OS.shell_open("https://icanhazip.com/") diff --git a/examples/multiplayer-lan/godot/lobby.tscn b/examples/multiplayer-lan/godot/lobby.tscn new file mode 100644 index 000000000..6be1c572e --- /dev/null +++ b/examples/multiplayer-lan/godot/lobby.tscn @@ -0,0 +1,174 @@ +[gd_scene load_steps=2 format=3 uid="uid://jhdlqsokif5o"] + +[ext_resource type="Script" path="res://lobby.gd" id="1"] + +[node name="Lobby" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource("1") + +[node name="Players" type="Panel" parent="."] +visible = false +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -126.0 +offset_top = -177.5 +offset_right = 126.0 +offset_bottom = 177.5 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="Label" type="Label" parent="Players"] +layout_mode = 0 +offset_left = 26.0 +offset_top = 7.0 +offset_right = 229.0 +offset_bottom = 30.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +text = "Awaiting Players..." +horizontal_alignment = 1 + +[node name="Start" type="Button" parent="Players"] +layout_mode = 0 +offset_left = 68.0 +offset_top = 307.0 +offset_right = 193.0 +offset_bottom = 336.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +text = "START!" + +[node name="List" type="ItemList" parent="Players"] +layout_mode = 0 +offset_left = 25.0 +offset_top = 37.0 +offset_right = 229.0 +offset_bottom = 296.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="PortForward" type="Label" parent="Players"] +layout_mode = 0 +offset_left = -124.0 +offset_top = 375.0 +offset_right = 128.0 +offset_bottom = 429.0 +theme_override_constants/line_spacing = 6 +text = "If you want non-LAN clients to connect, +make sure the port 10567 in UDP +is forwarded on your router." + +[node name="FindPublicIP" type="LinkButton" parent="Players"] +layout_mode = 0 +offset_left = 200.0 +offset_top = 409.5 +offset_right = 408.0 +offset_bottom = 432.5 +text = "Find your public IP address" + +[node name="Connect" type="Panel" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -138.5 +offset_top = -83.5 +offset_right = 138.5 +offset_bottom = 83.5 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="NameLabel" type="Label" parent="Connect"] +layout_mode = 0 +offset_left = 14.0 +offset_top = 11.0 +offset_right = 56.0 +offset_bottom = 25.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +text = "Name:" + +[node name="Name" type="LineEdit" parent="Connect"] +layout_mode = 0 +offset_left = 17.0 +offset_top = 38.0 +offset_right = 173.0 +offset_bottom = 69.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +text = "The Warrior" + +[node name="IPLabel" type="Label" parent="Connect"] +layout_mode = 0 +offset_left = 15.0 +offset_top = 74.0 +offset_right = 57.0 +offset_bottom = 97.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +theme_override_font_sizes/font_size = 16 +text = "IP:" + +[node name="IPAddress" type="LineEdit" parent="Connect"] +layout_mode = 0 +offset_left = 17.0 +offset_top = 102.0 +offset_right = 173.0 +offset_bottom = 133.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +text = "127.0.0.1" + +[node name="Host" type="Button" parent="Connect"] +layout_mode = 0 +offset_left = 181.0 +offset_top = 38.0 +offset_right = 246.0 +offset_bottom = 69.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +text = "Host" + +[node name="Join" type="Button" parent="Connect"] +layout_mode = 0 +offset_left = 181.0 +offset_top = 102.0 +offset_right = 246.0 +offset_bottom = 133.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +text = "Join" + +[node name="ErrorLabel" type="Label" parent="Connect"] +layout_mode = 0 +offset_left = 15.0 +offset_top = 138.0 +offset_right = 257.0 +offset_bottom = 161.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +theme_override_colors/font_color = Color(0.820312, 0.291595, 0.291595, 1) + +[node name="ErrorDialog" type="AcceptDialog" parent="."] + +[connection signal="pressed" from="Players/Start" to="." method="_on_start_pressed"] +[connection signal="pressed" from="Players/FindPublicIP" to="." method="_on_find_public_ip_pressed"] +[connection signal="pressed" from="Connect/Host" to="." method="_on_host_pressed"] +[connection signal="pressed" from="Connect/Join" to="." method="_on_join_pressed"] diff --git a/examples/multiplayer-lan/godot/montserrat.otf b/examples/multiplayer-lan/godot/montserrat.otf new file mode 100644 index 000000000..d5727507c Binary files /dev/null and b/examples/multiplayer-lan/godot/montserrat.otf differ diff --git a/examples/multiplayer-lan/godot/player.gd b/examples/multiplayer-lan/godot/player.gd new file mode 100644 index 000000000..b332fc761 --- /dev/null +++ b/examples/multiplayer-lan/godot/player.gd @@ -0,0 +1,75 @@ +extends CharacterBody2D + +const MOTION_SPEED = 90.0 +const BOMB_RATE = 0.5 + +@export +var synced_position := Vector2() + +@export +var stunned = false + +@onready +var inputs = $Inputs +var last_bomb_time = BOMB_RATE +var current_anim = "" + +func _ready(): + stunned = false + position = synced_position + if str(name).is_valid_int(): + get_node("Inputs/InputsSync").set_multiplayer_authority(str(name).to_int()) + + +func _physics_process(delta): + if multiplayer.multiplayer_peer == null or str(multiplayer.get_unique_id()) == str(name): + # The client which this player represent will update the controls state, and notify it to everyone. + inputs.update() + + if multiplayer.multiplayer_peer == null or is_multiplayer_authority(): + # The server updates the position that will be notified to the clients. + synced_position = position + # And increase the bomb cooldown spawning one if the client wants to. + last_bomb_time += delta + if not stunned and is_multiplayer_authority() and inputs.bombing and last_bomb_time >= BOMB_RATE: + last_bomb_time = 0.0 + get_node("../../BombSpawner").spawn([position, str(name).to_int()]) + else: + # The client simply updates the position to the last known one. + position = synced_position + + if not stunned: + # Everybody runs physics. I.e. clients tries to predict where they will be during the next frame. + velocity = inputs.motion * MOTION_SPEED + move_and_slide() + + # Also update the animation based on the last known player input state + var new_anim = "standing" + + if inputs.motion.y < 0: + new_anim = "walk_up" + elif inputs.motion.y > 0: + new_anim = "walk_down" + elif inputs.motion.x < 0: + new_anim = "walk_left" + elif inputs.motion.x > 0: + new_anim = "walk_right" + + if stunned: + new_anim = "stunned" + + if new_anim != current_anim: + current_anim = new_anim + get_node("anim").play(current_anim) + + +func set_player_name(value): + get_node("label").text = value + + +@rpc("call_local") +func exploded(_by_who): + if stunned: + return + stunned = true + get_node("anim").play("stunned") diff --git a/examples/multiplayer-lan/godot/player.tscn b/examples/multiplayer-lan/godot/player.tscn new file mode 100644 index 000000000..161dcbc81 --- /dev/null +++ b/examples/multiplayer-lan/godot/player.tscn @@ -0,0 +1,201 @@ +[gd_scene load_steps=14 format=3 uid="uid://dviwgv2ty8v6u"] + +[ext_resource type="Texture2D" uid="uid://bsqovikudjr0q" path="res://charwalk.png" id="2"] +[ext_resource type="FontFile" uid="uid://knb8u535cfkw" path="res://montserrat.otf" id="3"] + +[sub_resource type="CircleShape2D" id="1"] +radius = 20.0 + +[sub_resource type="Animation" id="2"] +resource_name = "standing" +length = 0.8 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2, 0.4, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [0, 4, 8, 12] +} + +[sub_resource type="Animation" id="3"] +resource_name = "stunned" +length = 1.2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".:stunned") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(1), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [false] +} +tracks/2/type = "value" +tracks/2/imported = false +tracks/2/enabled = true +tracks/2/path = NodePath("sprite:rotation") +tracks/2/interp = 1 +tracks/2/loop_wrap = true +tracks/2/keys = { +"times": PackedFloat32Array(0, 1), +"transitions": PackedFloat32Array(1, 1), +"update": 0, +"values": [-6.28319, 0.0] +} + +[sub_resource type="Animation" id="4"] +length = 0.8 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2, 0.4, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [0, 4, 8, 12] +} + +[sub_resource type="Animation" id="5"] +length = 0.8 +loop_mode = 1 +step = 0.2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2, 0.4, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [1, 5, 9, 13] +} + +[sub_resource type="Animation" id="6"] +length = 0.8 +loop_mode = 1 +step = 0.2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2, 0.4, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [3, 7, 11, 15] +} + +[sub_resource type="Animation" id="7"] +length = 0.8 +loop_mode = 1 +step = 0.2 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("sprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.2, 0.4, 0.6), +"transitions": PackedFloat32Array(1, 1, 1, 1), +"update": 1, +"values": [2, 6, 10, 14] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_yb83i"] +_data = { +"standing": SubResource("2"), +"stunned": SubResource("3"), +"walk_down": SubResource("4"), +"walk_left": SubResource("5"), +"walk_right": SubResource("6"), +"walk_up": SubResource("7") +} + +[sub_resource type="LabelSettings" id="LabelSettings_5huhx"] +outline_size = 8 +outline_color = Color(0, 0, 0, 1) + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_sh64w"] +properties/0/path = NodePath(".:synced_position") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath("label:text") +properties/1/spawn = true +properties/1/replication_mode = 0 + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_w53uu"] +properties/0/path = NodePath(".:motion") +properties/0/spawn = true +properties/0/replication_mode = 1 +properties/1/path = NodePath(".:bombing") +properties/1/spawn = true +properties/1/replication_mode = 1 + +[node name="player" type="Player"] +z_index = 10 +motion_mode = 1 + +[node name="sprite" type="Sprite2D" parent="."] +position = Vector2(0.0750351, 6.23615) +texture = ExtResource("2") +offset = Vector2(-0.0750351, -6.23615) +hframes = 4 +vframes = 4 + +[node name="shape" type="CollisionShape2D" parent="."] +shape = SubResource("1") + +[node name="anim" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_yb83i") +} + +[node name="label" type="Label" parent="."] +offset_left = -82.0 +offset_top = -35.0 +offset_right = 85.0 +offset_bottom = -14.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +theme_override_fonts/font = ExtResource("3") +theme_override_font_sizes/font_size = 16 +text = "Player 1" +label_settings = SubResource("LabelSettings_5huhx") +horizontal_alignment = 1 + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_sh64w") + +[node name="Inputs" type="PlayerControls" parent="."] + +[node name="InputsSync" type="MultiplayerSynchronizer" parent="Inputs"] +replication_config = SubResource("SceneReplicationConfig_w53uu") diff --git a/examples/multiplayer-lan/godot/player_controls.gd b/examples/multiplayer-lan/godot/player_controls.gd new file mode 100644 index 000000000..e232af36c --- /dev/null +++ b/examples/multiplayer-lan/godot/player_controls.gd @@ -0,0 +1,24 @@ +extends Node + +@export +var motion = Vector2(): + set(value): + # This will be sent by players, make sure values are within limits. + motion = clamp(value, Vector2(-1, -1), Vector2(1, 1)) + +@export +var bombing = false + +func update(): + var m = Vector2() + if Input.is_action_pressed("move_left"): + m += Vector2(-1, 0) + if Input.is_action_pressed("move_right"): + m += Vector2(1, 0) + if Input.is_action_pressed("move_up"): + m += Vector2(0, -1) + if Input.is_action_pressed("move_down"): + m += Vector2(0, 1) + + motion = m + bombing = Input.is_action_pressed("set_bomb") diff --git a/examples/multiplayer-lan/godot/project.godot b/examples/multiplayer-lan/godot/project.godot new file mode 100644 index 000000000..b1375034b --- /dev/null +++ b/examples/multiplayer-lan/godot/project.godot @@ -0,0 +1,89 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Multiplayer Bomber Demo" +config/description="A multiplayer implementation of the classic bomberman game. +One of the players should press \"Host\", while the other +should type in his address and press \"Play\"." +config/tags=PackedStringArray("2d", "demo", "network", "official") +run/main_scene="res://lobby.tscn" +config/features=PackedStringArray("4.3") +config/icon="res://icon.webp" + +[autoload] + +gamestate="*res://gamestate.gd" + +[display] + +window/stretch/mode="canvas_items" +window/stretch/aspect="expand" + +[input] + +move_down={ +"deadzone": 0.2, +"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":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, 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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null) +] +} +move_left={ +"deadzone": 0.2, +"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":65,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null) +, 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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null) +] +} +move_right={ +"deadzone": 0.2, +"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":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, 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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null) +] +} +move_up={ +"deadzone": 0.2, +"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":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +, 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) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null) +] +} +set_bomb={ +"deadzone": 0.2, +"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" + +[replication] + +config={ +"uid://dviwgv2ty8v6u": { +"replicated": true, +"spawn_properties": [], +"sync_interval": 16, +"sync_properties": [&":stunned", &"sprite:hframes", &"sprite:vframes", &":server_position", &"label:text"], +"synced": false +} +} diff --git a/examples/multiplayer-lan/godot/rock.gd b/examples/multiplayer-lan/godot/rock.gd new file mode 100644 index 000000000..b2a86c03c --- /dev/null +++ b/examples/multiplayer-lan/godot/rock.gd @@ -0,0 +1,6 @@ +extends CharacterBody2D + +@rpc("call_local") +func exploded(by_who): + $"../../Score".increase_score(by_who) + $"AnimationPlayer".play("explode") diff --git a/examples/multiplayer-lan/godot/rock.tscn b/examples/multiplayer-lan/godot/rock.tscn new file mode 100644 index 000000000..faf09f788 --- /dev/null +++ b/examples/multiplayer-lan/godot/rock.tscn @@ -0,0 +1,58 @@ +[gd_scene load_steps=6 format=3 uid="uid://bao3yernlglws"] + +[ext_resource type="Script" path="res://rock.gd" id="1"] +[ext_resource type="Texture2D" uid="uid://bdomqql6y50po" path="res://brickfloor.png" id="2"] + +[sub_resource type="RectangleShape2D" id="1"] +size = Vector2(48, 48) + +[sub_resource type="Animation" id="2"] +resource_name = "explode" +tracks/0/type = "method" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(1), +"transitions": PackedFloat32Array(1), +"values": [{ +"args": [], +"method": &"queue_free" +}] +} +tracks/1/type = "value" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath("Sprite:visible") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [false] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_6pqaw"] +_data = { +"explode": SubResource("2") +} + +[node name="Rock" type="CharacterBody2D"] +motion_mode = 1 +script = ExtResource("1") + +[node name="Sprite" type="Sprite2D" parent="."] +texture = ExtResource("2") +region_enabled = true +region_rect = Rect2(96, 0, 48, 48) + +[node name="Shape" type="CollisionShape2D" parent="."] +shape = SubResource("1") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_6pqaw") +} diff --git a/examples/multiplayer-lan/godot/rock_bit.png b/examples/multiplayer-lan/godot/rock_bit.png new file mode 100644 index 000000000..c616e6e67 Binary files /dev/null and b/examples/multiplayer-lan/godot/rock_bit.png differ diff --git a/examples/multiplayer-lan/godot/rust.gdextension b/examples/multiplayer-lan/godot/rust.gdextension new file mode 100644 index 000000000..f5ac6e698 --- /dev/null +++ b/examples/multiplayer-lan/godot/rust.gdextension @@ -0,0 +1,13 @@ +[configuration] +entry_symbol = "gdext_rust_init" +compatibility_minimum = 4.1 + +[libraries] +linux.debug.x86_64 = "res://../../../target/debug/libmultiplayer_lan.so" +linux.release.x86_64 = "res://../../../target/release/libmultiplayer_lan.so" +windows.debug.x86_64 = "res://../../../target/debug/multiplayer_lan.dll" +windows.release.x86_64 = "res://../../../target/release/multiplayer_lan.dll" +macos.debug = "res://../../../target/debug/libmultiplayer_lan.dylib" +macos.release = "res://../../../target/release/libmultiplayer_lan.dylib" +macos.debug.arm64 = "res://../../../target/debug/libmultiplayer_lan.dylib" +macos.release.arm64 = "res://../../../target/release/libmultiplayer_lan.dylib" \ No newline at end of file diff --git a/examples/multiplayer-lan/godot/score.gd b/examples/multiplayer-lan/godot/score.gd new file mode 100644 index 000000000..135842787 --- /dev/null +++ b/examples/multiplayer-lan/godot/score.gd @@ -0,0 +1,45 @@ +extends HBoxContainer + +var player_labels = {} + +func _process(_delta): + var rocks_left = $"../Rocks".get_child_count() + if rocks_left == 0: + var winner_name = "" + var winner_score = 0 + for p in player_labels: + if player_labels[p].score > winner_score: + winner_score = player_labels[p].score + winner_name = player_labels[p].name + + $"../Winner".set_text("THE WINNER IS:\n" + winner_name) + $"../Winner".show() + + +func increase_score(for_who): + assert(for_who in player_labels) + var pl = player_labels[for_who] + pl.score += 1 + pl.label.set_text(pl.name + "\n" + str(pl.score)) + + +func add_player(id, new_player_name): + var l = Label.new() + l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + l.set_text(new_player_name + "\n" + "0") + l.set_h_size_flags(SIZE_EXPAND_FILL) + var font = preload("res://montserrat.otf") + l.set("custom_fonts/font", font) + l.set("custom_font_size/font_size", 18) + add_child(l) + + player_labels[id] = { name = new_player_name, label = l, score = 0 } + + +func _ready(): + $"../Winner".hide() + set_process(true) + + +func _on_exit_game_pressed(): + gamestate.end_game() diff --git a/examples/multiplayer-lan/godot/screenshots/.gdignore b/examples/multiplayer-lan/godot/screenshots/.gdignore new file mode 100644 index 000000000..e69de29bb diff --git a/examples/multiplayer-lan/godot/screenshots/bomber.png b/examples/multiplayer-lan/godot/screenshots/bomber.png new file mode 100644 index 000000000..082934661 Binary files /dev/null and b/examples/multiplayer-lan/godot/screenshots/bomber.png differ diff --git a/examples/multiplayer-lan/godot/tile_scene.tscn b/examples/multiplayer-lan/godot/tile_scene.tscn new file mode 100644 index 000000000..8938349d5 --- /dev/null +++ b/examples/multiplayer-lan/godot/tile_scene.tscn @@ -0,0 +1,23 @@ +[gd_scene load_steps=3 format=3 uid="uid://c5m3rogpaglk1"] + +[ext_resource type="Texture2D" uid="uid://bdomqql6y50po" path="res://brickfloor.png" id="1"] + +[sub_resource type="RectangleShape2D" id="1"] +size = Vector2(48, 48) + +[node name="TileScene" type="Node2D"] + +[node name="Wall" type="Sprite2D" parent="."] +position = Vector2(24, 24) +texture = ExtResource("1") +region_rect = Rect2(0, 0, 48, 48) + +[node name="StaticBody2D" type="StaticBody2D" parent="Wall"] + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Wall/StaticBody2D"] +shape = SubResource("1") + +[node name="Floor" type="Sprite2D" parent="."] +position = Vector2(72, 24) +texture = ExtResource("1") +region_rect = Rect2(48, 0, 48, 48) diff --git a/examples/multiplayer-lan/godot/tileset.tres b/examples/multiplayer-lan/godot/tileset.tres new file mode 100644 index 000000000..1bfd01bdf --- /dev/null +++ b/examples/multiplayer-lan/godot/tileset.tres @@ -0,0 +1,15 @@ +[gd_resource type="TileSet" load_steps=3 format=3 uid="uid://do2l6lpuotti8"] + +[ext_resource type="Texture2D" uid="uid://bdomqql6y50po" path="res://brickfloor.png" id="1"] + +[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_qhkfp"] +texture = ExtResource("1") +texture_region_size = Vector2i(48, 48) +0:0/0 = 0 +0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-24, -24, 24, -24, 24, 24, -24, 24) +1:0/0 = 0 + +[resource] +tile_size = Vector2i(48, 48) +physics_layer_0/collision_layer = 1 +sources/0 = SubResource("TileSetAtlasSource_qhkfp") diff --git a/examples/multiplayer-lan/godot/world.tscn b/examples/multiplayer-lan/godot/world.tscn new file mode 100644 index 000000000..6033fbe18 --- /dev/null +++ b/examples/multiplayer-lan/godot/world.tscn @@ -0,0 +1,308 @@ +[gd_scene load_steps=5 format=3 uid="uid://by3f5o7dyoqx4"] + +[ext_resource type="TileSet" uid="uid://do2l6lpuotti8" path="res://tileset.tres" id="1"] +[ext_resource type="PackedScene" uid="uid://bao3yernlglws" path="res://rock.tscn" id="2"] +[ext_resource type="Script" path="res://score.gd" id="3"] +[ext_resource type="FontFile" uid="uid://knb8u535cfkw" path="res://montserrat.otf" id="4"] + +[node name="World" type="Node2D"] + +[node name="TileMap" type="TileMap" parent="."] +tile_set = ExtResource("1") +rendering_quadrant_size = 48 +format = 2 +layer_0/tile_data = PackedInt32Array(0, 0, 0, 65536, 0, 0, 131072, 0, 0, 196608, 0, 0, 262144, 0, 0, 327680, 0, 0, 393216, 0, 0, 458752, 0, 0, 524288, 0, 0, 589824, 0, 0, 655360, 0, 0, 720896, 0, 0, 786432, 0, 0, 1, 0, 0, 65537, 65536, 0, 131073, 65536, 0, 196609, 65536, 0, 262145, 65536, 0, 327681, 65536, 0, 393217, 65536, 0, 458753, 65536, 0, 524289, 65536, 0, 589825, 65536, 0, 655361, 65536, 0, 720897, 65536, 0, 786433, 0, 0, 2, 0, 0, 65538, 65536, 0, 131074, 0, 0, 196610, 65536, 0, 262146, 0, 0, 327682, 65536, 0, 393218, 0, 0, 458754, 65536, 0, 524290, 0, 0, 589826, 65536, 0, 655362, 0, 0, 720898, 65536, 0, 786434, 0, 0, 3, 0, 0, 65539, 65536, 0, 131075, 65536, 0, 196611, 65536, 0, 262147, 65536, 0, 327683, 65536, 0, 393219, 65536, 0, 458755, 65536, 0, 524291, 0, 0, 589827, 65536, 0, 655363, 65536, 0, 720899, 65536, 0, 786435, 0, 0, 4, 0, 0, 65540, 65536, 0, 131076, 0, 0, 196612, 0, 0, 262148, 0, 0, 327684, 65536, 0, 393220, 0, 0, 458756, 65536, 0, 524292, 0, 0, 589828, 65536, 0, 655364, 0, 0, 720900, 65536, 0, 786436, 0, 0, 5, 0, 0, 65541, 65536, 0, 131077, 65536, 0, 196613, 65536, 0, 262149, 65536, 0, 327685, 65536, 0, 393221, 65536, 0, 458757, 65536, 0, 524293, 65536, 0, 589829, 65536, 0, 655365, 65536, 0, 720901, 65536, 0, 786437, 0, 0, 6, 0, 0, 65542, 65536, 0, 131078, 0, 0, 196614, 65536, 0, 262150, 0, 0, 327686, 0, 0, 393222, 0, 0, 458758, 65536, 0, 524294, 0, 0, 589830, 65536, 0, 655366, 0, 0, 720902, 65536, 0, 786438, 0, 0, 7, 0, 0, 65543, 65536, 0, 131079, 65536, 0, 196615, 65536, 0, 262151, 65536, 0, 327687, 65536, 0, 393223, 65536, 0, 458759, 65536, 0, 524295, 65536, 0, 589831, 65536, 0, 655367, 65536, 0, 720903, 65536, 0, 786439, 0, 0, 8, 0, 0, 65544, 65536, 0, 131080, 0, 0, 196616, 65536, 0, 262152, 0, 0, 327688, 65536, 0, 393224, 0, 0, 458760, 65536, 0, 524296, 0, 0, 589832, 65536, 0, 655368, 0, 0, 720904, 65536, 0, 786440, 0, 0, 9, 0, 0, 65545, 65536, 0, 131081, 65536, 0, 196617, 65536, 0, 262153, 65536, 0, 327689, 65536, 0, 393225, 65536, 0, 458761, 65536, 0, 524297, 65536, 0, 589833, 65536, 0, 655369, 65536, 0, 720905, 65536, 0, 786441, 0, 0, 10, 0, 0, 65546, 65536, 0, 131082, 0, 0, 196618, 0, 0, 262154, 0, 0, 327690, 65536, 0, 393226, 0, 0, 458762, 65536, 0, 524298, 0, 0, 589834, 65536, 0, 655370, 0, 0, 720906, 65536, 0, 786442, 0, 0, 11, 0, 0, 65547, 65536, 0, 131083, 0, 0, 196619, 65536, 0, 262155, 65536, 0, 327691, 65536, 0, 393227, 65536, 0, 458763, 65536, 0, 524299, 65536, 0, 589835, 65536, 0, 655371, 65536, 0, 720907, 65536, 0, 786443, 0, 0, 12, 0, 0, 65548, 65536, 0, 131084, 0, 0, 196620, 65536, 0, 262156, 0, 0, 327692, 65536, 0, 393228, 0, 0, 458764, 65536, 0, 524300, 0, 0, 589836, 65536, 0, 655372, 0, 0, 720908, 65536, 0, 786444, 0, 0, 13, 0, 0, 65549, 65536, 0, 131085, 0, 0, 196621, 65536, 0, 262157, 65536, 0, 327693, 65536, 0, 393229, 0, 0, 458765, 65536, 0, 524301, 0, 0, 589837, 65536, 0, 655373, 65536, 0, 720909, 65536, 0, 786445, 0, 0, 14, 0, 0, 65550, 65536, 0, 131086, 0, 0, 196622, 65536, 0, 262158, 0, 0, 327694, 65536, 0, 393230, 0, 0, 458766, 65536, 0, 524302, 0, 0, 589838, 65536, 0, 655374, 0, 0, 720910, 65536, 0, 786446, 0, 0, 15, 0, 0, 65551, 65536, 0, 131087, 65536, 0, 196623, 65536, 0, 262159, 65536, 0, 327695, 65536, 0, 393231, 0, 0, 458767, 65536, 0, 524303, 65536, 0, 589839, 65536, 0, 655375, 65536, 0, 720911, 65536, 0, 786447, 0, 0, 16, 0, 0, 65552, 65536, 0, 131088, 0, 0, 196624, 65536, 0, 262160, 0, 0, 327696, 65536, 0, 393232, 0, 0, 458768, 65536, 0, 524304, 0, 0, 589840, 65536, 0, 655376, 0, 0, 720912, 65536, 0, 786448, 0, 0, 17, 0, 0, 65553, 65536, 0, 131089, 65536, 0, 196625, 65536, 0, 262161, 65536, 0, 327697, 65536, 0, 393233, 65536, 0, 458769, 65536, 0, 524305, 65536, 0, 589841, 65536, 0, 655377, 65536, 0, 720913, 65536, 0, 786449, 0, 0, 18, 0, 0, 65554, 65536, 0, 131090, 0, 0, 196626, 65536, 0, 262162, 0, 0, 327698, 0, 0, 393234, 0, 0, 458770, 65536, 0, 524306, 0, 0, 589842, 65536, 0, 655378, 0, 0, 720914, 65536, 0, 786450, 0, 0, 19, 0, 0, 65555, 65536, 0, 131091, 65536, 0, 196627, 65536, 0, 262163, 65536, 0, 327699, 65536, 0, 393235, 65536, 0, 458771, 65536, 0, 524307, 65536, 0, 589843, 65536, 0, 655379, 65536, 0, 720915, 65536, 0, 786451, 0, 0, 20, 0, 0, 65556, 0, 0, 131092, 0, 0, 196628, 0, 0, 262164, 0, 0, 327700, 0, 0, 393236, 0, 0, 458772, 0, 0, 524308, 0, 0, 589844, 0, 0, 655380, 0, 0, 720916, 0, 0, 786452, 0, 0, 21, 0, 0, 65557, 0, 0, 131093, 0, 0, 196629, 0, 0, 262165, 0, 0, 327701, 0, 0, 393237, 0, 0, 458773, 0, 0, 524309, 0, 0, 589845, 0, 0, 655381, 0, 0, 720917, 0, 0, 786453, 0, 0) + +[node name="SpawnPoints" type="Node2D" parent="."] + +[node name="0" type="Marker2D" parent="SpawnPoints"] +position = Vector2(72, 72) + +[node name="1" type="Marker2D" parent="SpawnPoints"] +position = Vector2(264, 216) + +[node name="2" type="Marker2D" parent="SpawnPoints"] +position = Vector2(72, 456) + +[node name="3" type="Marker2D" parent="SpawnPoints"] +position = Vector2(360, 552) + +[node name="4" type="Marker2D" parent="SpawnPoints"] +position = Vector2(840, 360) + +[node name="5" type="Marker2D" parent="SpawnPoints"] +position = Vector2(456, 264) + +[node name="6" type="Marker2D" parent="SpawnPoints"] +position = Vector2(696, 264) + +[node name="7" type="Marker2D" parent="SpawnPoints"] +position = Vector2(744, 456) + +[node name="8" type="Marker2D" parent="SpawnPoints"] +position = Vector2(312, 456) + +[node name="9" type="Marker2D" parent="SpawnPoints"] +position = Vector2(696, 72) + +[node name="10" type="Marker2D" parent="SpawnPoints"] +position = Vector2(504, 72) + +[node name="11" type="Marker2D" parent="SpawnPoints"] +position = Vector2(936, 72) + +[node name="Rocks" type="Node2D" parent="."] + +[node name="Rock0" parent="Rocks" instance=ExtResource("2")] +position = Vector2(120, 72) + +[node name="Rock1" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 168) + +[node name="Rock2" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 120) + +[node name="Rock3" parent="Rocks" instance=ExtResource("2")] +position = Vector2(216, 72) + +[node name="Rock4" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 72) + +[node name="Rock5" parent="Rocks" instance=ExtResource("2")] +position = Vector2(312, 72) + +[node name="Rock6" parent="Rocks" instance=ExtResource("2")] +position = Vector2(552, 168) + +[node name="Rock7" parent="Rocks" instance=ExtResource("2")] +position = Vector2(600, 168) + +[node name="Rock8" parent="Rocks" instance=ExtResource("2")] +position = Vector2(552, 216) + +[node name="Rock9" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 312) + +[node name="Rock10" parent="Rocks" instance=ExtResource("2")] +position = Vector2(120, 360) + +[node name="Rock11" parent="Rocks" instance=ExtResource("2")] +position = Vector2(168, 360) + +[node name="Rock12" parent="Rocks" instance=ExtResource("2")] +position = Vector2(216, 360) + +[node name="Rock13" parent="Rocks" instance=ExtResource("2")] +position = Vector2(120, 264) + +[node name="Rock14" parent="Rocks" instance=ExtResource("2")] +position = Vector2(168, 216) + +[node name="Rock15" parent="Rocks" instance=ExtResource("2")] +position = Vector2(72, 360) + +[node name="Rock16" parent="Rocks" instance=ExtResource("2")] +position = Vector2(72, 312) + +[node name="Rock17" parent="Rocks" instance=ExtResource("2")] +position = Vector2(72, 264) + +[node name="Rock18" parent="Rocks" instance=ExtResource("2")] +position = Vector2(360, 360) + +[node name="Rock19" parent="Rocks" instance=ExtResource("2")] +position = Vector2(408, 360) + +[node name="Rock20" parent="Rocks" instance=ExtResource("2")] +position = Vector2(504, 360) + +[node name="Rock21" parent="Rocks" instance=ExtResource("2")] +position = Vector2(600, 360) + +[node name="Rock22" parent="Rocks" instance=ExtResource("2")] +position = Vector2(648, 360) + +[node name="Rock23" parent="Rocks" instance=ExtResource("2")] +position = Vector2(504, 456) + +[node name="Rock24" parent="Rocks" instance=ExtResource("2")] +position = Vector2(552, 456) + +[node name="Rock25" parent="Rocks" instance=ExtResource("2")] +position = Vector2(552, 408) + +[node name="Rock26" parent="Rocks" instance=ExtResource("2")] +position = Vector2(360, 456) + +[node name="Rock27" parent="Rocks" instance=ExtResource("2")] +position = Vector2(360, 504) + +[node name="Rock28" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 504) + +[node name="Rock29" parent="Rocks" instance=ExtResource("2")] +position = Vector2(264, 552) + +[node name="Rock30" parent="Rocks" instance=ExtResource("2")] +position = Vector2(168, 456) + +[node name="Rock31" parent="Rocks" instance=ExtResource("2")] +position = Vector2(168, 504) + +[node name="Rock32" parent="Rocks" instance=ExtResource("2")] +position = Vector2(72, 552) + +[node name="Rock33" parent="Rocks" instance=ExtResource("2")] +position = Vector2(120, 552) + +[node name="Rock34" parent="Rocks" instance=ExtResource("2")] +position = Vector2(504, 552) + +[node name="Rock35" parent="Rocks" instance=ExtResource("2")] +position = Vector2(600, 552) + +[node name="Rock36" parent="Rocks" instance=ExtResource("2")] +position = Vector2(648, 552) + +[node name="Rock37" parent="Rocks" instance=ExtResource("2")] +position = Vector2(648, 504) + +[node name="Rock38" parent="Rocks" instance=ExtResource("2")] +position = Vector2(456, 216) + +[node name="Rock39" parent="Rocks" instance=ExtResource("2")] +position = Vector2(360, 216) + +[node name="Rock40" parent="Rocks" instance=ExtResource("2")] +position = Vector2(360, 168) + +[node name="Rock41" parent="Rocks" instance=ExtResource("2")] +position = Vector2(456, 120) + +[node name="Rock42" parent="Rocks" instance=ExtResource("2")] +position = Vector2(456, 408) + +[node name="Rock43" parent="Rocks" instance=ExtResource("2")] +position = Vector2(456, 456) + +[node name="Rock44" parent="Rocks" instance=ExtResource("2")] +position = Vector2(456, 504) + +[node name="Rock45" parent="Rocks" instance=ExtResource("2")] +position = Vector2(600, 264) + +[node name="Rock46" parent="Rocks" instance=ExtResource("2")] +position = Vector2(600, 72) + +[node name="Rock47" parent="Rocks" instance=ExtResource("2")] +position = Vector2(408, 72) + +[node name="Rock48" parent="Rocks" instance=ExtResource("2")] +position = Vector2(792, 168) + +[node name="Rock49" parent="Rocks" instance=ExtResource("2")] +position = Vector2(744, 168) + +[node name="Rock50" parent="Rocks" instance=ExtResource("2")] +position = Vector2(744, 264) + +[node name="Rock51" parent="Rocks" instance=ExtResource("2")] +position = Vector2(792, 264) + +[node name="Rock52" parent="Rocks" instance=ExtResource("2")] +position = Vector2(744, 360) + +[node name="Rock53" parent="Rocks" instance=ExtResource("2")] +position = Vector2(744, 408) + +[node name="Rock54" parent="Rocks" instance=ExtResource("2")] +position = Vector2(792, 552) + +[node name="Rock55" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 552) + +[node name="Rock56" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 504) + +[node name="Rock57" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 312) + +[node name="Rock58" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 264) + +[node name="Rock59" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 216) + +[node name="Rock60" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 120) + +[node name="Rock61" parent="Rocks" instance=ExtResource("2")] +position = Vector2(792, 72) + +[node name="Rock62" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 72) + +[node name="Rock63" parent="Rocks" instance=ExtResource("2")] +position = Vector2(936, 216) + +[node name="Rock64" parent="Rocks" instance=ExtResource("2")] +position = Vector2(936, 264) + +[node name="Rock65" parent="Rocks" instance=ExtResource("2")] +position = Vector2(936, 408) + +[node name="Rock66" parent="Rocks" instance=ExtResource("2")] +position = Vector2(888, 456) + +[node name="Rock67" parent="Rocks" instance=ExtResource("2")] +position = Vector2(936, 456) + +[node name="Rock68" parent="Rocks" instance=ExtResource("2")] +position = Vector2(792, 456) + +[node name="Rock69" parent="Rocks" instance=ExtResource("2")] +position = Vector2(840, 456) + +[node name="Players" type="Node2D" parent="."] + +[node name="Score" type="HBoxContainer" parent="."] +offset_right = 1024.0 +offset_bottom = 40.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource("3") + +[node name="Winner" type="Label" parent="."] +visible = false +offset_right = 1031.0 +offset_bottom = 617.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +theme_override_constants/shadow_offset_x = 2 +theme_override_constants/shadow_offset_y = 2 +theme_override_fonts/font = ExtResource("4") +text = "THE WINNER IS: +YOU" + +[node name="ExitGame" type="Button" parent="Winner"] +layout_mode = 0 +offset_left = 384.0 +offset_top = 408.0 +offset_right = 649.0 +offset_bottom = 469.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +theme_override_fonts/font = ExtResource("4") +text = "EXIT GAME" + +[node name="Camera2D" type="Camera2D" parent="."] +offset = Vector2(512, 300) + +[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://player.tscn") +spawn_path = NodePath("../Players") + +[node name="BombSpawner" type="BombSpawner" parent="."] +spawn_path = NodePath("..") + +[connection signal="pressed" from="Winner/ExitGame" to="Score" method="_on_exit_game_pressed"] diff --git a/examples/multiplayer-lan/rust/Cargo.toml b/examples/multiplayer-lan/rust/Cargo.toml new file mode 100644 index 000000000..e868e0d14 --- /dev/null +++ b/examples/multiplayer-lan/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "multiplayer-lan" +version = "0.1.0" +edition = "2021" +rust-version = "1.80" +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +godot = { path = "../../../godot", default-features = false} \ No newline at end of file diff --git a/examples/multiplayer-lan/rust/src/bomb_spawner.rs b/examples/multiplayer-lan/rust/src/bomb_spawner.rs new file mode 100644 index 000000000..be08a6dd9 --- /dev/null +++ b/examples/multiplayer-lan/rust/src/bomb_spawner.rs @@ -0,0 +1,45 @@ +use godot::classes::{MultiplayerSpawner, IMultiplayerSpawner}; +use godot::prelude::*; + +use crate::NetworkId; + +#[derive(GodotClass)] +#[class(base=MultiplayerSpawner)] +pub struct BombSpawner { + base: Base, +} + +#[godot_api] +impl BombSpawner { + #[func] + fn _spawn_bomb(data : VariantArray) -> Gd { + godot_print!("spawn"); + if let Ok(position) = data.get(0).unwrap().try_to::() { + if let Ok(from_player) = data.get(1).unwrap().try_to::() { + if let Ok(bomb_scene) = try_load::("res://bomb.tscn") { + godot_print!("{position} {from_player}"); + let mut bomb = bomb_scene.instantiate().unwrap(); + bomb.set("position".into(), &data.get(0).unwrap()); + bomb.set("from_player".into(), &data.get(1).unwrap()); + } + } + } + + Node::new_alloc() + } +} + +#[godot_api] +impl IMultiplayerSpawner for BombSpawner { + fn init(base: Base) -> Self { + Self { + base + } + } + + fn enter_tree(&mut self){ + let spawn_function = self.base().callable("_spawn_bomb"); + self.base_mut().set_spawn_function(spawn_function); + godot_print!("hello"); + } +} diff --git a/examples/multiplayer-lan/rust/src/bullet.rs b/examples/multiplayer-lan/rust/src/bullet.rs new file mode 100644 index 000000000..eca8a6821 --- /dev/null +++ b/examples/multiplayer-lan/rust/src/bullet.rs @@ -0,0 +1,48 @@ +use godot::classes::{CharacterBody2D, ICharacterBody2D, MultiplayerPeer, SceneTreeTimer}; +use godot::prelude::*; + +use crate::NetworkId; + +const SPEED: f32 = 500.0; +const LIFETIME: f64 = 2.0; + +#[derive(GodotClass)] +#[class(base=CharacterBody2D)] +pub struct Bullet { + direction: Vector2, + // who shot the bullet + #[var] + pub network_id: NetworkId, + // dont want the bullets to live forever + timer: OnReady>, + base: Base, +} + +#[godot_api] +impl ICharacterBody2D for Bullet { + fn init(base: Base) -> Self { + Self { + direction: Vector2::new(1., 0.), + network_id: MultiplayerPeer::TARGET_PEER_SERVER, + timer: OnReady::from_base_fn(|base| { + base.get_tree().unwrap().create_timer(LIFETIME).unwrap() + }), + base, + } + } + + fn ready(&mut self) { + self.direction = self.direction.rotated(self.base().get_rotation()); + let velocity = self.direction * SPEED; + self.base_mut().set_velocity(velocity); + } + + fn physics_process(&mut self, _delta: f64) { + // delete bullet once LIFETIME seconds have passed + if self.timer.get_time_left() <= 0. { + self.base_mut().queue_free(); + } + + self.base_mut().move_and_slide(); + } +} diff --git a/examples/multiplayer-lan/rust/src/lib.rs b/examples/multiplayer-lan/rust/src/lib.rs new file mode 100644 index 000000000..8255abf9c --- /dev/null +++ b/examples/multiplayer-lan/rust/src/lib.rs @@ -0,0 +1,15 @@ +use godot::prelude::*; + +type NetworkId = i32; + +mod bullet; +mod multiplayer_controller; +mod bomb_spawner; +mod player; +mod player_controls; +mod scene_manager; + +struct MultiplayerLan; + +#[gdextension] +unsafe impl ExtensionLibrary for MultiplayerLan {} diff --git a/examples/multiplayer-lan/rust/src/multiplayer_controller.rs b/examples/multiplayer-lan/rust/src/multiplayer_controller.rs new file mode 100644 index 000000000..05541b4eb --- /dev/null +++ b/examples/multiplayer-lan/rust/src/multiplayer_controller.rs @@ -0,0 +1,308 @@ +/* +use std::collections::HashMap; + +use godot::classes::{ + Button, Control, ENetMultiplayerPeer, IControl, LineEdit, MultiplayerApi, MultiplayerPeer, + RichTextLabel, +}; +use godot::global::Error; +use godot::obj::WithBaseField; +use godot::prelude::*; + +use crate::scene_manager::SceneManager; +use crate::NetworkId; + +const LOCALHOST: &str = "127.0.0.1"; +const PORT: i32 = 8910; + +#[derive(GodotClass)] +#[class(base=Control)] +pub struct MultiplayerController { + #[export] + address: GString, + port: i32, + #[export] + game_scene: Option>, + player_database: HashMap, + number_of_players_loaded: u32, + multiplayer: OnReady>, + base: Base, +} + +#[godot_api] +impl MultiplayerController { + // called when a new "peer" gets connected with the server. Both client and server get notified about this + #[func] + fn on_peer_connected(&self, network_id: NetworkId) { + godot_print!("Player connected: {network_id}"); + } + + // called when a new "peer" gets disconnected with the server. Both client and server get notified about this + #[func] + fn on_peer_disconnected(&mut self, network_id: NetworkId) { + godot_print!("Player Disconnected: {network_id}"); + self.player_database.remove(&network_id); + // TODO: delete player from game when player leaves lobby + } + + // called only from client to send information to server through send_player_information + #[func] + fn on_connected_to_server(&mut self) { + godot_print!("Connected to Server!"); + // send information to server + let username = self + .base() + .get_node_as::("UsernameLineEdit") + .get_text(); + let network_id = self.multiplayer.get_unique_id(); + // server always has peer id of TARGET_PEER_SERVER (1) + self.base_mut().rpc_id( + MultiplayerPeer::TARGET_PEER_SERVER.into(), + "send_player_information".into(), + &[Variant::from(username), Variant::from(network_id)], + ); + } + + // called only from clients + #[func] + fn on_connection_failed(&self) { + godot_print!("Couldn't Connect"); + } + + // utility function that converts our player database hashmap to a string + fn player_database_to_string(&self) -> String { + let mut string = String::default(); + for (network_id, username) in self.player_database.iter() { + string.push_str(&format!( + "network_id: {network_id}, username: {username} \n" + )); + } + string + } + + // this function should first be called by the player connecting to the server + // and then, the server should call this function on all the other players to propagate the information out + // this should result in each player having a fully populated player database containing everyone else in the lobby + #[rpc(any_peer)] + fn send_player_information(&mut self, name: GString, network_id: NetworkId) { + // insert new player data with network_id if it doesn't already exist + self.player_database.entry(network_id).or_insert(name); + + // print player information onto multiplayer log + let mut multiplayer_log = self + .base_mut() + .get_node_as::("MultiplayerLog"); + multiplayer_log.set_text(self.player_database_to_string().into()); + + if self.multiplayer.is_server() { + for (id, username) in self.player_database.clone().into_iter() { + godot_print!("sending player {id} data"); + self.base_mut().rpc( + "send_player_information".into(), + &[Variant::from(username), Variant::from(id)], + ); + } + } + } + + #[rpc(any_peer, call_local, reliable)] + fn load_game(&mut self) { + // start up game scene + let mut scene = self + .game_scene + .as_mut() + .unwrap() + .instantiate_as::(); + // have to put this into its own scope to avoid borrowing self as immutable when its already mutable + // note: you could also use drop(..) to drop reference to base + { + let mut base = self.base_mut(); + base.get_tree() + .unwrap() + .get_root() + .unwrap() + .add_child(scene.clone()); + // hide multiplayer menu + base.hide(); + } + + // add players to scene + let mut player_ids = Vec::::new(); + for (&network_id, username) in &self.player_database { + scene.bind_mut().add_player(network_id, username.clone()); + player_ids.push(network_id); + } + + if self.multiplayer.is_server() { + for id in player_ids { + // don't call rpc on server + if id == MultiplayerPeer::TARGET_PEER_SERVER { + continue; + } + // force other clients to also load the game up + self.base_mut().rpc_id(id.into(), "load_game".into(), &[]); + } + } + } + + // callback from scene_manager, tells the multiplayer_controller that this player has loaded in + // Every peer will call this when they have loaded the game scene. + #[rpc(any_peer, call_local, reliable)] + fn load_in_player(&mut self) { + // if server, start up game on everyone else's client + if self.multiplayer.is_server() { + let network_id = self.multiplayer.get_remote_sender_id(); + // only load in players that are actually in the player database + if !self.player_database.contains_key(&network_id) { + return; + } + godot_print!("loading in player {network_id}"); + self.number_of_players_loaded += 1; + // start game once everyone is loaded in + if self.number_of_players_loaded == self.player_database.len() as u32 { + let mut game_scene = self + .base_mut() + .get_tree() + .unwrap() + .get_root() + .unwrap() + .get_node_as::("Game"); + game_scene.bind_mut().start_game(); + } + } + } + + // set up server + #[func] + fn host_game(&mut self) { + let mut peer = ENetMultiplayerPeer::new_gd(); + let error = peer.create_server(self.port); + if error != Error::OK { + godot_print!("cannot host"); + return; + } + peer.get_host() + .unwrap() + .compress(godot::classes::enet_connection::CompressionMode::RANGE_CODER); + + self.multiplayer.set_multiplayer_peer(peer); + godot_print!("Waiting For Players!"); + } + + #[func] + fn on_host_button_down(&mut self) { + self.base_mut() + .get_node_as::