diff --git a/Main.tscn b/Main.tscn index c510508..ee4959b 100644 --- a/Main.tscn +++ b/Main.tscn @@ -1,13 +1,9 @@ -[gd_scene load_steps=12 format=2] +[gd_scene load_steps=7 format=2] -[ext_resource path="res://src/actors/players/playerE/PlayerE.tscn" type="PackedScene" id=1] -[ext_resource path="res://src/levels/TestBox.tscn" type="PackedScene" id=2] -[ext_resource path="res://src/ui/HUD.tscn" type="PackedScene" id=3] +[ext_resource path="res://src/World.tscn" type="PackedScene" id=1] [ext_resource path="res://src/ui/scene_transition.tscn" type="PackedScene" id=4] -[ext_resource path="res://src/classes/camera_guide.gd" type="Script" id=5] -[ext_resource path="res://src/Camera2D.gd" type="Script" id=6] [ext_resource path="res://src/Main.gd" type="Script" id=7] -[ext_resource path="res://assets/items/mushroom_icon.png" type="Texture" id=8] +[ext_resource path="res://src/ui/lobby.tscn" type="PackedScene" id=9] [sub_resource type="Shader" id=2] code = "shader_type canvas_item; @@ -22,9 +18,6 @@ void vertex() { shader = SubResource( 2 ) shader_param/cam_offset = Vector2( 0, 0 ) -[sub_resource type="RectangleShape2D" id=1] -extents = Vector2( 160, 90 ) - [node name="Main" type="Node2D"] script = ExtResource( 7 ) @@ -42,41 +35,8 @@ size = Vector2( 320, 180 ) handle_input_locally = false render_target_update_mode = 3 +[node name="Lobby" parent="ViewportContainer/Viewport" instance=ExtResource( 9 )] + [node name="SceneTransition" parent="ViewportContainer/Viewport" instance=ExtResource( 4 )] -[node name="TestBox" parent="ViewportContainer/Viewport" instance=ExtResource( 2 )] -__meta__ = { -"_edit_lock_": true -} - -[node name="PlayerE" parent="ViewportContainer/Viewport" instance=ExtResource( 1 )] -position = Vector2( 112, 56 ) - -[node name="PlayerE2" parent="ViewportContainer/Viewport" instance=ExtResource( 1 )] -position = Vector2( 112, 56 ) -player_number = 2 - -[node name="UI_Layer" parent="ViewportContainer/Viewport" instance=ExtResource( 3 )] - -[node name="CameraGuide" type="KinematicBody2D" parent="ViewportContainer/Viewport"] -unique_name_in_owner = true -position = Vector2( 160, 90 ) -collision_layer = 0 -collision_mask = 256 -script = ExtResource( 5 ) - -[node name="CollisionShape2D" type="CollisionShape2D" parent="ViewportContainer/Viewport/CameraGuide"] -visible = false -shape = SubResource( 1 ) -one_way_collision = true - -[node name="Camera2D" type="Camera2D" parent="ViewportContainer/Viewport"] -unique_name_in_owner = true -position = Vector2( 160, 90 ) -current = true -script = ExtResource( 6 ) -tracking_node_path = NodePath("../CameraGuide") - -[node name="MushroomIcon" type="Sprite" parent="ViewportContainer/Viewport"] -position = Vector2( 60, 160 ) -texture = ExtResource( 8 ) +[node name="World" parent="ViewportContainer/Viewport" instance=ExtResource( 1 )] diff --git a/lib/singleton_autoloads/LevelInfo.gd b/lib/singleton_autoloads/LevelInfo.gd index bd885a0..2352854 100644 --- a/lib/singleton_autoloads/LevelInfo.gd +++ b/lib/singleton_autoloads/LevelInfo.gd @@ -11,7 +11,7 @@ onready var current_music_track :AudioStream onready var viewport_container = get_node("/root/Main/ViewportContainer") #$ViewportContainer onready var viewport = get_node("/root/Main/ViewportContainer/Viewport") # $ViewportContainer/Viewport -onready var camera_guide :CameraGuide = get_node("/root/Main/ViewportContainer/Viewport/CameraGuide") +onready var camera_guide :CameraGuide = get_node("/root/Main/ViewportContainer/Viewport/World/CameraGuide") func change_music_track(_music_track :AudioStream ): if current_music_track != _music_track: diff --git a/lib/singleton_autoloads/NetworkManager.gd b/lib/singleton_autoloads/NetworkManager.gd new file mode 100644 index 0000000..f17be08 --- /dev/null +++ b/lib/singleton_autoloads/NetworkManager.gd @@ -0,0 +1,187 @@ +extends Node + + +# 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) + +# 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 = 4 + +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 = [] + + +func _ready(): + get_tree().connect("network_peer_connected", self, "_player_connected") + get_tree().connect("network_peer_disconnected", self,"_player_disconnected") + get_tree().connect("connected_to_server", self, "_connected_ok") + get_tree().connect("connection_failed", self, "_connected_fail") + get_tree().connect("server_disconnected", self, "_server_disconnected") + + +# Callback from SceneTree. +func _player_connected(id): + # Registration of a client beings here, tell the connected player that we are here. + rpc_id(id, "register_player", player_name) + + +# Callback from SceneTree. +func _player_disconnected(id): + if has_node("/root/Main"): # Game is in progress. + if get_tree().is_network_server(): + emit_signal("game_error", "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 + emit_signal("connection_succeeded") + + +# Callback from SceneTree, only for clients (not server). +func _server_disconnected(): + emit_signal("game_error", "Server disconnected") + end_game() + + +# Callback from SceneTree, only for clients (not server). +func _connected_fail(): + get_tree().set_network_peer(null) # Remove peer + emit_signal("connection_failed") + + +# Lobby management functions. + +remote func register_player(new_player_name): + var id = get_tree().get_rpc_sender_id() + print(id) + players[id] = new_player_name + emit_signal("player_list_changed") + + +func unregister_player(id): + players.erase(id) + emit_signal("player_list_changed") + + +remote func pre_start_game(spawn_points): + # Change scene. + var world = load("res://src/levels/TestBox.tscn").instance() + get_tree().get_root().get_node("/root/Main/ViewportContainer/Viewport").add_child(world) + + get_tree().get_root().get_node("/root/Main/ViewportContainer/Viewport/Lobby").hide() + + var player_scene = load("res://src/actors/players/playerE/PlayerE.tscn") + + + for p_id in spawn_points: + #var spawn_pos = world.get_node("SpawnPoints/" + str(spawn_points[p_id])).position + var player = player_scene.instance() + + player.set_name(str(p_id)) # Use unique ID as node name. + #player.position=spawn_pos + player.set_network_master(p_id) #set unique id as master. + + if p_id == get_tree().get_network_unique_id(): + # If node for this peer id, set name. + player.set_player_name(player_name) + else: + # Otherwise set name from peer. + player.set_player_name(players[p_id]) + + world.get_node("/root/Main/ViewportContainer/Viewport").add_child(player) + #world.get_node("Players").add_child(player) + +# # Set up score. +# world.get_node("Score").add_player(get_tree().get_network_unique_id(), player_name) +# for pn in players: +# world.get_node("Score").add_player(pn, players[pn]) + + if not get_tree().is_network_server(): + # Tell server we are ready to start. + rpc_id(1, "ready_to_start", get_tree().get_network_unique_id()) + elif players.size() == 0: + post_start_game() + + +remote func post_start_game(): + get_tree().set_pause(false) # Unpause and unleash the game! + + +remote func ready_to_start(id): + assert(get_tree().is_network_server()) + + if not id in players_ready: + players_ready.append(id) + + if players_ready.size() == players.size(): + for p in players: + rpc_id(p, "post_start_game") + post_start_game() + + +func host_game(new_player_name): + player_name = new_player_name + peer = NetworkedMultiplayerENet.new() + peer.create_server(DEFAULT_PORT, MAX_PEERS) + get_tree().set_network_peer(peer) + + +func join_game(ip, new_player_name): + player_name = new_player_name + peer = NetworkedMultiplayerENet.new() + peer.create_client(ip, DEFAULT_PORT) + get_tree().set_network_peer(peer) + + +func get_player_list(): + return players.values() + + +func get_player_name(): + return player_name + + +func begin_game(): + assert(get_tree().is_network_server()) + + # 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 + # Call to pre-start game with the spawn points. + for p in players: + rpc_id(p, "pre_start_game", spawn_points) + + pre_start_game(spawn_points) + + +func end_game(): + if has_node("/root/World"): # Game is in progress. + # End it + get_node("/root/World").queue_free() + + emit_signal("game_ended") + players.clear() diff --git a/project.godot b/project.godot index 34ac086..416ebd6 100644 --- a/project.godot +++ b/project.godot @@ -205,6 +205,7 @@ animation/layers/only_include_visible_layers_by_default=true PlayerInfo="*res://lib/singleton_autoloads/PlayerInfo.gd" UiManager="*res://lib/singleton_autoloads/UIManager.gd" LevelInfo="*res://lib/singleton_autoloads/LevelInfo.gd" +NetworkManager="*res://lib/singleton_autoloads/NetworkManager.gd" [debug] diff --git a/src/World.tscn b/src/World.tscn new file mode 100644 index 0000000..7feec59 --- /dev/null +++ b/src/World.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=7 format=2] + +[ext_resource path="res://src/classes/camera_guide.gd" type="Script" id=1] +[ext_resource path="res://src/actors/players/playerE/PlayerE.tscn" type="PackedScene" id=2] +[ext_resource path="res://src/Camera2D.gd" type="Script" id=3] +[ext_resource path="res://src/ui/HUD.tscn" type="PackedScene" id=4] +[ext_resource path="res://src/levels/TestBox.tscn" type="PackedScene" id=5] + +[sub_resource type="RectangleShape2D" id=1] +extents = Vector2( 160, 90 ) + +[node name="World" type="Node2D"] + +[node name="UI_Layer" parent="." instance=ExtResource( 4 )] + +[node name="TestBox" parent="." instance=ExtResource( 5 )] + +[node name="PlayerE" parent="." instance=ExtResource( 2 )] + +[node name="Camera2D" type="Camera2D" parent="."] +unique_name_in_owner = true +position = Vector2( 160, 90 ) +current = true +script = ExtResource( 3 ) +tracking_node_path = NodePath("../CameraGuide") + +[node name="CameraGuide" type="KinematicBody2D" parent="."] +unique_name_in_owner = true +position = Vector2( 160, 90 ) +collision_layer = 0 +collision_mask = 256 +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="CameraGuide"] +visible = false +shape = SubResource( 1 ) +one_way_collision = true diff --git a/src/ui/lobby.gd b/src/ui/lobby.gd new file mode 100644 index 0000000..da12233 --- /dev/null +++ b/src/ui/lobby.gd @@ -0,0 +1,94 @@ +extends Control + + +func _ready(): + # Called every time the node is added to the scene. + NetworkManager.connect("connection_failed", self, "_on_connection_failed") + NetworkManager.connect("connection_succeeded", self, "_on_connection_success") + NetworkManager.connect("player_list_changed", self, "refresh_lobby") + NetworkManager.connect("game_ended", self, "_on_game_ended") + NetworkManager.connect("game_error", self, "_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 + NetworkManager.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 + NetworkManager.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_minsize() + $Connect/Host.disabled = false + $Connect/Join.disabled = false + + +func refresh_lobby(): + var players = NetworkManager.get_player_list() + players.sort() + $Players/List.clear() + $Players/List.add_item(NetworkManager.get_player_name() + " (You)") + for p in players: + $Players/List.add_item(p) + + ## Only Server is allowed to press the start game button. + $Players/Start.disabled = not get_tree().is_network_server() + + +func _on_start_pressed(): + NetworkManager.begin_game() + + +func _on_find_public_ip_pressed(): + OS.shell_open("https://icanhazip.com/") diff --git a/src/ui/lobby.tscn b/src/ui/lobby.tscn new file mode 100644 index 0000000..fd00e35 --- /dev/null +++ b/src/ui/lobby.tscn @@ -0,0 +1,173 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://src/ui/lobby.gd" type="Script" id=1] +[ext_resource path="res://assets/Fonts/QuinqueFive.tres" type="DynamicFont" id=2] + +[node name="Lobby" type="Control"] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -160.0 +margin_top = -90.0 +margin_right = 160.0 +margin_bottom = 90.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +script = ExtResource( 1 ) + +[node name="Players" type="Panel" parent="."] +visible = false +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -135.0 +margin_top = -87.5 +margin_right = 135.0 +margin_bottom = 87.5 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="Label" type="Label" parent="Players"] +margin_left = 16.0 +margin_top = 17.5 +margin_right = 132.0 +margin_bottom = 31.5 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_fonts/font = ExtResource( 2 ) +text = "Awaiting Players..." + +[node name="Start" type="Button" parent="Players"] +margin_left = 26.0 +margin_top = 140.5 +margin_right = 78.0 +margin_bottom = 158.5 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) +text = "START!" + +[node name="List" type="ItemList" parent="Players"] +margin_left = 11.0 +margin_top = 37.0 +margin_right = 136.0 +margin_bottom = 129.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) + +[node name="PortForward" type="Label" parent="Players"] +margin_left = 164.0 +margin_top = 23.5 +margin_right = 257.0 +margin_bottom = 128.5 +custom_constants/line_spacing = 6 +custom_fonts/font = ExtResource( 2 ) +text = "If you want non-LAN clients to connect, +make sure the port 10567 in UDP +is forwarded on your router." +align = 1 +autowrap = true + +[node name="FindPublicIP" type="LinkButton" parent="Players"] +margin_left = 90.0 +margin_top = 141.5 +margin_right = 263.0 +margin_bottom = 155.5 +custom_fonts/font = ExtResource( 2 ) +text = "Find your public IP address" + +[node name="Connect" type="Panel" parent="."] +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +margin_left = -135.0 +margin_top = -75.0 +margin_right = 135.0 +margin_bottom = 75.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[node name="NameLabel" type="Label" parent="Connect"] +margin_left = 14.0 +margin_top = 11.0 +margin_right = 56.0 +margin_bottom = 25.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_fonts/font = ExtResource( 2 ) +text = "Name:" + +[node name="Name" type="LineEdit" parent="Connect"] +margin_left = 17.0 +margin_top = 30.0 +margin_right = 173.0 +margin_bottom = 54.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) +text = "The Warrior" + +[node name="IPLabel" type="Label" parent="Connect"] +margin_left = 15.0 +margin_top = 66.0 +margin_right = 57.0 +margin_bottom = 80.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_fonts/font = ExtResource( 2 ) +text = "IP:" + +[node name="IPAddress" type="LineEdit" parent="Connect"] +margin_left = 17.0 +margin_top = 85.0 +margin_right = 173.0 +margin_bottom = 109.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) +text = "127.0.0.1" + +[node name="Host" type="Button" parent="Connect"] +margin_left = 181.0 +margin_top = 31.0 +margin_right = 246.0 +margin_bottom = 51.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) +text = "Host" + +[node name="Join" type="Button" parent="Connect"] +margin_left = 181.0 +margin_top = 87.0 +margin_right = 246.0 +margin_bottom = 107.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 +custom_fonts/font = ExtResource( 2 ) +text = "Join" + +[node name="ErrorLabel" type="Label" parent="Connect"] +margin_left = 15.0 +margin_top = 125.0 +margin_right = 257.0 +margin_bottom = 139.0 +size_flags_horizontal = 2 +size_flags_vertical = 0 +custom_colors/font_color = Color( 0.820312, 0.291595, 0.291595, 1 ) +align = 1 + +[node name="ErrorDialog" type="AcceptDialog" parent="."] +margin_right = 55.0 +margin_bottom = 58.0 +size_flags_horizontal = 2 +size_flags_vertical = 2 + +[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"]