├── .gitignore ├── README.md ├── default_env.tres ├── icon.png ├── icon.png.import ├── network_sync.gd ├── networked_controller.gd ├── player.gd ├── player.tscn ├── player_camera.gd ├── player_camera.tscn ├── project.godot ├── sync_mesh.gd ├── sync_mesh.tscn ├── world.gd └── world.tscn /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor cache 2 | .godot 3 | .import 4 | /logs/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Packet loss add/change/remove localhost 2 | sudo tc qdisc add dev lo root netem loss 10.0% 3 | sudo tc qdisc change dev lo root netem loss 10.0% 4 | sudo tc qdisc del dev lo root netem loss 5 | 6 | Packet delay add/change/remove localhost 7 | sudo tc qdisc add dev lo root netem delay 250ms 8 | sudo tc qdisc change dev lo root netem delay 500ms 9 | sudo tc qdisc del dev lo root netem delay 10 | -------------------------------------------------------------------------------- /default_env.tres: -------------------------------------------------------------------------------- 1 | [gd_resource type="Environment" load_steps=2 format=2] 2 | 3 | [sub_resource type="Sky" id=1] 4 | 5 | [resource] 6 | background_mode = 2 7 | sky = SubResource( 1 ) 8 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GameNetworking/example-project/27363c6cd9c7f311673b63502a052d8108d32395/icon.png -------------------------------------------------------------------------------- /icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture2D" 5 | uid="uid://ckp0xxbsuksqe" 6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.png" 14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.stex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/lossy_quality=0.7 20 | compress/hdr_compression=1 21 | compress/bptc_ldr=0 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | compress/streamed=false 25 | mipmaps/generate=false 26 | mipmaps/limit=-1 27 | roughness/mode=0 28 | roughness/src_normal="" 29 | process/fix_alpha_border=true 30 | process/premult_alpha=false 31 | process/normal_map_invert_y=false 32 | process/HDR_as_SRGB=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /network_sync.gd: -------------------------------------------------------------------------------- 1 | extends SceneSynchronizer 2 | 3 | -------------------------------------------------------------------------------- /networked_controller.gd: -------------------------------------------------------------------------------- 1 | extends NetworkedController 2 | # Take cares to control the player and propagate the motion on the other peers 3 | 4 | 5 | const MAX_PLAYER_DISTANCE: float = 20.0 6 | 7 | var _position_id := -1 8 | var _rotation_id := -1 9 | 10 | 11 | func _ready(): 12 | # Notify the NetworkSync who is controlling parent nodes. 13 | NetworkSync.set_node_as_controlled_by(get_parent(), self) 14 | NetworkSync.register_variable(get_parent(), "translation") 15 | NetworkSync.register_variable(get_parent(), "velocity") 16 | NetworkSync.register_variable(get_parent(), "on_floor") 17 | if not get_tree().get_multiplayer().is_server(): 18 | set_physics_process(false) 19 | 20 | 21 | func _physics_process(_delta): 22 | """ 23 | for character in get_tree().get_nodes_in_group("characters"): 24 | if character != get_parent(): 25 | var delta_distance = character.get_global_transform().origin - get_parent().get_global_transform().origin 26 | 27 | var is_far_away = delta_distance.length_squared() > (MAX_PLAYER_DISTANCE * MAX_PLAYER_DISTANCE) 28 | set_doll_peer_active(character.get_network_master(), !is_far_away); 29 | """ 30 | pass 31 | 32 | 33 | func _collect_inputs(_delta: float, db: DataBuffer): 34 | # Collects the player inputs. 35 | 36 | var input_direction := Vector3() 37 | var is_jumping: bool = false 38 | 39 | if Input.is_action_pressed("forward"): 40 | input_direction -= get_parent().camera.global_transform.basis.z 41 | if Input.is_action_pressed("backward"): 42 | input_direction += get_parent().camera.global_transform.basis.z 43 | if Input.is_action_pressed("left"): 44 | input_direction -= get_parent().camera.global_transform.basis.x 45 | if Input.is_action_pressed("right"): 46 | input_direction += get_parent().camera.global_transform.basis.x 47 | if Input.is_action_pressed("jump"): 48 | is_jumping = true 49 | input_direction.y = 0 50 | input_direction = input_direction.normalized() 51 | 52 | db.add_bool(is_jumping) 53 | 54 | var has_input: bool = input_direction.length_squared() > 0.0 55 | db.add_bool(has_input) 56 | if has_input: 57 | db.add_normalized_vector2(Vector2(input_direction.x, input_direction.z), DataBuffer.COMPRESSION_LEVEL_3) 58 | 59 | 60 | func _controller_process(delta: float, db: DataBuffer): 61 | # Process the controller. 62 | 63 | # Take the inputs 64 | var is_jumping = db.read_bool() 65 | var input_direction := Vector2() 66 | 67 | var has_input = db.read_bool() 68 | if has_input: 69 | input_direction = db.read_normalized_vector2(DataBuffer.COMPRESSION_LEVEL_3) 70 | 71 | # Process the character 72 | get_parent().step_body(delta, Vector3(input_direction.x, 0.0, input_direction.y), is_jumping) 73 | 74 | 75 | func _count_input_size(inputs: DataBuffer) -> int: 76 | # Count the input buffer size. 77 | var size: int = 0 78 | size += inputs.get_bool_size() 79 | inputs.skip_bool() 80 | size += inputs.get_bool_size() 81 | if inputs.read_bool(): 82 | size += inputs.get_normalized_vector2_size(DataBuffer.COMPRESSION_LEVEL_3) 83 | 84 | return size 85 | 86 | 87 | func _are_inputs_different(inputs_A: DataBuffer, inputs_B: DataBuffer) -> bool: 88 | # Compare two inputs, returns true when those are different or false when are close enough. 89 | if inputs_A.read_bool() != inputs_B.read_bool(): 90 | return true 91 | 92 | var inp_A_has_i = inputs_A.read_bool() 93 | var inp_B_has_i = inputs_B.read_bool() 94 | if inp_A_has_i != inp_B_has_i: 95 | return true 96 | 97 | if inp_A_has_i: 98 | var inp_A_dir = inputs_A.read_normalized_vector2(DataBuffer.COMPRESSION_LEVEL_3) 99 | var inp_B_dir = inputs_B.read_normalized_vector2(DataBuffer.COMPRESSION_LEVEL_3) 100 | if (inp_A_dir - inp_B_dir).length_squared() > 0.0001: 101 | return true 102 | 103 | return false 104 | 105 | 106 | func _collect_epoch_data(buffer: DataBuffer): 107 | # Called on server when the collect state is triggered. 108 | # The collected `DataBuffer` is sent to the client that parse it using the 109 | # function `parse_epoch_data` and puts the data into the interpolator. 110 | # Later the function `apply_epoch` is called to apply the epoch 111 | # (already interpolated) data. 112 | 113 | # TODO The compression level for the character position should be scaled depending on how close 114 | # the character is to the local character: It should be 0 when the characters are near each other, 115 | # to avoid that a collision cause de-sync due to the sliglty different position introduced by the 116 | # high compression level. 117 | buffer.add_vector3(get_parent().global_transform.origin, DataBuffer.COMPRESSION_LEVEL_0) 118 | buffer.add_vector3(get_parent().mesh_container.rotation, DataBuffer.COMPRESSION_LEVEL_2) 119 | 120 | 121 | func _setup_interpolator(interpolator: Interpolator): 122 | # Called only on client doll to initialize the `Intepolator`. 123 | _position_id = interpolator.register_variable(Vector3(), Interpolator.FALLBACK_NEW_OR_NEAREST) 124 | _rotation_id = interpolator.register_variable(Vector3(), Interpolator.FALLBACK_NEW_OR_NEAREST) 125 | 126 | 127 | func _parse_epoch_data(interpolator: Interpolator, buffer: DataBuffer): 128 | # Called locally to parse the `DataBuffer` and store the data into the `Interpolator`. 129 | var position := buffer.read_vector3(DataBuffer.COMPRESSION_LEVEL_0) 130 | var rotation := buffer.read_vector3(DataBuffer.COMPRESSION_LEVEL_2) 131 | interpolator.epoch_insert(_position_id, position) 132 | interpolator.epoch_insert(_rotation_id, rotation) 133 | 134 | 135 | func _apply_epoch(_delta: float, interpolated_data: Array): 136 | # Happens only on doll client each frame. Here is necessary to apply the _already interpolated_ values. 137 | get_parent().global_transform.origin = interpolated_data[_position_id] 138 | get_parent().mesh_container.rotation = interpolated_data[_rotation_id] 139 | -------------------------------------------------------------------------------- /player.gd: -------------------------------------------------------------------------------- 1 | extends CharacterBody3D 2 | 3 | 4 | const MOVE_SPEED: int = 10 5 | const MOTION_INTERPOLATE_SPEED: int = 20 6 | const VELOCITY_INTERPOLATE_SPEED: int = 2 7 | const GRAVITY: int = 10 8 | const JUMP_IMPULSE: float = 4.0 9 | 10 | @onready var camera: PlayerCamera = $Camera 11 | 12 | var on_floor: bool = false 13 | 14 | @onready var mesh_container: Node3D = $MeshContainer 15 | @onready var _mesh: MeshInstance3D = $MeshContainer/Mesh 16 | 17 | 18 | func _init(): 19 | floor_stop_on_slope = true 20 | 21 | 22 | func set_color(color): 23 | var mat = _mesh.get_surface_override_material(0) 24 | if mat == null: 25 | mat = StandardMaterial3D.new() 26 | else: 27 | mat = mat.duplicate() 28 | mat.set_albedo(Color(color)) 29 | _mesh.set_surface_override_material(0, mat) 30 | 31 | 32 | # Computes one motion step. 33 | func step_body(delta: float, input_direction: Vector3, is_jumping: bool): 34 | _set_player_orientation(input_direction) 35 | 36 | var motion: Vector3 = input_direction * MOVE_SPEED 37 | var new_velocity: Vector3 38 | if on_floor and velocity.length() < MOVE_SPEED: 39 | new_velocity = velocity.lerp(motion, MOTION_INTERPOLATE_SPEED * delta) 40 | if is_jumping: 41 | new_velocity.y = new_velocity.y + JUMP_IMPULSE 42 | else: 43 | new_velocity = velocity.lerp(motion, VELOCITY_INTERPOLATE_SPEED * delta) 44 | new_velocity.y = new_velocity.y - GRAVITY * delta 45 | 46 | velocity = new_velocity 47 | move_and_slide() 48 | on_floor = is_on_floor() # Store in a variable to sync over network 49 | 50 | 51 | func _set_player_orientation(input_direction): 52 | if input_direction.length_squared() < 0.01: 53 | return 54 | mesh_container.transform = mesh_container.transform.looking_at(input_direction, Vector3(0, 1, 0)) 55 | -------------------------------------------------------------------------------- /player.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://bani3d0rw7ru5"] 2 | 3 | [ext_resource type="Script" path="res://player.gd" id="1"] 4 | [ext_resource type="Script" path="res://networked_controller.gd" id="2"] 5 | [ext_resource type="PackedScene" path="res://player_camera.tscn" id="3"] 6 | 7 | [sub_resource type="CapsuleShape3D" id="1"] 8 | radius = 0.2 9 | height = 1.6 10 | 11 | [sub_resource type="CapsuleMesh" id="2"] 12 | radius = 0.2 13 | radial_segments = 7 14 | 15 | [sub_resource type="CylinderMesh" id="3"] 16 | top_radius = 0.15 17 | bottom_radius = 0.15 18 | height = 0.2 19 | radial_segments = 18 20 | rings = 1 21 | 22 | [node name="Player" type="CharacterBody3D" groups=["characters"]] 23 | script = ExtResource( "1" ) 24 | 25 | [node name="NetworkedController" type="NetworkedController" parent="."] 26 | script = ExtResource( "2" ) 27 | 28 | [node name="Shape" type="CollisionShape3D" parent="."] 29 | shape = SubResource( "1" ) 30 | 31 | [node name="Camera" parent="." instance=ExtResource( "3" )] 32 | 33 | [node name="MeshContainer" type="Node3D" parent="."] 34 | 35 | [node name="Mesh" type="MeshInstance3D" parent="MeshContainer"] 36 | mesh = SubResource( "2" ) 37 | 38 | [node name="MeshInstance" type="MeshInstance3D" parent="MeshContainer/Mesh"] 39 | transform = Transform3D(1, 0, 0, 0, -1.62921e-07, -1, 0, 1, -1.62921e-07, 0, 0.658166, -0.147896) 40 | mesh = SubResource( "3" ) 41 | 42 | [node name="SpotLight" type="SpotLight3D" parent="MeshContainer/Mesh/MeshInstance"] 43 | transform = Transform3D(1, 0, 0, 0, -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, -0.192691, 5.96046e-08) 44 | light_color = Color(0.988235, 0.972549, 0.00784314, 1) 45 | light_energy = 14.23 46 | spot_attenuation = 1.56917 47 | spot_angle = 73.2909 48 | spot_angle_attenuation = 19.0273 49 | -------------------------------------------------------------------------------- /player_camera.gd: -------------------------------------------------------------------------------- 1 | class_name PlayerCamera 2 | extends SpringArm3D 3 | 4 | 5 | const MOUSE_ROTATION_SPEED := 0.002 6 | const ZOOM_SPEED := 0.5 7 | 8 | @onready var _camera: Camera3D = $Camera 9 | 10 | var _captured: bool 11 | 12 | 13 | func _ready() -> void: 14 | if is_multiplayer_authority(): 15 | _camera.current = true 16 | else: 17 | set_process_input(false) 18 | 19 | 20 | func _input(event: InputEvent) -> void: 21 | if event.is_action_pressed("ui_cursor"): 22 | if _captured: 23 | Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) 24 | else: 25 | Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) 26 | _captured = !_captured 27 | elif event.is_action_pressed("zoom_in"): 28 | spring_length += ZOOM_SPEED 29 | elif event.is_action_pressed("zoom_out"): 30 | spring_length -= ZOOM_SPEED 31 | elif _captured and event is InputEventMouseMotion: 32 | rotation.y -= event.relative.x * MOUSE_ROTATION_SPEED 33 | rotation.x -= event.relative.y * MOUSE_ROTATION_SPEED 34 | rotation.x = clamp(rotation.x, -PI / 2, 0.7) 35 | -------------------------------------------------------------------------------- /player_camera.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://player_camera.gd" type="Script" id=1] 4 | 5 | [node name="PlayerCamera" type="SpringArm3D"] 6 | transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0 ) 7 | spring_length = 5.0 8 | margin = 0.5 9 | script = ExtResource( 1 ) 10 | 11 | [node name="Camera" type="Camera3D" parent="."] 12 | h_offset = 1.0 13 | script = null 14 | -------------------------------------------------------------------------------- /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=5 10 | 11 | _global_script_classes=[{ 12 | "base": "SpringArm3D", 13 | "class": &"PlayerCamera", 14 | "language": &"GDScript", 15 | "path": "res://player_camera.gd" 16 | }] 17 | _global_script_class_icons={ 18 | "PlayerCamera": "" 19 | } 20 | 21 | [application] 22 | 23 | config/name="Net" 24 | run/main_scene="res://world.tscn" 25 | config/icon="res://icon.png" 26 | config/features=PackedStringArray("4.0") 27 | 28 | [autoload] 29 | 30 | NetworkSync="*res://network_sync.gd" 31 | 32 | [display] 33 | 34 | window/vsync/vsync_mode=0 35 | 36 | [editor] 37 | 38 | main_run_args="--server" 39 | 40 | [input] 41 | 42 | forward={ 43 | "deadzone": 0.5, 44 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 45 | ] 46 | } 47 | backward={ 48 | "deadzone": 0.5, 49 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 50 | ] 51 | } 52 | left={ 53 | "deadzone": 0.5, 54 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 55 | ] 56 | } 57 | right={ 58 | "deadzone": 0.5, 59 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 60 | ] 61 | } 62 | ui_cursor={ 63 | "deadzone": 0.5, 64 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":16777240,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 65 | ] 66 | } 67 | zoom_in={ 68 | "deadzone": 0.5, 69 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"pressed":false,"double_click":false,"script":null) 70 | ] 71 | } 72 | zoom_out={ 73 | "deadzone": 0.5, 74 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"pressed":false,"double_click":false,"script":null) 75 | ] 76 | } 77 | jump={ 78 | "deadzone": 0.5, 79 | "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"store_command":true,"alt_pressed":false,"shift_pressed":false,"meta_pressed":false,"command_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"unicode":0,"echo":false,"script":null) 80 | ] 81 | } 82 | 83 | [layer_names] 84 | 85 | 3d_physics/layer_1="Common" 86 | 3d_physics/layer_2="Player" 87 | 88 | [network] 89 | 90 | limits/packet_peer_stream/max_buffer_po2=10000 91 | limits/websocket_client/max_in_buffer_kb=10024 92 | limits/websocket_client/max_out_buffer_kb=10024 93 | limits/websocket_server/max_in_buffer_kb=10024 94 | limits/websocket_server/max_out_buffer_kb=10024 95 | 96 | [physics] 97 | 98 | common/physics_jitter_fix=0.0 99 | 3d/active_soft_world=false 100 | 101 | [rendering] 102 | 103 | environment/default_environment="res://default_env.tres" 104 | -------------------------------------------------------------------------------- /sync_mesh.gd: -------------------------------------------------------------------------------- 1 | extends MeshInstance3D 2 | # Move the box in sync with all peers 3 | 4 | 5 | var seek: float: 6 | set = set_seek, 7 | get = get_seek 8 | 9 | 10 | @onready var _player: AnimationPlayer = $AnimationPlayer 11 | 12 | 13 | func _ready(): 14 | NetworkSync.register_process(self, "_sync_process") 15 | NetworkSync.register_variable(self, "seek", "_on_seek_changed", NetworkSync.SYNC_RESET) 16 | 17 | # Manual process so we can process the animations in sync. 18 | _player.playback_process_mode = AnimationPlayer.ANIMATION_PROCESS_MANUAL 19 | 20 | 21 | func get_seek() -> float: 22 | return _player.get_current_animation_position() 23 | 24 | 25 | func set_seek(s): 26 | _player.seek(s) 27 | 28 | 29 | func _sync_process(delta: float): 30 | _player.advance(delta) 31 | 32 | 33 | func _on_seek_changed(): 34 | ## This function is called only when a reset occurs thanks to: NetworkSync.SYNC_RESET 35 | ## In alternative you can use this function instead 36 | ## ``` 37 | ## if NetworkSync.is_resetted(): 38 | ## _player.seek(seek) 39 | ## ``` 40 | 41 | _player.seek(seek) 42 | -------------------------------------------------------------------------------- /sync_mesh.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=5 format=3 uid="uid://usykowfpgf66"] 2 | 3 | [ext_resource type="Script" path="res://sync_mesh.gd" id="1"] 4 | 5 | [sub_resource type="BoxMesh" id="1"] 6 | 7 | [sub_resource type="Animation" id="2"] 8 | resource_name = "New Anim" 9 | length = 5.0 10 | loop_mode = 1 11 | tracks/0/type = "value" 12 | tracks/0/imported = false 13 | tracks/0/enabled = true 14 | tracks/0/path = NodePath(".:position") 15 | tracks/0/interp = 1 16 | tracks/0/loop_wrap = true 17 | tracks/0/keys = { 18 | "times": PackedFloat32Array(0, 2.5), 19 | "transitions": PackedFloat32Array(1, 1), 20 | "update": 0, 21 | "values": [Vector3(0, 0, 0), Vector3(0, 0, 7.33118)] 22 | } 23 | 24 | [sub_resource type="AnimationLibrary" id="AnimationLibrary_n5m35"] 25 | _data = { 26 | "New Anim": SubResource( "2" ) 27 | } 28 | 29 | [node name="SyncMesh" type="Node3D"] 30 | 31 | [node name="SyncMesh" type="MeshInstance3D" parent="."] 32 | mesh = SubResource( "1" ) 33 | script = ExtResource( "1" ) 34 | 35 | [node name="AnimationPlayer" type="AnimationPlayer" parent="SyncMesh"] 36 | autoplay = "New Anim" 37 | playback_process_mode = 2 38 | libraries = { 39 | "": SubResource( "AnimationLibrary_n5m35" ) 40 | } 41 | -------------------------------------------------------------------------------- /world.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const SERVER_PORT = 2000 4 | const MAX_PLAYERS = 5 5 | const SERVER_IP = "127.0.0.1" 6 | const COLORS_LIST = [ 7 | "#0ad609", 8 | "#f0bf58", 9 | "#1120d3", 10 | "#704c28", 11 | "#2af4ce" 12 | ] 13 | 14 | @onready var _menu: Control = $Menu 15 | 16 | var _player_id_counter = 0 17 | var _players = {} 18 | 19 | 20 | func _start_server(): 21 | _menu.hide() 22 | 23 | var net := ENetMultiplayerPeer.new() 24 | net.create_server(SERVER_PORT, MAX_PLAYERS) 25 | get_tree().get_multiplayer().connect("peer_connected", Callable(self, "_on_client_connected")) 26 | get_tree().get_multiplayer().connect("peer_disconnected", Callable(self, "_on_client_disconnected")) 27 | get_tree().get_multiplayer().set_multiplayer_peer(net) 28 | print("Server IP: ", IP.get_local_addresses()) 29 | 30 | 31 | func _start_client(): 32 | _menu.hide() 33 | 34 | var net := ENetMultiplayerPeer.new() 35 | net.create_client(SERVER_IP, SERVER_PORT) 36 | get_tree().get_multiplayer().set_multiplayer_peer(net) 37 | 38 | 39 | func _on_client_connected(peer_id): 40 | print("Connected: ", peer_id) 41 | var new_player_id = _player_id_counter 42 | _player_id_counter += 1 43 | _players[new_player_id] = {&"peer_id": peer_id, &"player_id": new_player_id} 44 | 45 | # Spawn player on server, for server any player is puppet (even if it's 46 | # autoritative) 47 | _spawn_new_player(new_player_id, peer_id) 48 | 49 | # Spawn the player on the client 50 | rpc_id(peer_id, &"_spawn_new_player", new_player_id, peer_id) 51 | 52 | # Tell anyone new player appeared 53 | for player_id in _players.keys(): 54 | if _players[player_id][&"peer_id"] != peer_id: 55 | rpc_id(_players[player_id][&"peer_id"], &"_spawn_new_player", new_player_id, 1) 56 | 57 | # Spawn the actual _players on this client 58 | for player_id in _players.keys(): 59 | if player_id != new_player_id: 60 | rpc_id(peer_id, &"_spawn_new_player", player_id, 1) 61 | 62 | 63 | func _on_client_disconnected(peer_id): 64 | print("Disconnected: ", peer_id) 65 | 66 | var disconnected_player_id = -1 67 | for player_id in _players.keys(): 68 | if _players[player_id][&"peer_id"] == peer_id: 69 | disconnected_player_id = player_id 70 | break 71 | 72 | if disconnected_player_id == -1: 73 | return 74 | 75 | _players.erase(disconnected_player_id) 76 | _remove_player(disconnected_player_id) 77 | 78 | # Tell anyone player disappeared 79 | for player_id in _players.keys(): 80 | rpc_id(_players[player_id][&"peer_id"], &"_remove_player", disconnected_player_id) 81 | 82 | 83 | @rpc(call_remote, any_peer, reliable) 84 | func _spawn_new_player(player_id, peer_id): 85 | print("Spawn player id: ", player_id, ", Peer_id: ", peer_id) 86 | print("While my peer id is: ", get_tree().get_multiplayer().multiplayer_peer.get_unique_id()) 87 | 88 | var player = load("res://player.tscn").instantiate() 89 | player.set_multiplayer_authority(peer_id) 90 | player.set_name("player_" + str(player_id)) 91 | get_tree().get_current_scene().add_child(player) 92 | player.set_color(COLORS_LIST[player_id]) 93 | 94 | 95 | @rpc(call_remote, any_peer, reliable) 96 | func _remove_player(player_id): 97 | var player_node = get_tree().get_current_scene().get_node("player_" + str(player_id)) 98 | if player_node != null: 99 | player_node.queue_free() 100 | -------------------------------------------------------------------------------- /world.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://dnw56nw0m0c31"] 2 | 3 | [ext_resource type="Script" path="res://world.gd" id="2"] 4 | [ext_resource type="PackedScene" uid="uid://usykowfpgf66" path="res://sync_mesh.tscn" id="3"] 5 | 6 | [sub_resource type="BoxShape3D" id="1"] 7 | 8 | [sub_resource type="BoxMesh" id="2"] 9 | 10 | [sub_resource type="ConcavePolygonShape3D" id="3"] 11 | data = PackedVector3Array(-1, 1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1, 1, -1, 1, -1, -1, 1, 1, 1, -1, -1, 1, -1, 1, -1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1, 1, 1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1, 1, 1, -1, 1, 1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, 1, -1, 1, 1, -1, -1, -1, -1, -1) 12 | 13 | [sub_resource type="Environment" id="4"] 14 | background_mode = 1 15 | background_color = Color(0.0705882, 0.827451, 1, 1) 16 | sdfgi_use_occlusion = true 17 | sdfgi_bounce_feedback = 0.3 18 | sdfgi_cascades = 2 19 | sdfgi_min_cell_size = 12.8 20 | sdfgi_cascade0_distance = 819.2 21 | sdfgi_max_distance = 3276.8 22 | sdfgi_energy = 7.429 23 | glow_blend_mode = 0 24 | fog_light_color = Color(0.701961, 0.2, 0.333333, 1) 25 | volumetric_fog_density = 0.1293 26 | 27 | [sub_resource type="BoxMesh" id="5"] 28 | 29 | [sub_resource type="BoxShape3D" id="6"] 30 | 31 | [node name="World" type="Node"] 32 | script = ExtResource( "2" ) 33 | 34 | [node name="DirectionalLight" type="DirectionalLight3D" parent="."] 35 | transform = Transform3D(-0.0134716, -0.995255, 0.0963678, -0.13779, 0.0973049, 0.98567, -0.99037, 3.38571e-08, -0.138447, 0, -5.26483, 0) 36 | shadow_enabled = true 37 | 38 | [node name="StaticBody" type="StaticBody3D" parent="."] 39 | transform = Transform3D(50, 0, 0, 0, 0.999999, 0, 0, 0, 50, 0, -1, 0) 40 | collision_mask = 3 41 | 42 | [node name="CollisionShape" type="CollisionShape3D" parent="StaticBody"] 43 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0) 44 | shape = SubResource( "1" ) 45 | disabled = true 46 | 47 | [node name="MeshInstance" type="MeshInstance3D" parent="StaticBody/CollisionShape"] 48 | mesh = SubResource( "2" ) 49 | 50 | [node name="StaticBody" type="StaticBody3D" parent="StaticBody/CollisionShape/MeshInstance"] 51 | 52 | [node name="CollisionShape" type="CollisionShape3D" parent="StaticBody/CollisionShape/MeshInstance/StaticBody"] 53 | shape = SubResource( "3" ) 54 | 55 | [node name="ServerCamera" type="Camera3D" parent="."] 56 | transform = Transform3D(1, 0, 0, 0, 0.866025, 0.5, 0, -0.5, 0.866025, 0, 10.6518, 37.3083) 57 | current = true 58 | 59 | [node name="Menu" type="Control" parent="."] 60 | anchor_right = 1.0 61 | 62 | [node name="VBoxContainer" type="VBoxContainer" parent="Menu"] 63 | anchor_left = 0.5 64 | anchor_right = 0.5 65 | 66 | [node name="ServerButton" type="Button" parent="Menu/VBoxContainer"] 67 | offset_right = 107.0 68 | offset_bottom = 31.0 69 | text = "Start Server" 70 | 71 | [node name="ClientButton" type="Button" parent="Menu/VBoxContainer"] 72 | offset_top = 35.0 73 | offset_right = 107.0 74 | offset_bottom = 66.0 75 | text = "Client Server" 76 | 77 | [node name="SyncMesh" parent="." instance=ExtResource( "3" )] 78 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.12977, 0.11825, 0.354554) 79 | 80 | [node name="SyncMesh2" parent="." instance=ExtResource( "3" )] 81 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6.21811, 0.11825, 0.46023) 82 | 83 | [node name="WorldEnvironment" type="WorldEnvironment" parent="."] 84 | environment = SubResource( "4" ) 85 | 86 | [node name="StaticBody2" type="StaticBody3D" parent="."] 87 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 6) 88 | 89 | [node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody2"] 90 | mesh = SubResource( "5" ) 91 | 92 | [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody2"] 93 | shape = SubResource( "6" ) 94 | 95 | [node name="StaticBody3" type="StaticBody3D" parent="."] 96 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 11) 97 | 98 | [node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3"] 99 | mesh = SubResource( "5" ) 100 | 101 | [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3"] 102 | shape = SubResource( "6" ) 103 | 104 | [node name="StaticBody4" type="StaticBody3D" parent="."] 105 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, 0, 11) 106 | 107 | [node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody4"] 108 | mesh = SubResource( "5" ) 109 | 110 | [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody4"] 111 | shape = SubResource( "6" ) 112 | 113 | [node name="StaticBody6" type="StaticBody3D" parent="."] 114 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -8.57699) 115 | 116 | [node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody6"] 117 | mesh = SubResource( "5" ) 118 | 119 | [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody6"] 120 | shape = SubResource( "6" ) 121 | 122 | [node name="StaticBody5" type="StaticBody3D" parent="."] 123 | transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6, 0, 11) 124 | 125 | [node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody5"] 126 | mesh = SubResource( "5" ) 127 | 128 | [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody5"] 129 | shape = SubResource( "6" ) 130 | 131 | [connection signal="button_up" from="Menu/VBoxContainer/ServerButton" to="." method="_start_server"] 132 | [connection signal="button_up" from="Menu/VBoxContainer/ClientButton" to="." method="_start_client"] 133 | --------------------------------------------------------------------------------