├── .gitignore ├── Assets ├── back.png ├── back.png.import ├── cod.jpeg ├── cod.jpeg.import ├── eye.png ├── eye.png.import ├── floor.png ├── floor.png.import ├── front.png ├── front.png.import ├── side.png └── side.png.import ├── LICENSE ├── README.md ├── Scenes ├── Bullet.tscn ├── GameInstance.tscn ├── GameInstanceServer.tscn ├── Main_Menu.tscn ├── Notifications.tscn └── Player.tscn ├── Scripts ├── Autoloads │ ├── NetworkManager.gd │ ├── Tools.gd │ └── UUID.gd ├── Bullet.gd ├── GameInstance.gd ├── GameInstanceBase.gd ├── GameInstanceClient.gd ├── GameInstanceServer.gd ├── Player.gd ├── Services │ ├── AuthService.gd │ └── LobbyService.gd └── Types │ ├── BaseClass.gd │ ├── ClientSnapshot.gd │ ├── LobbyClass.gd │ ├── ServerSnapshot.gd │ └── UserClass.gd ├── build_and_debug.ps1 ├── default_env.tres ├── icon.png ├── icon.png.import └── project.godot /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | data_*/ 10 | -------------------------------------------------------------------------------- /Assets/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/back.png -------------------------------------------------------------------------------- /Assets/back.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/back.png-053dd747ba6ac70cbaf48f0fcdf7d070.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/back.png" 13 | dest_files=[ "res://.import/back.png-053dd747ba6ac70cbaf48f0fcdf7d070.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=false 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 | -------------------------------------------------------------------------------- /Assets/cod.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/cod.jpeg -------------------------------------------------------------------------------- /Assets/cod.jpeg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/cod.jpeg-001ab1436c8992b06295e45066c40045.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/cod.jpeg" 13 | dest_files=[ "res://.import/cod.jpeg-001ab1436c8992b06295e45066c40045.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=false 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 | -------------------------------------------------------------------------------- /Assets/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/eye.png -------------------------------------------------------------------------------- /Assets/eye.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/eye.png-7630948329d5e29781df0fb2b21bd0ec.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/eye.png" 13 | dest_files=[ "res://.import/eye.png-7630948329d5e29781df0fb2b21bd0ec.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=1 23 | flags/filter=false 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 | -------------------------------------------------------------------------------- /Assets/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/floor.png -------------------------------------------------------------------------------- /Assets/floor.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/floor.png-1e503fdcbfc8415a976a6bbee47dc71e.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/floor.png" 13 | dest_files=[ "res://.import/floor.png-1e503fdcbfc8415a976a6bbee47dc71e.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=1 23 | flags/filter=false 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 | -------------------------------------------------------------------------------- /Assets/front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/front.png -------------------------------------------------------------------------------- /Assets/front.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/front.png-ecb07e1567a33b88e6cd56211ec7d4fe.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/front.png" 13 | dest_files=[ "res://.import/front.png-ecb07e1567a33b88e6cd56211ec7d4fe.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=false 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 | -------------------------------------------------------------------------------- /Assets/side.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/Assets/side.png -------------------------------------------------------------------------------- /Assets/side.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/side.png-92869e89de85ccd39704e0215aaa4396.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://Assets/side.png" 13 | dest_files=[ "res://.import/side.png-92869e89de85ccd39704e0215aaa4396.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=false 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 NovemberDev 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 | # **novemberdev_multiplayer_godot** 2 | 3 | This repository is based on a video I made on how to do multiplayer in godot. 4 | 5 | Entities synchronize their state using snapshots instead of remote procedure calls. This is a bit different from most godot multiplayer tutorials, but it gets closer to how it's been done for the past decade - check out valve's wiki article on this: 6 | 7 | https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking 8 | 9 | Click to see the video for full details: 10 | 11 | [![NovemberDev Multiplayer Tutorial Godot](https://img.youtube.com/vi/Gx--b6kbZfs/0.jpg)](https://www.youtube.com/watch?v=Gx--b6kbZfs) 12 | 13 | -------------------------------------------------------------------------------- /Scenes/Bullet.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=2] 2 | 3 | [ext_resource path="res://Assets/cod.jpeg" type="Texture" id=1] 4 | [ext_resource path="res://Scripts/Bullet.gd" type="Script" id=2] 5 | 6 | [sub_resource type="RectangleShape2D" id=1] 7 | extents = Vector2( 32.2282, 26.8726 ) 8 | 9 | [node name="Bullet" type="Area2D"] 10 | script = ExtResource( 2 ) 11 | 12 | [node name="Sprite" type="Sprite" parent="."] 13 | position = Vector2( 0, -1.90735e-06 ) 14 | rotation = 1.57079 15 | scale = Vector2( 0.149341, 0.163343 ) 16 | texture = ExtResource( 1 ) 17 | 18 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 19 | shape = SubResource( 1 ) 20 | -------------------------------------------------------------------------------- /Scenes/GameInstance.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=2] 2 | 3 | [ext_resource path="res://Scripts/GameInstance.gd" type="Script" id=1] 4 | [ext_resource path="res://Assets/floor.png" type="Texture" id=2] 5 | [ext_resource path="res://Assets/eye.png" type="Texture" id=3] 6 | 7 | [sub_resource type="Animation" id=1] 8 | length = 10.0 9 | loop = true 10 | tracks/0/type = "value" 11 | tracks/0/path = NodePath(".:modulate") 12 | tracks/0/interp = 1 13 | tracks/0/loop_wrap = true 14 | tracks/0/imported = false 15 | tracks/0/enabled = true 16 | tracks/0/keys = { 17 | "times": PoolRealArray( 0, 5.1, 10 ), 18 | "transitions": PoolRealArray( 1, 1, 1 ), 19 | "update": 0, 20 | "values": [ Color( 0, 0, 0, 1 ), Color( 1, 1, 1, 1 ), Color( 0, 0, 0, 1 ) ] 21 | } 22 | tracks/1/type = "value" 23 | tracks/1/path = NodePath(".:scale") 24 | tracks/1/interp = 1 25 | tracks/1/loop_wrap = true 26 | tracks/1/imported = false 27 | tracks/1/enabled = true 28 | tracks/1/keys = { 29 | "times": PoolRealArray( 0, 5, 10 ), 30 | "transitions": PoolRealArray( 1, 1, 1 ), 31 | "update": 0, 32 | "values": [ Vector2( 2, 2 ), Vector2( 2.2, 2.2 ), Vector2( 2, 2 ) ] 33 | } 34 | 35 | [sub_resource type="GDScript" id=2] 36 | script/source = "extends AnimationPlayer 37 | 38 | func _ready(): 39 | get_parent().visible = true 40 | play(\"base\") 41 | " 42 | 43 | [node name="GameInstance" type="Node2D"] 44 | script = ExtResource( 1 ) 45 | 46 | [node name="Floor" type="Sprite" parent="."] 47 | position = Vector2( -3.05176e-05, -3.05176e-05 ) 48 | scale = Vector2( 15, 15 ) 49 | texture = ExtResource( 2 ) 50 | region_enabled = true 51 | region_rect = Rect2( 0, 0.156, 128, 128 ) 52 | 53 | [node name="ParallaxBackground" type="ParallaxBackground" parent="."] 54 | follow_viewport_enable = true 55 | follow_viewport_scale = 0.3 56 | scroll_offset = Vector2( 1, 1 ) 57 | scroll_base_offset = Vector2( 1, 1 ) 58 | scroll_limit_begin = Vector2( 1, 1 ) 59 | 60 | [node name="ParallaxLayer" type="ParallaxLayer" parent="ParallaxBackground"] 61 | 62 | [node name="Eyes" type="Sprite" parent="ParallaxBackground/ParallaxLayer"] 63 | visible = false 64 | modulate = Color( 0.980392, 0.980392, 0.980392, 1 ) 65 | position = Vector2( 8.1127, 21.1281 ) 66 | scale = Vector2( 2.3, 2.3 ) 67 | texture = ExtResource( 3 ) 68 | region_enabled = true 69 | region_rect = Rect2( 0, 0.1, 4096, 4096 ) 70 | 71 | [node name="AnimationPlayer" type="AnimationPlayer" parent="ParallaxBackground/ParallaxLayer/Eyes"] 72 | anims/base = SubResource( 1 ) 73 | script = SubResource( 2 ) 74 | 75 | [node name="PLAYERS" type="Node2D" parent="."] 76 | position = Vector2( -78.4003, 0 ) 77 | 78 | [node name="CanvasLayer" type="CanvasLayer" parent="."] 79 | 80 | [node name="Score" type="RichTextLabel" parent="CanvasLayer"] 81 | margin_left = 19.799 82 | margin_top = 19.799 83 | margin_right = 200.799 84 | margin_bottom = 86.799 85 | text = "Player 1 [0] 86 | Player 2 [5]" 87 | 88 | [node name="End" type="Label" parent="CanvasLayer"] 89 | visible = false 90 | anchor_right = 1.0 91 | anchor_bottom = 1.0 92 | text = "You have won the game" 93 | align = 1 94 | valign = 1 95 | __meta__ = { 96 | "_edit_use_anchors_": false 97 | } 98 | 99 | [node name="GoToMainMenu" type="Button" parent="CanvasLayer/End"] 100 | anchor_left = 0.5 101 | anchor_top = 0.5 102 | anchor_right = 0.5 103 | anchor_bottom = 0.5 104 | margin_left = -54.7903 105 | margin_top = 19.345 106 | margin_right = 44.2097 107 | margin_bottom = 39.345 108 | text = "Back to Menu" 109 | __meta__ = { 110 | "_edit_use_anchors_": false 111 | } 112 | 113 | [node name="Panel" type="Panel" parent="CanvasLayer"] 114 | margin_left = 9.0 115 | margin_top = 103.0 116 | margin_right = 137.0 117 | margin_bottom = 152.0 118 | __meta__ = { 119 | "_edit_use_anchors_": false 120 | } 121 | 122 | [node name="Label" type="Label" parent="CanvasLayer/Panel"] 123 | anchor_right = 1.0 124 | margin_top = 6.0 125 | margin_bottom = 20.0 126 | text = "LAG" 127 | align = 1 128 | valign = 1 129 | __meta__ = { 130 | "_edit_use_anchors_": false 131 | } 132 | 133 | [node name="HSlider" type="HSlider" parent="CanvasLayer/Panel"] 134 | anchor_left = 0.5 135 | anchor_top = 1.0 136 | anchor_right = 0.5 137 | anchor_bottom = 1.0 138 | margin_left = -48.5 139 | margin_top = -26.0 140 | margin_right = 48.5 141 | margin_bottom = -10.0 142 | max_value = 1.0 143 | step = 0.01 144 | __meta__ = { 145 | "_edit_use_anchors_": false 146 | } 147 | 148 | [node name="Panel1" type="Panel" parent="CanvasLayer"] 149 | margin_left = 9.0 150 | margin_top = 164.0 151 | margin_right = 137.0 152 | margin_bottom = 213.0 153 | __meta__ = { 154 | "_edit_use_anchors_": false 155 | } 156 | 157 | [node name="Label" type="Label" parent="CanvasLayer/Panel1"] 158 | anchor_right = 1.0 159 | margin_top = 6.0 160 | margin_bottom = 20.0 161 | text = "LOSS" 162 | align = 1 163 | valign = 1 164 | __meta__ = { 165 | "_edit_use_anchors_": false 166 | } 167 | 168 | [node name="HSlider" type="HSlider" parent="CanvasLayer/Panel1"] 169 | anchor_left = 0.5 170 | anchor_top = 1.0 171 | anchor_right = 0.5 172 | anchor_bottom = 1.0 173 | margin_left = -48.5 174 | margin_top = -26.0 175 | margin_right = 48.5 176 | margin_bottom = -10.0 177 | min_value = 2.0 178 | max_value = 5.0 179 | value = 5.0 180 | __meta__ = { 181 | "_edit_use_anchors_": false 182 | } 183 | 184 | [node name="SPAWNS" type="Node2D" parent="."] 185 | 186 | [node name="0" type="Position2D" parent="SPAWNS"] 187 | position = Vector2( -599.722, -602.242 ) 188 | 189 | [node name="1" type="Position2D" parent="SPAWNS"] 190 | position = Vector2( 602.242, -594.683 ) 191 | 192 | [node name="2" type="Position2D" parent="SPAWNS"] 193 | position = Vector2( 602.242, 597.202 ) 194 | 195 | [node name="3" type="Position2D" parent="SPAWNS"] 196 | position = Vector2( -604.762, 599.722 ) 197 | 198 | [node name="4" type="Position2D" parent="SPAWNS"] 199 | -------------------------------------------------------------------------------- /Scenes/GameInstanceServer.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://Scripts/GameInstance.gd" type="Script" id=1] 4 | 5 | [sub_resource type="World" id=1] 6 | 7 | [node name="GameInstance" type="Viewport"] 8 | size = Vector2( 64, 64 ) 9 | own_world = true 10 | world = SubResource( 1 ) 11 | hdr = false 12 | disable_3d = true 13 | usage = 0 14 | script = ExtResource( 1 ) 15 | 16 | [node name="PLAYERS" type="Node2D" parent="."] 17 | position = Vector2( -78.4003, 0 ) 18 | 19 | [node name="SPAWNS" type="Node2D" parent="."] 20 | 21 | [node name="0" type="Position2D" parent="SPAWNS"] 22 | position = Vector2( -599.722, -602.242 ) 23 | 24 | [node name="1" type="Position2D" parent="SPAWNS"] 25 | position = Vector2( 602.242, -594.683 ) 26 | 27 | [node name="2" type="Position2D" parent="SPAWNS"] 28 | position = Vector2( 602.242, 597.202 ) 29 | 30 | [node name="3" type="Position2D" parent="SPAWNS"] 31 | position = Vector2( -604.762, 599.722 ) 32 | 33 | [node name="4" type="Position2D" parent="SPAWNS"] 34 | -------------------------------------------------------------------------------- /Scenes/Main_Menu.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=2] 2 | 3 | [sub_resource type="GDScript" id=1] 4 | script/source = "extends Control 5 | 6 | func _ready(): 7 | if !NetworkManager.is_server: 8 | on_disconnected() 9 | LobbyService.connect(\"client_on_gameover\", self, \"on_gameover\") 10 | LobbyService.connect(\"client_on_lobby_start\", self, \"on_lobby_start\") 11 | NetworkManager.connect(\"client_on_disconnected\", self, \"on_disconnected\") 12 | NetworkManager.connect(\"client_on_connected\", self, \"on_connected\") 13 | NetworkManager.emit_signal(\"client_start_connecting\") 14 | setup_main_menu_ui() 15 | setup_account_ui() 16 | else: 17 | $Connect/Label.text = \"Server is running\" 18 | 19 | func on_disconnected(): 20 | $Account.visible = false 21 | $Connect.visible = true 22 | $Main.visible = false 23 | 24 | func on_connected(): 25 | $Account.visible = true 26 | $Connect.visible = false 27 | $Main.visible = false 28 | 29 | func on_gameover(): 30 | on_show_main_menu() 31 | 32 | func on_lobby_start(): 33 | visible = false 34 | 35 | # Account ------- 36 | func setup_account_ui(): 37 | $Account/Login.connect(\"pressed\", self, \"on_authorize_pressed\", [\"login\"]) 38 | $Account/Register.connect(\"pressed\", self, \"on_authorize_pressed\", [\"register\"]) 39 | AuthService.connect(\"client_on_authorized\", self, \"on_show_main_menu\") 40 | 41 | func on_authorize_pressed(type): 42 | if $Account/User.text.length() < 3 or $Account/User.text.length() > 12: 43 | Notifications.notify(\"Invalid username (3 to 12 characters allowed)\") 44 | return 45 | if $Account/Password.text.length() < 6 or $Account/Password.text.length() > 32: 46 | Notifications.notify(\"Invalid password (6 to 32 characters allowed)\") 47 | return 48 | 49 | AuthService.client_authorize($Account/User.text, $Account/Password.text, type) 50 | 51 | func set_auto_login(index): 52 | $Account/User.text = AuthService.auto_login_creds[index].user 53 | $Account/Password.text = AuthService.auto_login_creds[index].password 54 | 55 | # Main Menu ------- 56 | func setup_main_menu_ui(): 57 | $Main/Play.connect(\"pressed\", self, \"on_play_pressed\") 58 | 59 | func on_show_main_menu(): 60 | $Main/AnimationPlayer.stop() 61 | $Main/Play.disabled = false 62 | $Account.visible = false 63 | $Connect.visible = false 64 | $Main.visible = true 65 | visible = true 66 | print(\"everything visible..\") 67 | 68 | func on_play_pressed(): 69 | $Main/AnimationPlayer.play(\"base\") 70 | LobbyService.client_play() 71 | $Main/Play.disabled = true 72 | " 73 | 74 | [sub_resource type="StyleBoxFlat" id=2] 75 | bg_color = Color( 0, 0.501961, 0.992157, 1 ) 76 | 77 | [sub_resource type="StyleBoxFlat" id=3] 78 | bg_color = Color( 0, 0.168627, 0.25098, 1 ) 79 | 80 | [sub_resource type="Animation" id=4] 81 | loop = true 82 | tracks/0/type = "value" 83 | tracks/0/path = NodePath("ProgressBar:value") 84 | tracks/0/interp = 1 85 | tracks/0/loop_wrap = true 86 | tracks/0/imported = false 87 | tracks/0/enabled = true 88 | tracks/0/keys = { 89 | "times": PoolRealArray( 0, 1 ), 90 | "transitions": PoolRealArray( 1, 1 ), 91 | "update": 0, 92 | "values": [ 0.0, 100.0 ] 93 | } 94 | tracks/1/type = "value" 95 | tracks/1/path = NodePath("ProgressBar2:value") 96 | tracks/1/interp = 1 97 | tracks/1/loop_wrap = true 98 | tracks/1/imported = false 99 | tracks/1/enabled = true 100 | tracks/1/keys = { 101 | "times": PoolRealArray( 0, 1 ), 102 | "transitions": PoolRealArray( 1, 1 ), 103 | "update": 0, 104 | "values": [ 0.0, 100.0 ] 105 | } 106 | 107 | [node name="Main_Menu" type="Control"] 108 | anchor_right = 1.0 109 | anchor_bottom = 1.0 110 | script = SubResource( 1 ) 111 | __meta__ = { 112 | "_edit_use_anchors_": false 113 | } 114 | 115 | [node name="Connect" type="Control" parent="."] 116 | anchor_right = 1.0 117 | anchor_bottom = 1.0 118 | __meta__ = { 119 | "_edit_use_anchors_": false 120 | } 121 | 122 | [node name="Label" type="Label" parent="Connect"] 123 | anchor_left = 0.5 124 | anchor_top = 0.5 125 | anchor_right = 0.5 126 | anchor_bottom = 0.5 127 | margin_left = -102.5 128 | margin_top = -7.0 129 | margin_right = 102.5 130 | margin_bottom = 7.0 131 | text = "Connecting to Online-Services..." 132 | align = 1 133 | __meta__ = { 134 | "_edit_use_anchors_": false 135 | } 136 | 137 | [node name="Account" type="Control" parent="."] 138 | visible = false 139 | anchor_right = 2.0 140 | anchor_bottom = 2.0 141 | margin_right = -1024.0 142 | margin_bottom = -600.0 143 | __meta__ = { 144 | "_edit_use_anchors_": false 145 | } 146 | 147 | [node name="User" type="LineEdit" parent="Account"] 148 | anchor_left = 0.5 149 | anchor_top = 0.5 150 | anchor_right = 0.5 151 | anchor_bottom = 0.5 152 | margin_left = -108.5 153 | margin_top = -54.2596 154 | margin_right = 108.5 155 | margin_bottom = -26.2596 156 | placeholder_text = "Enter Username" 157 | 158 | [node name="Password" type="LineEdit" parent="Account"] 159 | anchor_left = 0.5 160 | anchor_top = 0.5 161 | anchor_right = 0.5 162 | anchor_bottom = 0.5 163 | margin_left = -108.5 164 | margin_top = -14.0 165 | margin_right = 108.5 166 | margin_bottom = 14.0 167 | secret = true 168 | placeholder_text = "Enter Password" 169 | 170 | [node name="Register" type="Button" parent="Account"] 171 | anchor_left = 0.5 172 | anchor_top = 0.5 173 | anchor_right = 0.5 174 | anchor_bottom = 0.5 175 | margin_left = -108.0 176 | margin_top = 28.0 177 | margin_right = -13.0 178 | margin_bottom = 58.0 179 | text = "REGISTER" 180 | 181 | [node name="Login" type="Button" parent="Account"] 182 | anchor_left = 0.5 183 | anchor_top = 0.5 184 | anchor_right = 0.5 185 | anchor_bottom = 0.5 186 | margin_left = 7.0 187 | margin_top = 28.0 188 | margin_right = 110.0 189 | margin_bottom = 58.0 190 | text = "LOGIN" 191 | __meta__ = { 192 | "_edit_use_anchors_": false 193 | } 194 | 195 | [node name="Main" type="Control" parent="."] 196 | visible = false 197 | anchor_right = 2.0 198 | anchor_bottom = 2.0 199 | margin_right = -1024.0 200 | margin_bottom = -600.0 201 | __meta__ = { 202 | "_edit_use_anchors_": false 203 | } 204 | 205 | [node name="Play" type="Button" parent="Main"] 206 | anchor_left = 0.5 207 | anchor_top = 0.5 208 | anchor_right = 0.5 209 | anchor_bottom = 0.5 210 | margin_left = -67.0002 211 | margin_top = -21.4997 212 | margin_right = 67.0002 213 | margin_bottom = 21.4997 214 | text = "Quick Play" 215 | __meta__ = { 216 | "_edit_use_anchors_": false 217 | } 218 | 219 | [node name="ProgressBar" type="ProgressBar" parent="Main"] 220 | anchor_left = 0.5 221 | anchor_top = 0.5 222 | anchor_right = 0.5 223 | anchor_bottom = 0.5 224 | margin_left = -66.9077 225 | margin_top = 28.4078 226 | margin_right = -2.90771 227 | margin_bottom = 42.4078 228 | custom_styles/fg = SubResource( 2 ) 229 | custom_styles/bg = SubResource( 3 ) 230 | custom_colors/font_color = Color( 0, 0, 0, 0 ) 231 | __meta__ = { 232 | "_edit_use_anchors_": false 233 | } 234 | 235 | [node name="ProgressBar2" type="ProgressBar" parent="Main"] 236 | anchor_left = 0.5 237 | anchor_top = 0.5 238 | anchor_right = 0.5 239 | anchor_bottom = 0.5 240 | margin_left = 65.644 241 | margin_top = 28.4078 242 | margin_right = 129.644 243 | margin_bottom = 42.4078 244 | rect_scale = Vector2( -1, 1 ) 245 | custom_styles/fg = SubResource( 2 ) 246 | custom_styles/bg = SubResource( 3 ) 247 | custom_colors/font_color = Color( 0, 0, 0, 0 ) 248 | __meta__ = { 249 | "_edit_use_anchors_": false 250 | } 251 | 252 | [node name="AnimationPlayer" type="AnimationPlayer" parent="Main"] 253 | anims/base = SubResource( 4 ) 254 | -------------------------------------------------------------------------------- /Scenes/Notifications.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [sub_resource type="StyleBoxFlat" id=1] 4 | bg_color = Color( 0, 0, 0, 1 ) 5 | border_width_left = 1 6 | border_width_top = 1 7 | border_width_right = 1 8 | border_width_bottom = 1 9 | 10 | [sub_resource type="GDScript" id=2] 11 | script/source = "extends Panel 12 | 13 | var time = 0.0 14 | var notifications = [] 15 | 16 | func _process(delta): 17 | if notifications.size() > 0 and time <= 0.0: 18 | $MESSAGE.text = notifications.pop_front() 19 | visible = true 20 | time = 3.0 21 | elif time <= 0.0: 22 | visible = false 23 | 24 | if time > 0.0: 25 | time -= delta 26 | 27 | func notify(text): 28 | notifications.push_back(text) 29 | " 30 | 31 | [node name="NOTIFICATION" type="Panel"] 32 | anchor_left = 1.0 33 | anchor_top = 1.0 34 | anchor_right = 1.0 35 | anchor_bottom = 1.0 36 | margin_left = -202.0 37 | margin_top = -96.0 38 | margin_right = -10.0 39 | margin_bottom = -10.0 40 | custom_styles/panel = SubResource( 1 ) 41 | script = SubResource( 2 ) 42 | __meta__ = { 43 | "_edit_use_anchors_": false 44 | } 45 | 46 | [node name="MESSAGE" type="Label" parent="."] 47 | anchor_right = 1.0 48 | anchor_bottom = 1.0 49 | margin_left = 9.0 50 | margin_top = 3.0 51 | margin_right = -8.0 52 | margin_bottom = -4.0 53 | text = "NOTIFICATION" 54 | align = 1 55 | valign = 1 56 | autowrap = true 57 | clip_text = true 58 | __meta__ = { 59 | "_edit_use_anchors_": false 60 | } 61 | -------------------------------------------------------------------------------- /Scenes/Player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=24 format=2] 2 | 3 | [ext_resource path="res://Assets/front.png" type="Texture" id=1] 4 | [ext_resource path="res://Assets/back.png" type="Texture" id=2] 5 | [ext_resource path="res://Assets/side.png" type="Texture" id=3] 6 | [ext_resource path="res://Assets/eye.png" type="Texture" id=4] 7 | [ext_resource path="res://Scripts/Player.gd" type="Script" id=5] 8 | 9 | [sub_resource type="SpriteFrames" id=1] 10 | animations = [ { 11 | "frames": [ ExtResource( 2 ), ExtResource( 1 ), ExtResource( 3 ) ], 12 | "loop": false, 13 | "name": "default", 14 | "speed": 5.0 15 | } ] 16 | 17 | [sub_resource type="Animation" id=2] 18 | length = 0.3 19 | loop = true 20 | step = 0.01 21 | tracks/0/type = "value" 22 | tracks/0/path = NodePath("AnimatedSprite:frame") 23 | tracks/0/interp = 1 24 | tracks/0/loop_wrap = true 25 | tracks/0/imported = false 26 | tracks/0/enabled = true 27 | tracks/0/keys = { 28 | "times": PoolRealArray( 0 ), 29 | "transitions": PoolRealArray( 1 ), 30 | "update": 1, 31 | "values": [ 0 ] 32 | } 33 | tracks/1/type = "value" 34 | tracks/1/path = NodePath("AnimatedSprite:position") 35 | tracks/1/interp = 1 36 | tracks/1/loop_wrap = true 37 | tracks/1/imported = false 38 | tracks/1/enabled = true 39 | tracks/1/keys = { 40 | "times": PoolRealArray( 0, 0.08, 0.15, 0.23, 0.3 ), 41 | "transitions": PoolRealArray( 1, 1, 1, 1, 1 ), 42 | "update": 0, 43 | "values": [ Vector2( 1.12246, -0.491079 ), Vector2( -0.14966, -1.66499 ), Vector2( -1.26277, 0.0701504 ), Vector2( -0.201106, -1.98302 ), Vector2( 1.12246, -0.491079 ) ] 44 | } 45 | tracks/2/type = "value" 46 | tracks/2/path = NodePath("AnimatedSprite:rotation_degrees") 47 | tracks/2/interp = 1 48 | tracks/2/loop_wrap = true 49 | tracks/2/imported = false 50 | tracks/2/enabled = true 51 | tracks/2/keys = { 52 | "times": PoolRealArray( 0, 0.15, 0.3 ), 53 | "transitions": PoolRealArray( 1, 1, 1 ), 54 | "update": 0, 55 | "values": [ 7.72939, -16.5958, 7.72939 ] 56 | } 57 | 58 | [sub_resource type="Animation" id=3] 59 | length = 0.3 60 | loop = true 61 | step = 0.01 62 | tracks/0/type = "value" 63 | tracks/0/path = NodePath("AnimatedSprite:frame") 64 | tracks/0/interp = 1 65 | tracks/0/loop_wrap = true 66 | tracks/0/imported = false 67 | tracks/0/enabled = true 68 | tracks/0/keys = { 69 | "times": PoolRealArray( 0 ), 70 | "transitions": PoolRealArray( 1 ), 71 | "update": 1, 72 | "values": [ 1 ] 73 | } 74 | tracks/1/type = "value" 75 | tracks/1/path = NodePath("AnimatedSprite:position") 76 | tracks/1/interp = 1 77 | tracks/1/loop_wrap = true 78 | tracks/1/imported = false 79 | tracks/1/enabled = true 80 | tracks/1/keys = { 81 | "times": PoolRealArray( 0, 0.08, 0.15, 0.23, 0.3 ), 82 | "transitions": PoolRealArray( 1, 1, 1, 1, 1 ), 83 | "update": 0, 84 | "values": [ Vector2( 1.12246, -0.491079 ), Vector2( -0.14966, -1.66499 ), Vector2( -1.26277, 0.0701504 ), Vector2( -0.201106, -1.98302 ), Vector2( 1.12246, -0.491079 ) ] 85 | } 86 | tracks/2/type = "value" 87 | tracks/2/path = NodePath("AnimatedSprite:rotation_degrees") 88 | tracks/2/interp = 1 89 | tracks/2/loop_wrap = true 90 | tracks/2/imported = false 91 | tracks/2/enabled = true 92 | tracks/2/keys = { 93 | "times": PoolRealArray( 0, 0.15, 0.3 ), 94 | "transitions": PoolRealArray( 1, 1, 1 ), 95 | "update": 0, 96 | "values": [ 7.72939, -16.5958, 7.72939 ] 97 | } 98 | 99 | [sub_resource type="Animation" id=4] 100 | resource_name = "walk_sideways_left" 101 | length = 0.3 102 | loop = true 103 | step = 0.01 104 | tracks/0/type = "value" 105 | tracks/0/path = NodePath("AnimatedSprite:frame") 106 | tracks/0/interp = 1 107 | tracks/0/loop_wrap = true 108 | tracks/0/imported = false 109 | tracks/0/enabled = true 110 | tracks/0/keys = { 111 | "times": PoolRealArray( 0 ), 112 | "transitions": PoolRealArray( 1 ), 113 | "update": 1, 114 | "values": [ 2 ] 115 | } 116 | tracks/1/type = "value" 117 | tracks/1/path = NodePath("AnimatedSprite:position") 118 | tracks/1/interp = 1 119 | tracks/1/loop_wrap = true 120 | tracks/1/imported = false 121 | tracks/1/enabled = true 122 | tracks/1/keys = { 123 | "times": PoolRealArray( 0, 0.08, 0.15, 0.23, 0.3 ), 124 | "transitions": PoolRealArray( 1, 1, 1, 1, 1 ), 125 | "update": 0, 126 | "values": [ Vector2( 1.12246, -0.491079 ), Vector2( -0.14966, -1.66499 ), Vector2( -1.26277, 0.0701504 ), Vector2( -0.201106, -1.98302 ), Vector2( 1.12246, -0.491079 ) ] 127 | } 128 | tracks/2/type = "value" 129 | tracks/2/path = NodePath("AnimatedSprite:rotation_degrees") 130 | tracks/2/interp = 1 131 | tracks/2/loop_wrap = true 132 | tracks/2/imported = false 133 | tracks/2/enabled = true 134 | tracks/2/keys = { 135 | "times": PoolRealArray( 0, 0.15, 0.3 ), 136 | "transitions": PoolRealArray( 1, 1, 1 ), 137 | "update": 0, 138 | "values": [ 7.72939, -16.5958, 7.72939 ] 139 | } 140 | tracks/3/type = "value" 141 | tracks/3/path = NodePath("AnimatedSprite:flip_h") 142 | tracks/3/interp = 1 143 | tracks/3/loop_wrap = true 144 | tracks/3/imported = false 145 | tracks/3/enabled = true 146 | tracks/3/keys = { 147 | "times": PoolRealArray( 0 ), 148 | "transitions": PoolRealArray( 1 ), 149 | "update": 1, 150 | "values": [ true ] 151 | } 152 | 153 | [sub_resource type="Animation" id=5] 154 | length = 0.3 155 | loop = true 156 | step = 0.01 157 | tracks/0/type = "value" 158 | tracks/0/path = NodePath("AnimatedSprite:frame") 159 | tracks/0/interp = 1 160 | tracks/0/loop_wrap = true 161 | tracks/0/imported = false 162 | tracks/0/enabled = true 163 | tracks/0/keys = { 164 | "times": PoolRealArray( 0 ), 165 | "transitions": PoolRealArray( 1 ), 166 | "update": 1, 167 | "values": [ 2 ] 168 | } 169 | tracks/1/type = "value" 170 | tracks/1/path = NodePath("AnimatedSprite:position") 171 | tracks/1/interp = 1 172 | tracks/1/loop_wrap = true 173 | tracks/1/imported = false 174 | tracks/1/enabled = true 175 | tracks/1/keys = { 176 | "times": PoolRealArray( 0, 0.08, 0.15, 0.23, 0.3 ), 177 | "transitions": PoolRealArray( 1, 1, 1, 1, 1 ), 178 | "update": 0, 179 | "values": [ Vector2( 1.12246, -0.491079 ), Vector2( -0.14966, -1.66499 ), Vector2( -1.26277, 0.0701504 ), Vector2( -0.201106, -1.98302 ), Vector2( 1.12246, -0.491079 ) ] 180 | } 181 | tracks/2/type = "value" 182 | tracks/2/path = NodePath("AnimatedSprite:rotation_degrees") 183 | tracks/2/interp = 1 184 | tracks/2/loop_wrap = true 185 | tracks/2/imported = false 186 | tracks/2/enabled = true 187 | tracks/2/keys = { 188 | "times": PoolRealArray( 0, 0.15, 0.3 ), 189 | "transitions": PoolRealArray( 1, 1, 1 ), 190 | "update": 0, 191 | "values": [ 7.72939, -16.5958, 7.72939 ] 192 | } 193 | tracks/3/type = "value" 194 | tracks/3/path = NodePath("AnimatedSprite:flip_h") 195 | tracks/3/interp = 1 196 | tracks/3/loop_wrap = true 197 | tracks/3/imported = false 198 | tracks/3/enabled = true 199 | tracks/3/keys = { 200 | "times": PoolRealArray( 0 ), 201 | "transitions": PoolRealArray( 1 ), 202 | "update": 1, 203 | "values": [ false ] 204 | } 205 | 206 | [sub_resource type="AnimationNodeAnimation" id=6] 207 | animation = "walk_sideways_left" 208 | 209 | [sub_resource type="AnimationNodeAnimation" id=7] 210 | animation = "walk_sideways_right" 211 | 212 | [sub_resource type="AnimationNodeAnimation" id=8] 213 | animation = "walk_backward" 214 | 215 | [sub_resource type="AnimationNodeAnimation" id=9] 216 | animation = "walk_forward" 217 | 218 | [sub_resource type="AnimationNodeBlendSpace2D" id=10] 219 | blend_point_0/node = SubResource( 6 ) 220 | blend_point_0/pos = Vector2( -1, 0 ) 221 | blend_point_1/node = SubResource( 7 ) 222 | blend_point_1/pos = Vector2( 1, 0 ) 223 | blend_point_2/node = SubResource( 8 ) 224 | blend_point_2/pos = Vector2( 0, 1 ) 225 | blend_point_3/node = SubResource( 9 ) 226 | blend_point_3/pos = Vector2( 0, -1 ) 227 | blend_mode = 1 228 | 229 | [sub_resource type="AnimationNodeTimeScale" id=11] 230 | 231 | [sub_resource type="AnimationNodeBlendTree" id=12] 232 | graph_offset = Vector2( -468, 15.7544 ) 233 | nodes/movement/node = SubResource( 10 ) 234 | nodes/movement/position = Vector2( -200, 100 ) 235 | nodes/movement_time/node = SubResource( 11 ) 236 | nodes/movement_time/position = Vector2( 40, 100 ) 237 | nodes/output/position = Vector2( 260, 100 ) 238 | node_connections = [ "output", 0, "movement_time", "movement_time", 0, "movement" ] 239 | 240 | [sub_resource type="RectangleShape2D" id=13] 241 | extents = Vector2( 24.9816, 35.8542 ) 242 | 243 | [sub_resource type="StyleBoxFlat" id=14] 244 | bg_color = Color( 0.203922, 0.392157, 0.839216, 1 ) 245 | 246 | [sub_resource type="StyleBoxFlat" id=15] 247 | bg_color = Color( 1, 0, 0, 1 ) 248 | 249 | [sub_resource type="Gradient" id=16] 250 | colors = PoolColorArray( 1, 0, 0, 1, 1, 0, 0, 1 ) 251 | 252 | [sub_resource type="GradientTexture" id=17] 253 | gradient = SubResource( 16 ) 254 | 255 | [sub_resource type="Animation" id=18] 256 | length = 0.5 257 | tracks/0/type = "value" 258 | tracks/0/path = NodePath("Sprite:modulate") 259 | tracks/0/interp = 1 260 | tracks/0/loop_wrap = true 261 | tracks/0/imported = false 262 | tracks/0/enabled = true 263 | tracks/0/keys = { 264 | "times": PoolRealArray( 0, 0.1, 0.5 ), 265 | "transitions": PoolRealArray( 1, 1, 1 ), 266 | "update": 0, 267 | "values": [ Color( 1, 1, 1, 0 ), Color( 1, 1, 1, 1 ), Color( 1, 1, 1, 0 ) ] 268 | } 269 | 270 | [node name="KinematicBody2D" type="KinematicBody2D" groups=[ 271 | "player", 272 | ]] 273 | script = ExtResource( 5 ) 274 | 275 | [node name="AnimatedSprite" type="AnimatedSprite" parent="."] 276 | light_mask = 2 277 | position = Vector2( -0.977784, -0.48099 ) 278 | rotation = -0.22887 279 | scale = Vector2( 7.32464, 7.32464 ) 280 | frames = SubResource( 1 ) 281 | frame = 1 282 | flip_h = true 283 | 284 | [node name="AnimationPlayer" type="AnimationPlayer" parent="."] 285 | anims/walk_backward = SubResource( 2 ) 286 | anims/walk_forward = SubResource( 3 ) 287 | anims/walk_sideways_left = SubResource( 4 ) 288 | anims/walk_sideways_right = SubResource( 5 ) 289 | 290 | [node name="AnimationTree" type="AnimationTree" parent="."] 291 | tree_root = SubResource( 12 ) 292 | anim_player = NodePath("../AnimationPlayer") 293 | active = true 294 | parameters/movement/blend_position = Vector2( 0, -1 ) 295 | parameters/movement_time/scale = 0.0 296 | 297 | [node name="Camera2D" type="Camera2D" parent="."] 298 | 299 | [node name="Light2D" type="Light2D" parent="."] 300 | position = Vector2( -22.6922, 39.362 ) 301 | scale = Vector2( 2.9814, 2.25166 ) 302 | texture = ExtResource( 4 ) 303 | 304 | [node name="CollisionShape2D" type="CollisionShape2D" parent="."] 305 | position = Vector2( 1.33633, -2.6727 ) 306 | shape = SubResource( 13 ) 307 | 308 | [node name="Info" type="Label" parent="."] 309 | margin_left = -94.3406 310 | margin_top = -117.477 311 | margin_right = 103.659 312 | margin_bottom = -58.4773 313 | text = "NAME & ID" 314 | align = 1 315 | valign = 1 316 | autowrap = true 317 | __meta__ = { 318 | "_edit_use_anchors_": false 319 | } 320 | 321 | [node name="Health" type="ProgressBar" parent="."] 322 | margin_left = -48.2703 323 | margin_top = -80.7838 324 | margin_right = 57.7297 325 | margin_bottom = -66.7838 326 | custom_styles/fg = SubResource( 14 ) 327 | custom_styles/bg = SubResource( 15 ) 328 | custom_colors/font_color = Color( 1, 1, 1, 0 ) 329 | value = 100.0 330 | __meta__ = { 331 | "_edit_use_anchors_": false 332 | } 333 | 334 | [node name="Sprite" type="Sprite" parent="."] 335 | modulate = Color( 1, 1, 1, 0 ) 336 | scale = Vector2( 0.0430662, 111.596 ) 337 | texture = SubResource( 17 ) 338 | 339 | [node name="FX" type="AnimationPlayer" parent="."] 340 | anims/damage = SubResource( 18 ) 341 | -------------------------------------------------------------------------------- /Scripts/Autoloads/NetworkManager.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends Node 5 | 6 | # Server 7 | var is_server 8 | var server_port = 3333 9 | var WSS : WebSocketServer 10 | 11 | # Client 12 | var retry_time = 5.0 13 | var client_url = "ws://127.0.0.1:" 14 | var retry_timer = 0.0 15 | var retry_connecting 16 | var WSC : WebSocketClient 17 | 18 | signal client_on_connected 19 | signal client_on_disconnected 20 | signal client_start_connecting 21 | signal server_client_connected 22 | signal server_client_disconnected 23 | 24 | func _ready(): 25 | for i in range(OS.get_cmdline_args().size()): 26 | match OS.get_cmdline_args()[i]: 27 | "--server": 28 | is_server = true 29 | "--userindex": 30 | get_node("/root/Main_Menu").set_auto_login(int(OS.get_cmdline_args()[i+1])) 31 | 32 | if !is_server: 33 | connect("client_start_connecting", self, "client_try_connect") 34 | else: 35 | print("Server init...") 36 | WSS = WebSocketServer.new() 37 | WSS.listen(3333, PoolStringArray([]), true) 38 | get_tree().set_network_peer(WSS) 39 | get_tree().network_peer.connect("peer_connected", self, "server_on_peer_connected") 40 | get_tree().network_peer.connect("peer_disconnected", self, "server_on_peer_disconnected") 41 | OS.set_window_title("Server on Port " + str(server_port)) 42 | OS.window_minimized = true 43 | 44 | # Server & Client 45 | func _process(delta): 46 | if is_server: 47 | WSS.poll() 48 | else: 49 | if WSC.get_connection_status() == WSC.CONNECTION_CONNECTING or WSC.get_connection_status() == WSC.CONNECTION_CONNECTED: 50 | WSC.poll() 51 | 52 | if retry_connecting: 53 | if retry_timer < 0.0: 54 | client_try_connect() 55 | retry_timer = retry_time 56 | retry_timer -= delta 57 | 58 | # Client ----------- 59 | func client_try_connect(): 60 | retry_connecting = true 61 | WSC = WebSocketClient.new() 62 | WSC.connect_to_url(client_url + str(server_port), PoolStringArray([]), true) 63 | set_network_master(1) 64 | get_tree().set_network_peer(WSC) 65 | get_tree().network_peer.connect("connection_succeeded", self, "client_on_connected") 66 | get_tree().network_peer.connect("connection_failed", self, "client_on_disconnect_server") 67 | get_tree().network_peer.connect("server_disconnected", self, "client_on_disconnect_server") 68 | 69 | func client_on_disconnect_server(): 70 | Notifications.notify("Disconnected from Online-Services") 71 | if !retry_connecting: 72 | emit_signal("client_on_disconnected") 73 | retry_connecting = true 74 | 75 | func client_on_connected(): 76 | retry_connecting = false 77 | Notifications.notify("Connected to Online-Services") 78 | emit_signal("client_on_connected") 79 | 80 | # Server ----------- 81 | func server_on_peer_connected(id): 82 | Notifications.notify("Peer connected: " + str(id)) 83 | emit_signal("server_client_connected", int(id)) 84 | 85 | func server_on_peer_disconnected(id): 86 | Notifications.notify("Peer disconnected " + str(id)) 87 | emit_signal("server_client_disconnected", int(id)) 88 | 89 | func caller(): 90 | return get_tree().get_rpc_sender_id() 91 | -------------------------------------------------------------------------------- /Scripts/Autoloads/Tools.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var file = File.new() 4 | 5 | func open_json_file(path): 6 | if file.open(path, file.READ) == 0: 7 | var data = JSON.parse(file.get_as_text()).result 8 | file.close() 9 | return data 10 | return null 11 | 12 | func get_dict_val(dict, val): 13 | if dict.has(val): 14 | return dict.get(val) 15 | return null 16 | -------------------------------------------------------------------------------- /Scripts/Autoloads/UUID.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2018 Xavier Sellier 3 | # MIT License 4 | extends Node 5 | 6 | static func getRandomInt(max_value): 7 | randomize() 8 | return randi() % max_value 9 | 10 | static func randomBytes(n): 11 | var r = [] 12 | 13 | for index in range(0, n): 14 | r.append(getRandomInt(256)) 15 | 16 | return r 17 | 18 | static func uuidbin(): 19 | var b = randomBytes(16) 20 | 21 | b[6] = (b[6] & 0x0f) | 0x40 22 | b[8] = (b[8] & 0x3f) | 0x80 23 | return b 24 | 25 | static func NewID() -> String: 26 | var b = uuidbin() 27 | 28 | var low = '%02x%02x%02x%02x' % [b[0], b[1], b[2], b[3]] 29 | var mid = '%02x%02x' % [b[4], b[5]] 30 | var hi = '%02x%02x' % [b[6], b[7]] 31 | var clock = '%02x%02x' % [b[8], b[9]] 32 | var node = '%02x%02x%02x%02x%02x%02x' % [b[10], b[11], b[12], b[13], b[14], b[15]] 33 | 34 | return '%s%s%s%s%s' % [low, mid, hi, clock, node] 35 | -------------------------------------------------------------------------------- /Scripts/Bullet.gd: -------------------------------------------------------------------------------- 1 | extends Area2D 2 | 3 | var DAMAGE = 25 4 | var SPEED = 500.0 5 | var owner_id = null 6 | var auto_destroy = 5.0 7 | var game_instance = null 8 | var direction = Vector2.ZERO 9 | var target_position = Vector2.ZERO 10 | 11 | func _ready(): 12 | target_position = global_position 13 | connect("body_entered", self, "on_body_entered") 14 | 15 | func _physics_process(delta): 16 | if NetworkManager.is_server: 17 | global_position = global_position + direction * SPEED * delta 18 | game_instance.server_snapshot[str(name)] = { position = global_position } 19 | 20 | func _process(delta): 21 | auto_destroy -= delta 22 | if auto_destroy <= 0.0: 23 | queue_free() 24 | 25 | func on_body_entered(body): 26 | if body.is_in_group("player"): 27 | if int(body.name) != owner_id: 28 | if NetworkManager.is_server: 29 | queue_free() 30 | body.take_damage(get_network_master(), DAMAGE) 31 | game_instance.server_queue_free_broadcast(name, str(name)) 32 | -------------------------------------------------------------------------------- /Scripts/GameInstance.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends "res://Scripts/GameInstanceServer.gd" 5 | 6 | var bullet_scene = "res://Scenes/Bullet.tscn" 7 | var client_user_scene = "res://Scenes/Player.tscn" 8 | var server_user_scene = load("res://Scenes/Player.tscn") 9 | 10 | # Client ------ 11 | func _ready(): 12 | if !NetworkManager.is_server: 13 | rpc_id(1, "cl_user_ready") 14 | $CanvasLayer/Panel/HSlider.connect("value_changed", self, "on_lag_changed") 15 | $CanvasLayer/End/GoToMainMenu.connect("pressed", self, "on_go_to_main_menu") 16 | $CanvasLayer/Panel1/HSlider.connect("value_changed", self, "on_loss_changed") 17 | 18 | # Server sent us an object, dissect it 19 | func _on_server_object_received(obj): 20 | if Tools.get_dict_val(obj, "type") == "anim": 21 | var node = get_player_node(obj.id) 22 | if node != null: 23 | node.anim_tree.set(obj.key, obj.value) 24 | elif Tools.get_dict_val(obj, "score") != null: 25 | $CanvasLayer/Score.text = obj.score 26 | elif Tools.get_dict_val(obj, "type") == "health": 27 | var node = get_player_node(obj.id) 28 | if node != null: 29 | node.get_node("FX").play("damage") 30 | node.get_node("Health").value = int(obj.value) 31 | node.health = obj.value 32 | 33 | # When we create an instance, we check if extra params 34 | # have been supplied to set properties on these new instances 35 | func _on_server_instantiate(data, instance): 36 | if instance == null: return 37 | ._on_server_instantiate(data, instance) 38 | if Tools.get_dict_val(data.params, "direction") != null: 39 | instance.direction = data.params.direction 40 | return 41 | var username = Tools.get_dict_val(data.params, "username") 42 | if username != null: 43 | instance.set_username(username) 44 | 45 | func on_go_to_main_menu(): 46 | LobbyService.emit_signal("client_on_gameover") 47 | queue_free() 48 | 49 | func on_lag_changed(val): 50 | $CanvasLayer/Panel/Label.text = "Lag: " + str(val) + "s" 51 | lag = val 52 | 53 | func on_loss_changed(val): 54 | $CanvasLayer/Panel1/Label.text = "Lose every: " + str(int(val)) + " packets" 55 | loss = int(val) 56 | 57 | puppet func srv_game_over(winner_id): 58 | $PLAYERS.queue_free() 59 | $CanvasLayer/End.visible = true 60 | if int(winner_id) == get_tree().get_network_unique_id(): 61 | $CanvasLayer/End.text = "You have won the game" 62 | else: 63 | $CanvasLayer/End.text = "You have lost the game" 64 | 65 | # Server ------ 66 | const MAX_SCORE = 2 67 | const RESPAWN_TIME = 5 68 | var server_respawn_queue = {} 69 | 70 | # Override: when a user is done connecting and 71 | # ready to play, we spawn their character across 72 | # all other clients 73 | func _on_user_ready(): 74 | server_instantiate_broadcast({ 75 | position = get_node("SPAWNS/" + str(randi()%($SPAWNS.get_child_count() - 1))).global_position, 76 | owner_id = str(NetworkManager.caller()), 77 | name = str(NetworkManager.caller()), 78 | scene = client_user_scene, 79 | path = "PLAYERS", 80 | params = { 81 | username = current_lobby.users[str(NetworkManager.caller())].name, 82 | set_game_instance = true 83 | } 84 | }) 85 | 86 | # if more than one player 87 | # is ready, we start 88 | if ready_count > 1: 89 | server_broadcast_object({ 90 | score = get_score() 91 | }) 92 | 93 | func get_score(): 94 | var res = "Scores:\n" 95 | for user in current_lobby.users.values(): 96 | res += str(user.name) + " [" + str(user.score) + "]\n" 97 | return res 98 | 99 | # User shoots a bullet, 100 | # instantiate it across all clients 101 | remote func cl_shoot(direction): 102 | var node = get_player_node(NetworkManager.caller()) 103 | if node != null: 104 | server_instantiate_broadcast({ 105 | position = node.global_position + direction.normalized() * 100.0, 106 | name = "bullet_" + UUID.NewID().substr(0, 8), 107 | owner_id = str(NetworkManager.caller()), 108 | scene = bullet_scene, 109 | path = ".", 110 | params = { 111 | username = current_lobby.users[str(NetworkManager.caller())].name, 112 | set_game_instance = true, 113 | direction = direction 114 | } 115 | }) 116 | 117 | func server_broadcast_death(killer_id, id): 118 | server_queue_free_broadcast(str(id), "PLAYERS/" + str(id)) 119 | current_lobby.users[str(killer_id)].score += 1 120 | server_broadcast_object({ 121 | score = get_score() 122 | }) 123 | if current_lobby.users[str(killer_id)].score >= MAX_SCORE: 124 | for user in current_lobby.users.values(): 125 | rpc_id(int(user.id), "srv_game_over", killer_id) 126 | user.score = 0 127 | queue_free() 128 | server_respawn_queue[id] = RESPAWN_TIME 129 | 130 | func _process(delta): 131 | for id in server_respawn_queue.keys(): 132 | server_respawn_queue[id] -= delta 133 | if server_respawn_queue[id] <= 0.0: 134 | server_instantiate_broadcast({ 135 | position = get_node("SPAWNS/" + str(randi()%($SPAWNS.get_child_count() - 1))).global_position, 136 | owner_id = str(id), 137 | name = str(id), 138 | scene = client_user_scene, 139 | path = "PLAYERS", 140 | params = { 141 | username = current_lobby.users[str(id)].name, 142 | set_game_instance = true 143 | } 144 | }) 145 | server_respawn_queue.erase(id) 146 | 147 | -------------------------------------------------------------------------------- /Scripts/GameInstanceBase.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | # lag 4 | var lag = 0.0 5 | 6 | # packet loss 7 | var loss = 10 8 | var loss_counter = 0 9 | 10 | var current_lobby 11 | 12 | func _ready(): 13 | set_network_master(1) 14 | 15 | # Get the player node by User-Id 16 | func get_player_node(id): 17 | return get_node2("PLAYERS/" + str(id)) 18 | 19 | # Check if the node exists 20 | # before getting it 21 | func get_node2(path): 22 | if has_node(str(path)): 23 | return get_node(str(path)) 24 | return null 25 | 26 | func game_instance_instantiate(data): 27 | if !has_node(data.path + "/" + str(data.name)): 28 | var new_instance = load(data.scene).instance() 29 | new_instance.set_network_master(int(data.owner_id)) 30 | new_instance.global_position = data.position 31 | new_instance.name = str(data.name) 32 | if Tools.get_dict_val(data.params, "set_game_instance"): 33 | new_instance.game_instance = self 34 | get_node(data.path).add_child(new_instance) 35 | return new_instance 36 | return null 37 | 38 | func game_instance_queue_free(path): 39 | if has_node(path): 40 | get_node(path).queue_free() 41 | -------------------------------------------------------------------------------- /Scripts/GameInstanceClient.gd: -------------------------------------------------------------------------------- 1 | extends "res://Scripts/GameInstanceBase.gd" 2 | 3 | # Interpolation time is at 100ms 4 | # since the server runs at 20 fps 5 | # and the snapshot interval is therefore 50ms. 6 | # 100ms gives us air around >= 3 packets 7 | const INTERPOLATION_TIME = 0.1 8 | 9 | var snapshot_buffer = [] 10 | var last_snapshot_time = 0.0 11 | var current_client_time = 0.0 12 | var current_rendering_time = 0.0 13 | 14 | func _process(delta): 15 | if !NetworkManager.is_server: 16 | current_client_time += delta 17 | current_rendering_time = current_client_time - INTERPOLATION_TIME 18 | 19 | # if we have any snapshots 20 | if snapshot_buffer.size() > 1: 21 | 22 | # if we have more than 2 snapshots (3 or more) 23 | # where the second oldest snapshot's time is way behind the rendering time 24 | while(snapshot_buffer.size() > 2 and current_rendering_time >= snapshot_buffer[1].time): 25 | # drop the oldest snapshot, because it is outdated 26 | snapshot_buffer.pop_front() 27 | 28 | # loop all entities within the second oldest snapshot 29 | for entity_path in snapshot_buffer[1].keys(): 30 | var node = get_node2(entity_path) 31 | if node != null and !(node.is_network_master() and node.is_in_group("player")): 32 | if !snapshot_buffer[0].has(entity_path): continue 33 | if !snapshot_buffer[1].has(entity_path): continue 34 | 35 | # get the current_render_time between the snapshots 36 | # and transform it to a value between 0 and 1, which 37 | # is how close the values of the entity should be to 38 | # the second latest snapshot 39 | 40 | # get time span between oldest snapshot and current_render_time 41 | # then check how much this is on the timespan between the oldest and second oldest snapshot 42 | var t = clamp((snapshot_buffer[1].time - snapshot_buffer[0].time) / (current_rendering_time - snapshot_buffer[0].time), 0, 1) 43 | # lerp lerp, it do be smooth 44 | var dist = (snapshot_buffer[1].time - snapshot_buffer[0].time) * 550.0 45 | node.global_position = lerp(node.global_position, 46 | lerp(snapshot_buffer[0][entity_path].position, 47 | snapshot_buffer[1][entity_path].position, 1.0 - t), 48 | delta * dist * 0.5) 49 | 50 | 51 | func client_send_snapshot(snapshot): 52 | if snapshot != null: 53 | rpc_unreliable_id(1, "cl_snapshot", snapshot) 54 | 55 | func client_object(obj): 56 | rpc_id(1, "cl_broadcast_object", obj) 57 | 58 | puppet func srv_snapshot(snapshot): 59 | if loss > 1: 60 | loss_counter += 1 61 | if loss_counter % loss == 0: 62 | return 63 | snapshot.time = current_client_time + lag 64 | snapshot_buffer.push_back(snapshot) 65 | 66 | puppet func srv_object(obj): 67 | _on_server_object_received(obj) 68 | 69 | puppet func srv_instantiate(data): 70 | var new_instance = game_instance_instantiate(data) 71 | _on_server_instantiate(data, new_instance) 72 | 73 | puppet func srv_destroy(path): 74 | game_instance_queue_free(path) 75 | 76 | # Abstract methods, 77 | # Override these if you need 78 | # their functionality 79 | func _on_server_object_received(obj): pass 80 | func _on_server_instantiate(data, new_instance): pass 81 | -------------------------------------------------------------------------------- /Scripts/GameInstanceServer.gd: -------------------------------------------------------------------------------- 1 | extends "res://Scripts/GameInstanceClient.gd" 2 | 3 | var ready_count = 0 4 | var timestep = 0.03 5 | var current_time = 0.0 6 | var server_snapshot = {} 7 | var server_tracked_objects = [] 8 | var server_tracked_node_instances = {} 9 | 10 | func _process(delta): 11 | if NetworkManager.is_server: 12 | current_time += delta 13 | if current_time >= timestep: 14 | current_time = 0.0 15 | for id in server_snapshot.keys(): 16 | var node = get_player_node(id) 17 | if node != null: 18 | node.server_handle_cl_snapshot(server_snapshot[id]) 19 | for user in current_lobby.users.values(): 20 | rpc_unreliable_id(int(user.id), "srv_snapshot", server_snapshot) 21 | 22 | # User signals that they are ready, 23 | # replicate every node that has been created 24 | # and do the same with all objects that have 25 | # been sent 26 | remote func cl_user_ready(): 27 | ready_count += 1 28 | for tracked_node in server_tracked_node_instances.values(): 29 | rpc_id(int(NetworkManager.caller()), "srv_instantiate", tracked_node) 30 | for tracked_obj in server_tracked_objects: 31 | rpc_id(int(NetworkManager.caller()), "srv_object", tracked_obj) 32 | _on_user_ready() 33 | 34 | # Keep track of the latest values 35 | # on a per-client basis 36 | remote func cl_snapshot(client_snapshot): 37 | server_snapshot["PLAYERS/" + str(NetworkManager.caller())] = client_snapshot 38 | var node = get_player_node(NetworkManager.caller()) 39 | if node != null: 40 | node.global_position = server_snapshot["PLAYERS/" + str(NetworkManager.caller())].position 41 | _on_client_snapshot_updated(server_snapshot["PLAYERS/" + str(NetworkManager.caller())]) 42 | 43 | remote func cl_sync_anim(data): 44 | _on_client_broadcast_animation(data) 45 | data.id = NetworkManager.caller() 46 | for user in current_lobby.users.values(): 47 | rpc_id(int(user.id), "srv_sync_anim", data) 48 | 49 | # Removes a node across all clients and 50 | # remove it from the tracked instances, 51 | # so new clients dont create this node 52 | func server_queue_free_broadcast(name, path): 53 | if server_tracked_node_instances.has(name): 54 | server_tracked_node_instances.erase(name) 55 | game_instance_queue_free(path) 56 | for user in current_lobby.users.values(): 57 | rpc_id(int(user.id), "srv_destroy", path) 58 | 59 | # Instantiates a node across all clients, 60 | # we also keep track of the new instance to 61 | # instantiate it on new clients as well when we 62 | # initially sync the game for them 63 | func server_instantiate_broadcast(data): 64 | server_tracked_node_instances[name] = data 65 | var new_instance = game_instance_instantiate(data) 66 | for user in current_lobby.users.values(): 67 | rpc_id(int(user.id), "srv_instantiate", data) 68 | _on_server_instantiate(data, new_instance) 69 | 70 | # Broadcasts an object with values 71 | # to every client (convenience) 72 | func server_broadcast_object(obj): 73 | server_tracked_objects.push_back(obj) 74 | for user in current_lobby.users.values(): 75 | rpc_id(int(user.id), "srv_object", obj) 76 | 77 | # Broadcasts an object with values 78 | # to every client from another client 79 | remote func cl_broadcast_object(obj): 80 | server_tracked_objects.push_back(obj) 81 | for user in current_lobby.users.values(): 82 | rpc_id(int(user.id), "srv_object", obj) 83 | 84 | # Abstract methods, 85 | # Override these if you need 86 | # their functionality 87 | 88 | # A connected user signals ready 89 | func _on_user_ready(): pass 90 | 91 | # A client sent their world state, 92 | # keep track of additional values if needed 93 | # (quaternion rotations, velocity, timestamps...) 94 | func _on_client_snapshot_updated(client_snapshot): pass 95 | 96 | # We are about to relay a clients 97 | # reliable animation change to other clients 98 | func _on_client_broadcast_animation(data): pass 99 | func _on_server_instantiate(data, instance): pass 100 | -------------------------------------------------------------------------------- /Scripts/Player.gd: -------------------------------------------------------------------------------- 1 | extends KinematicBody2D 2 | 3 | const SPEED = 500.0 4 | const SHOOT_TIMEOUT = 0.5 5 | 6 | var username 7 | var anim_tree 8 | var health = 100 9 | var game_instance 10 | var anim_state = {} 11 | var can_move = true 12 | var shoot_timeout = 0.0 13 | var direction_input = Vector2.ZERO 14 | var aim_direction = Vector2(0, -1) 15 | 16 | func _ready(): 17 | anim_tree = $AnimationTree 18 | anim_tree.active = true 19 | 20 | func _process(delta): 21 | if !is_network_master(): return 22 | direction_input = Vector2.ZERO 23 | $Camera2D.current = true 24 | shoot_timeout -= delta 25 | 26 | if can_move: 27 | if Input.is_key_pressed(KEY_W): 28 | direction_input.y = -1 29 | if Input.is_key_pressed(KEY_S): 30 | direction_input.y = 1 31 | if Input.is_key_pressed(KEY_A): 32 | direction_input.x = -1 33 | if Input.is_key_pressed(KEY_D): 34 | direction_input.x = 1 35 | if direction_input != Vector2.ZERO: 36 | aim_direction = direction_input 37 | if Input.is_action_just_pressed("ui_select") and shoot_timeout <= 0.0: 38 | game_instance.rpc_id(1, "cl_shoot", aim_direction) 39 | shoot_timeout = SHOOT_TIMEOUT 40 | 41 | client_send_snapshot() 42 | 43 | set_anim("parameters/movement/blend_position", Vector2(1, -1) * direction_input) 44 | set_anim("parameters/movement_time/scale", direction_input.length()) 45 | 46 | move_and_slide(direction_input * SPEED) 47 | 48 | func set_username(username): 49 | self.username = username 50 | $Info.text = str(username) 51 | 52 | func client_send_snapshot(): 53 | var cl_snapshot = ClientSnapshot.new() 54 | cl_snapshot.position = global_position 55 | game_instance.client_send_snapshot(cl_snapshot.to_object()) 56 | 57 | func server_handle_cl_snapshot(client_snapshot): 58 | global_position = client_snapshot.position 59 | 60 | func set_anim(key, value): 61 | if is_network_master(): 62 | if anim_state.has(key): 63 | if anim_state[key] == value: 64 | return 65 | anim_state[key] = value 66 | game_instance.client_object({ 67 | id = get_tree().get_network_unique_id(), 68 | type = "anim", 69 | value = value, 70 | key = key 71 | }) 72 | anim_tree.set(key, value) 73 | 74 | func take_damage(bullet_owner_id, damage): 75 | health -= damage 76 | $FX.play("damage") 77 | game_instance.server_broadcast_object({ 78 | type = "health", 79 | value = health, 80 | id = int(name) 81 | }) 82 | if health <= 0.0: 83 | game_instance.server_broadcast_death(bullet_owner_id, int(name)) 84 | 85 | func die(): 86 | if is_network_master(): 87 | can_move = false 88 | 89 | func set_health(data): 90 | health = data.health 91 | $Health.value = data.health 92 | if data.is_taking_damage: 93 | $FX.play("damage") 94 | -------------------------------------------------------------------------------- /Scripts/Services/AuthService.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends Node 5 | 6 | # We allow the users to reconnect within 30 seconds to not 7 | # lose their authorization status 8 | const DISCONNECT_QUEUE_TIME = 30.0 9 | var disconnect_timer = 0.0 10 | 11 | var Users : Dictionary = {} 12 | var current_user : UserClass 13 | var disconnect_queue = [] 14 | var auto_login_creds = [ 15 | { user = "NovemberDev", password = "asdasdasd" }, 16 | { user = "TestUser", password = "asdasdasd"} 17 | ] 18 | 19 | signal client_on_authorized 20 | signal server_on_client_login 21 | signal server_on_client_logout 22 | 23 | # Since mobile networks disconnect and reconnect rapidly, 24 | # we still keep the user profile loaded for a short period of time 25 | # until the user gets removed from the users dictionary. 26 | # If the user reconnects, a removal won't happen. 27 | func _ready(): 28 | NetworkManager.connect("server_client_disconnected", self, "queue_for_disconnect") 29 | NetworkManager.connect("server_client_connected", self, "dequeue_from_disconnect") 30 | 31 | if !NetworkManager.is_server: 32 | try_auto_login() 33 | 34 | func _process(delta): 35 | disconnect_timer -= delta 36 | if disconnect_queue.size() > 0 and disconnect_timer <= 0.0: 37 | disconnect_timer = DISCONNECT_QUEUE_TIME 38 | var user = disconnect_queue.pop_back() 39 | emit_signal("server_on_client_logout", user) 40 | user.save_state() 41 | Users.erase(user.id) 42 | 43 | # Server ------- 44 | remote func cl_authenticate(data): 45 | var user = UserClass.new(data) 46 | if user.server_initialize_user(data.type == "register"): 47 | dequeue_from_disconnect(user.id) 48 | user.id = NetworkManager.caller() 49 | Users[NetworkManager.caller()] = user 50 | rpc_id(NetworkManager.caller(), "srv_authenticate", true, "Welcome " + user.name + "!", { 51 | name = data.username, 52 | token = user.token 53 | }) 54 | emit_signal("server_on_client_login", user) 55 | else: 56 | rpc_id(NetworkManager.caller(), "srv_authenticate", false, "Invalid user", null) 57 | 58 | # Enqueue for complete disconnect including the removal from the 59 | # Users dictionary until the next login / connection happens 60 | func queue_for_disconnect(id): 61 | # User might not be authorized, but disconnects 62 | if Users.has(int(id)): 63 | disconnect_queue.push_back(Users[int(id)]) 64 | 65 | # Reconnect happened before cleanup timer elapsed 66 | # remove user from disconnect_queue 67 | func dequeue_from_disconnect(id): 68 | var index = disconnect_queue.find(int(id)) 69 | if index != -1: 70 | disconnect_queue.remove(index) 71 | return true 72 | return false 73 | 74 | func get_current_user(): 75 | return Users[NetworkManager.caller()] 76 | 77 | # Client ------- 78 | # If the client has a token stored locally, we attempt 79 | # to use it for login behind the scenes 80 | func try_auto_login(): 81 | var token_data = Tools.open_json_file("user://token.json") 82 | if token_data != null: 83 | self.token = token_data.token 84 | if self.token != null: 85 | AuthService.rpc_id(1, "cl_authenticate", { 86 | token = self.token, 87 | type = "login" 88 | }) 89 | 90 | func client_authorize(username, password, type): 91 | AuthService.rpc_id(1, "cl_authenticate", { 92 | password = password, 93 | username = username, 94 | type = type 95 | }) 96 | 97 | puppet func srv_authenticate(result, message, user): 98 | Notifications.notify(message) 99 | 100 | if result: 101 | var new_user = UserClass.new(user) 102 | self.current_user = new_user 103 | emit_signal("client_on_authorized") 104 | 105 | -------------------------------------------------------------------------------- /Scripts/Services/LobbyService.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends ViewportContainer 5 | 6 | var waiting_queue = [] 7 | var lobbies : Dictionary = {} 8 | var current_lobby : LobbyClass 9 | 10 | signal client_on_gameover 11 | signal client_on_lobby_start 12 | signal server_on_lobby_created 13 | signal server_on_game_instance_created 14 | 15 | func _ready(): 16 | set_network_master(1) 17 | rect_size = OS.window_size 18 | 19 | # Client ------ 20 | func client_play(): 21 | rpc_id(1, "cl_quick_play") 22 | 23 | puppet func srv_create_game_instance(game_instance_properties): 24 | if current_lobby != null: 25 | if is_instance_valid(current_lobby): 26 | current_lobby.queue_free() 27 | current_lobby = LobbyClass.new(game_instance_properties.lobby_name) 28 | LobbyService.add_child(current_lobby) 29 | current_lobby.create_game_instance(game_instance_properties) 30 | 31 | # Server ------ 32 | remote func cl_quick_play(): 33 | waiting_queue.push_back(AuthService.get_current_user()) 34 | 35 | func _process(delta): 36 | if waiting_queue.size() >= 2: 37 | on_match_found() 38 | 39 | func on_match_found(): 40 | # create lobby 41 | var new_lobby = LobbyClass.new(UUID.NewID()) 42 | lobbies[new_lobby.name] = new_lobby 43 | var user1 = waiting_queue.pop_front() 44 | new_lobby.users[user1.id] = user1 45 | var user2 = waiting_queue.pop_front() 46 | new_lobby.users[user2.id] = user2 47 | emit_signal("server_on_lobby_created", new_lobby) 48 | 49 | # in this case we launch the game instantly, 50 | # we don't show a lobby with a list of players, 51 | # options or other fancy things 52 | var game_instance_properties = { 53 | name = UUID.NewID(), 54 | lobby_name = new_lobby.name 55 | } 56 | new_lobby.create_game_instance(game_instance_properties) 57 | rpc_id(int(user1.id), "srv_create_game_instance", game_instance_properties) 58 | rpc_id(int(user2.id), "srv_create_game_instance", game_instance_properties) 59 | emit_signal("server_on_game_instance_created", new_lobby) 60 | -------------------------------------------------------------------------------- /Scripts/Types/BaseClass.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends Node 5 | class_name BaseClass 6 | 7 | var id : String 8 | 9 | func _init(name = null): 10 | self.id = UUID.NewID() 11 | 12 | if name == null: 13 | self.name = str(self.id) 14 | else: 15 | self.name = name 16 | -------------------------------------------------------------------------------- /Scripts/Types/ClientSnapshot.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends Node 5 | class_name ClientSnapshot 6 | 7 | # This class can be 8 | # extended by more properties 9 | # that will be set inside the 10 | # Player script 11 | var position : Vector2 12 | 13 | # This method will get all properties 14 | # we want to send to the server 15 | func to_object(): 16 | return { 17 | position = position 18 | } 19 | -------------------------------------------------------------------------------- /Scripts/Types/LobbyClass.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends BaseClass 5 | class_name LobbyClass 6 | 7 | var current_game_instance 8 | var users : Dictionary = {} 9 | var game_instance_scene = load("res://Scenes/GameInstance.tscn") 10 | var game_instance_scene_server = load("res://Scenes/GameInstanceServer.tscn") 11 | 12 | func _init(name : String): 13 | ._init(name) 14 | pass 15 | 16 | func create_game_instance(game_instance_properties): 17 | if NetworkManager.is_server: 18 | current_game_instance = game_instance_scene_server.instance() 19 | else: 20 | current_game_instance = game_instance_scene.instance() 21 | current_game_instance.name = game_instance_properties.name 22 | current_game_instance.current_lobby = self 23 | LobbyService.add_child(current_game_instance) 24 | # Set per-game-instance properties (like map or game mode) 25 | # should be passed from the Lobby, a complex lobby 26 | # system can be implemented 27 | LobbyService.emit_signal("client_on_lobby_start") 28 | -------------------------------------------------------------------------------- /Scripts/Types/ServerSnapshot.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name ServerSnapshot 3 | 4 | var position : Vector2 5 | -------------------------------------------------------------------------------- /Scripts/Types/UserClass.gd: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | extends BaseClass 5 | class_name UserClass 6 | 7 | var token 8 | var password 9 | var loaded : bool = false 10 | 11 | # Extra properties 12 | var score = 0 13 | 14 | func _init(data): 15 | ._init() 16 | 17 | self.id = str(NetworkManager.caller()) 18 | self.password = Tools.get_dict_val(data, "password") 19 | var n = Tools.get_dict_val(data, "username") 20 | if n != null: 21 | self.name = n 22 | self.token = Tools.get_dict_val(data, "token") 23 | 24 | # Server ----- 25 | func server_initialize_user(is_register): 26 | var directory = Directory.new() 27 | directory.open("user://") 28 | directory.make_dir("users") 29 | print(self.name) 30 | # if the user supplied a token 31 | if token != null: 32 | # we open a file that is named by the token and extract the username 33 | # for further processing 34 | var user_info = Tools.open_json_file("user://users/" + self.token.to_upper() + ".json") 35 | if user_info != null: 36 | self.name = user_info.name 37 | if directory.file_exists("user://users/" + self.name.to_upper() + ".json"): 38 | if is_register: return false 39 | var this_user = Tools.open_json_file("user://users/" + self.name.to_upper() + ".json") 40 | if this_user == null: 41 | return false 42 | if this_user != null: 43 | if self.password.sha256_text() == this_user.password: 44 | loaded = true 45 | return true 46 | else: 47 | return false 48 | elif is_register: 49 | var file = File.new() 50 | self.token = UUID.NewID().sha256_text() 51 | if file.open("user://users/" + self.token.to_upper() + ".json", file.WRITE) == 0: 52 | file.store_string(JSON.print({ 53 | name = self.name.to_upper() 54 | })) 55 | file.close() 56 | if file.open("user://users/" + self.name.to_upper() + ".json", file.WRITE) == 0: 57 | file.store_string(JSON.print({ 58 | username = self.name.to_upper(), 59 | password = self.password.sha256_text() 60 | })) 61 | file.close() 62 | loaded = true 63 | return true 64 | return false 65 | 66 | func save_state(): 67 | pass 68 | -------------------------------------------------------------------------------- /build_and_debug.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # Author: @November_Dev 3 | # 4 | $godotPath = "C:\Projects\Godot.exe" 5 | $output = "C:\Projects\GD_BUILDS\02_MULTIPLAYER.exe" 6 | 7 | # --no-window is bugged 8 | cmd.exe /c $godotPath --no-window --export-debug default_export $output 9 | 10 | Start-Process "cmd.exe" -ArgumentList "/c $output --server" -PassThru 11 | 12 | for ($i = 0; $i -lt $args[0]; $i++) { 13 | Start-Process "cmd.exe" -ArgumentList "/c $output --userindex $i" -PassThru 14 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NovemberDev/novemberdev_multiplayer_godot/a10d9c4ca8fbbadd9dfcd6253c464994f176f93b/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 | -------------------------------------------------------------------------------- /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 | "base": "Node", 13 | "class": "BaseClass", 14 | "language": "GDScript", 15 | "path": "res://Scripts/Types/BaseClass.gd" 16 | }, { 17 | "base": "Node", 18 | "class": "ClientSnapshot", 19 | "language": "GDScript", 20 | "path": "res://Scripts/Types/ClientSnapshot.gd" 21 | }, { 22 | "base": "BaseClass", 23 | "class": "LobbyClass", 24 | "language": "GDScript", 25 | "path": "res://Scripts/Types/LobbyClass.gd" 26 | }, { 27 | "base": "Node", 28 | "class": "ServerSnapshot", 29 | "language": "GDScript", 30 | "path": "res://Scripts/Types/ServerSnapshot.gd" 31 | }, { 32 | "base": "BaseClass", 33 | "class": "UserClass", 34 | "language": "GDScript", 35 | "path": "res://Scripts/Types/UserClass.gd" 36 | } ] 37 | _global_script_class_icons={ 38 | "BaseClass": "", 39 | "ClientSnapshot": "", 40 | "LobbyClass": "", 41 | "ServerSnapshot": "", 42 | "UserClass": "" 43 | } 44 | 45 | [application] 46 | 47 | config/name="02_MULTIPLAYER" 48 | run/main_scene="res://Scenes/Main_Menu.tscn" 49 | config/icon="res://icon.png" 50 | 51 | [autoload] 52 | 53 | Tools="*res://Scripts/Autoloads/Tools.gd" 54 | UUID="*res://Scripts/Autoloads/UUID.gd" 55 | Notifications="*res://Scenes/Notifications.tscn" 56 | NetworkManager="*res://Scripts/Autoloads/NetworkManager.gd" 57 | AuthService="*res://Scripts/Services/AuthService.gd" 58 | LobbyService="*res://Scripts/Services/LobbyService.gd" 59 | 60 | [display] 61 | 62 | window/size/width=800 63 | window/stretch/mode="2d" 64 | window/stretch/aspect="keep" 65 | 66 | [rendering] 67 | 68 | environment/default_clear_color=Color( 0, 0, 0, 1 ) 69 | environment/default_environment="res://default_env.tres" 70 | --------------------------------------------------------------------------------