├── .gitignore ├── LICENSE ├── README.md ├── client ├── ClientEntry.gd ├── ClientEntry.tscn ├── game │ ├── ClientGame.gd │ └── ClientGame.tscn ├── lobby │ ├── ClientLobby.gd │ └── ClientLobby.tscn └── main_menu │ ├── MainMenu.gd │ └── MainMenu.tscn ├── common ├── game │ ├── Game.gd │ ├── Game.tscn │ ├── Player.gd │ └── Player.tscn └── lobby │ ├── Lobby.gd │ ├── Lobby.tscn │ └── NamePlate.tscn ├── default_env.tres ├── docs ├── scene_flow.png └── scene_flow.png.import ├── entry ├── Entry.gd └── Entry.tscn ├── export_presets.cfg ├── icon.png ├── icon.png.import ├── networking ├── BaseNetwork.gd ├── ClientNetwork.gd ├── GameData.gd └── ServerNetwork.gd ├── project.godot └── server ├── ServerEntry.gd ├── ServerEntry.tscn ├── game ├── ServerGame.gd └── ServerGame.tscn └── lobby ├── ServerLobby.gd └── ServerLobby.tscn /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | 5 | # Imported translations (automatically generated from CSV files) 6 | *.translation 7 | 8 | # Mono-specific ignores 9 | .mono/ 10 | data_*/ 11 | mono_crash.*.json 12 | 13 | # System/tool-specific ignores 14 | .directory 15 | *~ 16 | 17 | export 18 | export/* 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GodotClientServer 2 | **A project template for a Dedicated Server & Client within a single project.** 3 | 4 | 5 | ## Why? 6 | There is already a good tutorial out there showing how to set up a dedicated server in Godot using two separate projects. This has a number of advantages. However, it has in my view one huge disadvantage: Maintainability. 7 | 8 | In a two project setup, you have to somehow keep shared logic in sync between the two projects. You can imagine a number of ways to do this, but I would massively prefer to just fix a bug once, and not have any steps to remember. Because I'll forget. 9 | 10 | So this idea here is to have one Godot project, and keep as much of the code shared between client and server as possible. 11 | 12 | 13 | ## High level design 14 | This relies on an aspect of Godot's export system called "Features." Each export template specifies which Features it supports, such as Windows, 64 bit, PC vs Mobile, etc. 15 | 16 | These are the built-in features, but Godot also allows you to specify your own custom features. So you could, for instance, have two different Windows Export Presets, and they could each contain different custom Features that you specify. 17 | 18 | Importantly, the presence of these features can be queried at runtime: 19 | 20 | ``` OS.has_feature("X") ``` 21 | 22 | You probably see where this is going now. 23 | 24 | We can have two different export templates, and each has it's own custom feature: `client` for one, and `server` for the other. 25 | 26 | Our `Main Scene` will be a simple `Entry` scene that detects which feature is present, and then launches into a different Scene accordingly. 27 | 28 | The Client will launch into a Main Menu, and the Server will launch into a Lobby scene, where it will open a port and begin listening for clients. 29 | 30 | 31 | # Project Structure 32 | ``` 33 | root/ 34 | | 35 | - common 36 | - client 37 | - server 38 | ``` 39 | 40 | **common** contains the bulk of the game code. This is all of the code that runs on both client and server. This is where the real benefit of this architecture comes from. 41 | 42 | 43 | ## Client/Server specific code 44 | Each scene in `common` will have a corresponding inherited scene in both client and server. This allows you to do client or server specific stuff quite easily. 45 | 46 | The scenes are named to prevent confusion in an editor so: 47 | ``` 48 | common/Lobby.tscn 49 | client/ClientLobby.tscn 50 | server/ServerLobby.tscn 51 | ``` 52 | 53 | So the only trick here is that scene transitions must be in the inherited scenes, since the server will change to `ServerGame.tscn` and clients will change to `ClientGame.tscn`. This can be accomplished easily with overriden methods or signals. 54 | 55 | ## Running on a headless machine 56 | If this was for some real game, the server likely would be on a headless machine. To accommodate this, we can use Godot's server export template. It is a graphicless version of Godot, and will not attempt to open a window or use any graphics API. 57 | 58 | This means you can easily run the dedicated server on a headless Linux box. 59 | 60 | https://godotengine.org/download/server 61 | 62 | ## Exporting the Linux Server 63 | In the **Export Project** dialog, click on the **Linux Server** preset. Then export the pack file, not the full Export Template. 64 | 65 | You can then run the dedicated server as such: 66 | ``` 67 | ./Godot_v3.2-stable_linux_server.64 --main-pack Server.pck 68 | ``` 69 | 70 | ## Down sides 71 | - This will still load all of the graphical assets, so it will not be as slim in memory as it could possibly be in the two project approach. 72 | 73 | ## Misc 74 | - This is shown working with a Lobby based game, but it could just as easily work with a join-in-progress type of game. 75 | - I'm not completely happy with how I split up the Network classes (BaseNetwork, ClientNetwork, ServerNetwork). So I may actualy get rid of that. 76 | - This is *NOT* Server authoratative. It could be in theory, and that is one big advantage I see is that all of the game logic is already running on the server. 77 | -------------------------------------------------------------------------------- /client/ClientEntry.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Do any client specific setup here 4 | # Then launch into your first scene 5 | func _ready(): 6 | get_tree().change_scene("res://client/main_menu/MainMenu.tscn") 7 | -------------------------------------------------------------------------------- /client/ClientEntry.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://client/ClientEntry.gd" type="Script" id=1] 4 | 5 | [node name="ClientEntry" type="Node"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /client/game/ClientGame.gd: -------------------------------------------------------------------------------- 1 | extends "res://common/game/Game.gd" 2 | 3 | func _ready(): 4 | pass 5 | 6 | remotesync func on_pre_configure_complete(): 7 | print("All clients are configured. Starting the game.") 8 | get_tree().paused = false 9 | -------------------------------------------------------------------------------- /client/game/ClientGame.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://common/game/Game.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://client/game/ClientGame.gd" type="Script" id=2] 5 | 6 | [node name="Game" instance=ExtResource( 1 )] 7 | script = ExtResource( 2 ) 8 | -------------------------------------------------------------------------------- /client/lobby/ClientLobby.gd: -------------------------------------------------------------------------------- 1 | extends "res://common/lobby/Lobby.gd" 2 | 3 | func _ready(): 4 | ClientNetwork.connect("start_game", self, "on_start_game") 5 | 6 | # Tell the server about you 7 | ServerNetwork.register_self(get_tree().get_network_unique_id(), ClientNetwork.localPlayerName) 8 | 9 | func _on_StartButton_pressed(): 10 | ClientNetwork.start_game() 11 | 12 | func on_start_game(): 13 | get_tree().change_scene("res://client/game/ClientGame.tscn") 14 | -------------------------------------------------------------------------------- /client/lobby/ClientLobby.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://common/lobby/Lobby.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://client/lobby/ClientLobby.gd" type="Script" id=2] 5 | 6 | [node name="Lobby" instance=ExtResource( 1 )] 7 | script = ExtResource( 2 ) 8 | 9 | [node name="StartButton" type="Button" parent="." index="2"] 10 | margin_left = 772.0 11 | margin_top = 257.0 12 | margin_right = 855.0 13 | margin_bottom = 277.0 14 | text = "Start Game" 15 | __meta__ = { 16 | "_edit_use_anchors_": false 17 | } 18 | [connection signal="pressed" from="StartButton" to="." method="_on_StartButton_pressed"] 19 | -------------------------------------------------------------------------------- /client/main_menu/MainMenu.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | func _ready(): 4 | get_tree().connect('connected_to_server', self, 'on_connected_to_server') 5 | 6 | 7 | func _on_ConnectButton_pressed(): 8 | var ip := $ServerIpLabel/ServerIp.text as String 9 | var playerName := $PlayerNameLabel/PlayerName.text as String 10 | connect_to_server(playerName, ip) 11 | 12 | 13 | func connect_to_server(playerName: String, serverIp: String): 14 | ClientNetwork.join_game(serverIp, playerName) 15 | 16 | 17 | func on_connected_to_server(): 18 | get_tree().change_scene("res://client/lobby/ClientLobby.tscn") 19 | -------------------------------------------------------------------------------- /client/main_menu/MainMenu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://client/main_menu/MainMenu.gd" type="Script" id=1] 4 | 5 | [node name="MainMenu" type="Control"] 6 | anchor_right = 1.0 7 | anchor_bottom = 1.0 8 | script = ExtResource( 1 ) 9 | __meta__ = { 10 | "_edit_use_anchors_": false 11 | } 12 | 13 | [node name="ScreenTitle" type="Label" parent="."] 14 | margin_left = 30.0 15 | margin_top = 39.0 16 | margin_right = 101.0 17 | margin_bottom = 53.0 18 | text = "Main Menu" 19 | 20 | [node name="PlayerNameLabel" type="Label" parent="."] 21 | margin_left = 193.0 22 | margin_top = 110.0 23 | margin_right = 265.0 24 | margin_bottom = 124.0 25 | text = "Player Name" 26 | 27 | [node name="PlayerName" type="LineEdit" parent="PlayerNameLabel"] 28 | margin_left = 1.0 29 | margin_top = 22.0 30 | margin_right = 242.0 31 | margin_bottom = 46.0 32 | text = "A Player" 33 | __meta__ = { 34 | "_edit_use_anchors_": false 35 | } 36 | 37 | [node name="ServerIpLabel" type="Label" parent="."] 38 | margin_left = 193.0 39 | margin_top = 179.0 40 | margin_right = 233.0 41 | margin_bottom = 193.0 42 | text = "Server IP" 43 | __meta__ = { 44 | "_edit_use_anchors_": false 45 | } 46 | 47 | [node name="ServerIp" type="LineEdit" parent="ServerIpLabel"] 48 | margin_top = 21.0 49 | margin_right = 243.0 50 | margin_bottom = 45.0 51 | text = "127.0.0.1" 52 | __meta__ = { 53 | "_edit_use_anchors_": false 54 | } 55 | 56 | [node name="ConnectButton" type="Button" parent="."] 57 | margin_left = 375.0 58 | margin_top = 268.0 59 | margin_right = 439.0 60 | margin_bottom = 288.0 61 | text = "Connect" 62 | __meta__ = { 63 | "_edit_use_anchors_": false 64 | } 65 | [connection signal="pressed" from="ConnectButton" to="." method="_on_ConnectButton_pressed"] 66 | -------------------------------------------------------------------------------- /common/game/Game.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | func _ready(): 4 | print("Entering game") 5 | get_tree().paused = true 6 | 7 | ClientNetwork.connect("remove_player", self, "remove_player") 8 | 9 | pre_configure() 10 | 11 | 12 | func remove_player(playerId: int): 13 | var playerNode = get_node(str(playerId)) 14 | playerNode.queue_free() 15 | 16 | 17 | func pre_configure(): 18 | var order := 0 19 | var sortedPlayers = [] 20 | for playerId in GameData.players: 21 | sortedPlayers.push_back(playerId) 22 | 23 | sortedPlayers.sort() 24 | 25 | for playerId in sortedPlayers: 26 | spawn_player(playerId, order) 27 | order += 1 28 | 29 | if not get_tree().is_network_server(): 30 | # Report that this client is done 31 | rpc_id(ServerNetwork.SERVER_ID, "on_client_ready", get_tree().get_network_unique_id()) 32 | 33 | 34 | func spawn_player(playerId, order): 35 | print("Creating player game object") 36 | 37 | var player = GameData.players[playerId] 38 | var playerName = player[GameData.PLAYER_NAME] 39 | 40 | var scene = preload("res://common/game/Player.tscn") 41 | 42 | var node = scene.instance() 43 | node.set_network_master(playerId) 44 | node.set_name(str(playerId)) 45 | 46 | node.position.x = 100 * (order + 1) 47 | node.position.y = 100 48 | 49 | node.get_node("NameLabel").text = playerName 50 | 51 | add_child(node) 52 | 53 | 54 | remotesync func on_pre_configure_complete(): 55 | print("All clients are configured. Starting the game.") 56 | get_tree().paused = false 57 | -------------------------------------------------------------------------------- /common/game/Game.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://common/game/Game.gd" type="Script" id=1] 4 | 5 | [node name="Game" type="Node2D"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /common/game/Player.gd: -------------------------------------------------------------------------------- 1 | extends KinematicBody2D 2 | 3 | const SPEED := 50.0 4 | 5 | puppet func network_update(networkPosition: Vector2): 6 | self.position = networkPosition 7 | 8 | func _physics_process(delta): 9 | if is_network_master(): 10 | var velocity := Vector2.ZERO 11 | if Input.is_action_pressed("ui_down"): 12 | velocity.y += SPEED 13 | if Input.is_action_pressed("ui_up"): 14 | velocity.y -= SPEED 15 | if Input.is_action_pressed("ui_left"): 16 | velocity.x -= SPEED 17 | if Input.is_action_pressed("ui_right"): 18 | velocity.x += SPEED 19 | 20 | velocity = move_and_slide(velocity) 21 | 22 | rpc_unreliable("network_update", self.position) 23 | 24 | func set_player_name(playerName: String): 25 | $NameLabel.text = playerName 26 | -------------------------------------------------------------------------------- /common/game/Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://common/game/Player.gd" type="Script" id=1] 4 | 5 | [sub_resource type="RectangleShape2D" id=1] 6 | extents = Vector2( 20, 20 ) 7 | 8 | [node name="Player" type="KinematicBody2D"] 9 | script = ExtResource( 1 ) 10 | 11 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 12 | shape = SubResource( 1 ) 13 | 14 | [node name="ColorRect" type="ColorRect" parent="."] 15 | margin_left = -20.0 16 | margin_top = -20.0 17 | margin_right = 20.0 18 | margin_bottom = 20.0 19 | 20 | [node name="NameLabel" type="Label" parent="."] 21 | margin_left = -30.0 22 | margin_top = 40.0 23 | margin_right = 116.0 24 | margin_bottom = 90.0 25 | text = "Name" 26 | __meta__ = { 27 | "_edit_use_anchors_": false 28 | } 29 | -------------------------------------------------------------------------------- /common/lobby/Lobby.gd: -------------------------------------------------------------------------------- 1 | extends Control 2 | 3 | func _ready(): 4 | ClientNetwork.connect("create_player", self, "create_player") 5 | ClientNetwork.connect("remove_player", self, "remove_player") 6 | 7 | func create_player(playerId: int): 8 | print("Creating player in lobby") 9 | #var player = GameData.players[playerId] 10 | var namePlateScene = preload("res://common/lobby/NamePlate.tscn") 11 | 12 | var namePlateNode = namePlateScene.instance() 13 | namePlateNode.set_network_master(playerId) 14 | namePlateNode.set_name(str(playerId)) 15 | 16 | var player = GameData.players[playerId] 17 | namePlateNode.get_node("Name").text = player[GameData.PLAYER_NAME] 18 | 19 | #playerNode.position.x = 100 20 | #playerNode.position.y = 100 21 | 22 | $Players.add_child(namePlateNode) 23 | 24 | func remove_player(playerId: int): 25 | var name = str(playerId) 26 | for child in $Players.get_children(): 27 | if child.name == name: 28 | print("Player removed") 29 | $Players.remove_child(child) 30 | break 31 | -------------------------------------------------------------------------------- /common/lobby/Lobby.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://common/lobby/Lobby.gd" type="Script" id=1] 4 | 5 | [node name="Lobby" type="Control"] 6 | anchor_right = 1.0 7 | anchor_bottom = 1.0 8 | script = ExtResource( 1 ) 9 | __meta__ = { 10 | "_edit_use_anchors_": false 11 | } 12 | 13 | [node name="Title" type="Label" parent="."] 14 | margin_right = 40.0 15 | margin_bottom = 14.0 16 | text = "Lobby" 17 | __meta__ = { 18 | "_edit_use_anchors_": false 19 | } 20 | 21 | [node name="Players" type="VBoxContainer" parent="."] 22 | margin_left = 138.0 23 | margin_top = 73.0 24 | margin_right = 666.0 25 | margin_bottom = 347.0 26 | __meta__ = { 27 | "_edit_use_anchors_": false 28 | } 29 | -------------------------------------------------------------------------------- /common/lobby/NamePlate.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=2] 2 | 3 | [node name="NamePlate" type="HBoxContainer"] 4 | anchor_right = 0.187 5 | anchor_bottom = 0.083 6 | margin_right = -0.488007 7 | margin_bottom = 0.199997 8 | __meta__ = { 9 | "_edit_use_anchors_": false 10 | } 11 | 12 | [node name="Name" type="Label" parent="."] 13 | margin_top = 18.0 14 | margin_right = 81.0 15 | margin_bottom = 32.0 16 | text = "Player Name" 17 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="ProceduralSky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | background_sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /docs/scene_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/GodotClientServer/6a9dafac4aaa4ced331c16a3da4ffda66c6b6c9f/docs/scene_flow.png -------------------------------------------------------------------------------- /docs/scene_flow.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/scene_flow.png-0d85fb260be04447c231ae92cb1991dd.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://docs/scene_flow.png" 13 | dest_files=[ "res://.import/scene_flow.png-0d85fb260be04447c231ae92cb1991dd.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /entry/Entry.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Entry point for the whole app 4 | # Determine the type of app this is, and load the entry point for that type 5 | func _ready(): 6 | print("Application started") 7 | if OS.has_feature("server"): 8 | print("Is server") 9 | get_tree().change_scene("res://server/ServerEntry.tscn") 10 | elif OS.has_feature("client"): 11 | print("Is client") 12 | get_tree().change_scene("res://client/ClientEntry.tscn") 13 | # When running from the editor, this is how we'll default to being a client 14 | else: 15 | print("Could not detect application type! Defaulting to client.") 16 | get_tree().change_scene("res://client/ClientEntry.tscn") 17 | #get_tree().change_scene("res://server/ServerEntry.tscn") 18 | -------------------------------------------------------------------------------- /entry/Entry.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://entry/Entry.gd" type="Script" id=1] 4 | 5 | [node name="Entry" type="Node"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /export_presets.cfg: -------------------------------------------------------------------------------- 1 | [preset.0] 2 | 3 | name="Client - Windows Desktop" 4 | platform="Windows Desktop" 5 | runnable=true 6 | custom_features="client" 7 | export_filter="all_resources" 8 | include_filter="" 9 | exclude_filter="" 10 | export_path="export/client/Client.exe" 11 | patch_list=PoolStringArray( ) 12 | script_export_mode=1 13 | script_encryption_key="" 14 | 15 | [preset.0.options] 16 | 17 | texture_format/bptc=false 18 | texture_format/s3tc=true 19 | texture_format/etc=false 20 | texture_format/etc2=false 21 | texture_format/no_bptc_fallbacks=true 22 | binary_format/64_bits=true 23 | binary_format/embed_pck=true 24 | custom_template/release="" 25 | custom_template/debug="" 26 | codesign/enable=false 27 | codesign/identity_type=0 28 | codesign/identity="" 29 | codesign/password="" 30 | codesign/timestamp=true 31 | codesign/timestamp_server_url="" 32 | codesign/digest_algorithm=1 33 | codesign/description="" 34 | codesign/custom_options=PoolStringArray( ) 35 | application/icon="" 36 | application/file_version="" 37 | application/product_version="" 38 | application/company_name="" 39 | application/product_name="" 40 | application/file_description="" 41 | application/copyright="" 42 | application/trademarks="" 43 | 44 | [preset.1] 45 | 46 | name="Server - Linux" 47 | platform="Linux/X11" 48 | runnable=false 49 | custom_features="server" 50 | export_filter="all_resources" 51 | include_filter="" 52 | exclude_filter="" 53 | export_path="export/server/Server.x86_64" 54 | patch_list=PoolStringArray( ) 55 | script_export_mode=1 56 | script_encryption_key="" 57 | 58 | [preset.1.options] 59 | 60 | texture_format/bptc=false 61 | texture_format/s3tc=true 62 | texture_format/etc=false 63 | texture_format/etc2=false 64 | texture_format/no_bptc_fallbacks=true 65 | binary_format/64_bits=true 66 | binary_format/embed_pck=false 67 | custom_template/release="" 68 | custom_template/debug="" 69 | 70 | [preset.2] 71 | 72 | name="Server - Windows" 73 | platform="Windows Desktop" 74 | runnable=false 75 | custom_features="server" 76 | export_filter="all_resources" 77 | include_filter="" 78 | exclude_filter="" 79 | export_path="export/server/Server.exe" 80 | patch_list=PoolStringArray( ) 81 | script_export_mode=1 82 | script_encryption_key="" 83 | 84 | [preset.2.options] 85 | 86 | texture_format/bptc=false 87 | texture_format/s3tc=true 88 | texture_format/etc=false 89 | texture_format/etc2=false 90 | texture_format/no_bptc_fallbacks=true 91 | binary_format/64_bits=true 92 | binary_format/embed_pck=false 93 | custom_template/release="" 94 | custom_template/debug="" 95 | codesign/enable=false 96 | codesign/identity_type=0 97 | codesign/identity="" 98 | codesign/password="" 99 | codesign/timestamp=true 100 | codesign/timestamp_server_url="" 101 | codesign/digest_algorithm=1 102 | codesign/description="" 103 | codesign/custom_options=PoolStringArray( ) 104 | application/icon="" 105 | application/file_version="" 106 | application/product_version="" 107 | application/company_name="" 108 | application/product_name="" 109 | application/file_description="" 110 | application/copyright="" 111 | application/trademarks="" 112 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/GodotClientServer/6a9dafac4aaa4ced331c16a3da4ffda66c6b6c9f/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://icon.png" 13 | dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=true 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | stream=false 32 | size_limit=0 33 | detect_3d=true 34 | svg/scale=1.0 35 | -------------------------------------------------------------------------------- /networking/BaseNetwork.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | signal remove_player 4 | 5 | func _ready(): 6 | reset_network() 7 | 8 | get_tree().connect("network_peer_disconnected", self, "_player_disconnected") 9 | 10 | # Every network peer needs to clean up the disconnected client 11 | func _player_disconnected(id): 12 | print("Player disconnected: " + str(id)) 13 | GameData.players.erase(id) 14 | 15 | emit_signal("remove_player", id) 16 | print("Total players: %d" % GameData.players.size()) 17 | 18 | # Completely reset the game state and clear the network 19 | func reset_network(): 20 | var peer = get_tree().network_peer 21 | if peer != null: 22 | peer.close_connection() 23 | 24 | # Cleanup all state related to the game session 25 | GameData.reset() 26 | -------------------------------------------------------------------------------- /networking/ClientNetwork.gd: -------------------------------------------------------------------------------- 1 | extends "BaseNetwork.gd" 2 | 3 | signal create_player 4 | signal start_game 5 | 6 | var localPlayerName: String 7 | 8 | func join_game(serverIp: String, playerName: String) -> bool: 9 | get_tree().connect('connected_to_server', self, 'on_connected_to_server') 10 | 11 | self.localPlayerName = playerName 12 | 13 | var peer = NetworkedMultiplayerENet.new() 14 | var result = peer.create_client(serverIp, ServerNetwork.SERVER_PORT) 15 | 16 | if result == OK: 17 | get_tree().set_network_peer(peer) 18 | print("Connecting to server...") 19 | return true 20 | else: 21 | return false 22 | 23 | 24 | func on_connected_to_server(): 25 | print("Connected to server.") 26 | 27 | 28 | func register_player(recipientId: int, playerId: int, playerName: String): 29 | rpc_id(recipientId, "on_register_player", playerId, playerName) 30 | 31 | 32 | remote func on_register_player(playerId: int, playerName: String): 33 | print(playerName) 34 | print("on_register_player: " + str(playerId)) 35 | GameData.add_player(playerId, playerName) 36 | emit_signal("create_player", playerId) 37 | print("Total players: %d" % GameData.players.size()) 38 | 39 | 40 | func start_game(): 41 | rpc("on_start_game") 42 | 43 | 44 | remotesync func on_start_game(): 45 | emit_signal("start_game") 46 | -------------------------------------------------------------------------------- /networking/GameData.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var players = {} 4 | 5 | const PLAYER_ID = "id" 6 | const PLAYER_NAME = "name" 7 | func create_new_player(playerId: int, playerName: String) -> Dictionary: 8 | return { PLAYER_ID: playerId, PLAYER_NAME: playerName } 9 | 10 | func add_player(playerId: int, playerName: String): 11 | var newPlayer = create_new_player(playerId, playerName) 12 | self.players[playerId] = newPlayer 13 | 14 | func reset(): 15 | self.players = {} 16 | -------------------------------------------------------------------------------- /networking/ServerNetwork.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const SERVER_ID := 1 4 | const SERVER_PORT := 3000 5 | const MAX_PLAYERS := 15 6 | 7 | func _player_connected(id): 8 | print("Player connected: " + str(id)) 9 | 10 | 11 | # Called by clients when they connect 12 | func register_self(playerId: int, playerName: String): 13 | rpc_id(SERVER_ID, "on_register_self", playerId, playerName) 14 | 15 | 16 | remote func on_register_self(playerId, playerName): 17 | # Register this client with the server 18 | ClientNetwork.on_register_player(playerId, playerName) 19 | 20 | # Register the new player with all existing clients 21 | for curPlayerId in GameData.players: 22 | ClientNetwork.register_player(curPlayerId, playerId, playerName) 23 | 24 | # Catch the new player up on who is already here 25 | for curPlayerId in GameData.players: 26 | if curPlayerId != playerId: 27 | var player = GameData.players[curPlayerId] 28 | ClientNetwork.register_player(playerId, curPlayerId, player.name) 29 | 30 | 31 | func is_hosting() -> bool: 32 | if get_tree().network_peer != null and get_tree().network_peer.get_connection_status() != NetworkedMultiplayerENet.ConnectionStatus.CONNECTION_DISCONNECTED: 33 | return true 34 | else: 35 | return false 36 | 37 | func host_game() -> bool: 38 | ClientNetwork.reset_network() 39 | 40 | var peer = NetworkedMultiplayerENet.new() 41 | var result = peer.create_server(SERVER_PORT, MAX_PLAYERS) 42 | if result == OK: 43 | get_tree().set_network_peer(peer) 44 | 45 | get_tree().connect("network_peer_connected", self, "_player_connected") 46 | 47 | print("Server started.") 48 | return true 49 | else: 50 | print("Failed to host game: %d" % result) 51 | return false 52 | -------------------------------------------------------------------------------- /project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=4 10 | 11 | _global_script_classes=[ ] 12 | _global_script_class_icons={ 13 | 14 | } 15 | 16 | [application] 17 | 18 | config/name="ClientServer" 19 | run/main_scene="res://entry/Entry.tscn" 20 | config/icon="res://icon.png" 21 | 22 | [autoload] 23 | 24 | ClientNetwork="*res://networking/ClientNetwork.gd" 25 | ServerNetwork="*res://networking/ServerNetwork.gd" 26 | GameData="*res://networking/GameData.gd" 27 | 28 | [debug] 29 | 30 | gdscript/warnings/unused_argument=false 31 | gdscript/warnings/return_value_discarded=false 32 | 33 | [layer_names] 34 | 35 | 2d_physics/layer_1="players" 36 | 37 | [rendering] 38 | 39 | environment/default_environment="res://default_env.tres" 40 | -------------------------------------------------------------------------------- /server/ServerEntry.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # Do any server specific setup here 4 | # Then open a lobby and start listening for users 5 | func _ready(): 6 | get_tree().change_scene("res://server/lobby/ServerLobby.tscn") 7 | -------------------------------------------------------------------------------- /server/ServerEntry.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://server/ServerEntry.gd" type="Script" id=1] 4 | 5 | [node name="ServerEntry" type="Node"] 6 | script = ExtResource( 1 ) 7 | -------------------------------------------------------------------------------- /server/game/ServerGame.gd: -------------------------------------------------------------------------------- 1 | extends "res://common/game/Game.gd" 2 | 3 | var unreadyPlayers := {} 4 | 5 | func _ready(): 6 | ClientNetwork.connect("remove_player", self, "remove_player") 7 | 8 | for playerId in GameData.players: 9 | unreadyPlayers[playerId] = playerId 10 | 11 | remote func on_client_ready(playerId): 12 | print("client ready: %s" % playerId) 13 | unreadyPlayers.erase(playerId) 14 | print("Still waiting on %d players" % unreadyPlayers.size()) 15 | 16 | # All clients are done, unpause the game 17 | if unreadyPlayers.empty(): 18 | print("Starting the game") 19 | rpc("on_pre_configure_complete") 20 | 21 | 22 | func remove_player(playerId: int): 23 | # If all players are gone, return to lobby 24 | if GameData.players.empty(): 25 | print("All players disconnected, returning to lobby") 26 | get_tree().change_scene("res://server/lobby/ServerLobby.tscn") 27 | else: 28 | print("Players remaining: %d" % GameData.players.size()) 29 | -------------------------------------------------------------------------------- /server/game/ServerGame.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://common/game/Game.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://server/game/ServerGame.gd" type="Script" id=2] 5 | 6 | [node name="Game" instance=ExtResource( 1 )] 7 | script = ExtResource( 2 ) 8 | -------------------------------------------------------------------------------- /server/lobby/ServerLobby.gd: -------------------------------------------------------------------------------- 1 | extends "res://common/lobby/Lobby.gd" 2 | 3 | func _ready(): 4 | if not ServerNetwork.is_hosting(): 5 | if not ServerNetwork.host_game(): 6 | print("Failed to start server, shutting down.") 7 | get_tree().quit() 8 | return 9 | 10 | ClientNetwork.connect("start_game", self, "on_start_game") 11 | 12 | 13 | func on_start_game(): 14 | get_tree().change_scene("res://server/game/ServerGame.tscn") 15 | -------------------------------------------------------------------------------- /server/lobby/ServerLobby.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://common/lobby/Lobby.tscn" type="PackedScene" id=1] 4 | [ext_resource path="res://server/lobby/ServerLobby.gd" type="Script" id=2] 5 | 6 | 7 | [node name="Lobby" instance=ExtResource( 1 )] 8 | script = ExtResource( 2 ) 9 | --------------------------------------------------------------------------------