Add multiplayer game mode over the network

This commit is contained in:
Squinternator 2024-04-07 21:03:56 +00:00
parent ecc195e38e
commit 35af9ecc2d
13 changed files with 278 additions and 81 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Godot 4+ specific ignores
.godot/
# Godot-specific ignores
.import/
export.cfg
export_presets.cfg
# Blender
*.blend1
# Imported translations (automatically generated from CSV files)
*.translation
# Added by Squinternator: weird *.tmp files added by the script editor
*.tmp
# Added by Squinternator: *.dll files compiled by the windows build
addons/godot-jolt/windows/~godot-jolt_windows-x64_editor.dll
addons/terrain_3d/bin/~libterrain.windows.debug.x86_64.dll

View file

@ -15,6 +15,14 @@ const jetpack_force_factor : float = 2
var g : float = ProjectSettings.get_setting("physics/3d/default_gravity") # in m/s²
var gravity : Vector3 = g * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
@export var player_id = 1:
set(id):
player_id = id
$PlayerInput.set_multiplayer_authority(id)
@onready var input = $PlayerInput
@onready var camera = $SpringArm3D/Camera3D
# floor detection
@onready var hud = $HUD
@onready var shape_cast = $ShapeCast3D
@ -25,15 +33,19 @@ signal energy_changed(energy)
func _ready():
energy_changed.connect(hud._on_energy_changed)
if player_id == multiplayer.get_unique_id():
camera.current = true
else:
$HUD.hide()
func is_on_floor():
func is_on_floor() -> bool:
return shape_cast.is_colliding()
func is_skiing() -> bool:
return Input.is_action_pressed("ski")
return input.skiing
func handle_jetpack(delta, direction):
if Input.is_action_pressed("jump_and_jet"):
if input.jetting:
if energy > 0:
var up_vector = Vector3.UP * jetpack_vertical_force * jetpack_force_factor
var side_vector = direction * jetpack_horizontal_force * jetpack_force_factor
@ -41,26 +53,29 @@ func handle_jetpack(delta, direction):
energy -= energy_drain_rate * delta
else:
energy += energy_charge_rate * delta
energy = clamp(energy, 0, max_energy)
energy_changed.emit(energy)
func _process(delta):
%SpringArm3D.global_transform.basis = Basis.from_euler(Vector3(input.camera_rotation.y, input.camera_rotation.x, 0.0))
func _physics_process(delta):
# retrieve user's direction vector
var _input_dir = Input.get_vector("left", "right", "forward", "backward")
var _input_dir = input.direction
# compute direction in local space
var _direction = (transform.basis * Vector3(_input_dir.x, 0, _input_dir.y)).normalized()
# adjust direction based on spring arm rotation
_direction = _direction.rotated(Vector3.UP, $SpringArm3D.rotation.y)
handle_jetpack(delta, _direction)
# handle ski
if is_skiing():
physics_material_override.friction = 0
else:
physics_material_override.friction = 1
if is_on_floor():
if not _direction.is_zero_approx() and not is_skiing():
# retrieve collision normal
@ -71,11 +86,13 @@ func _physics_process(delta):
if slope_angle <= max_floor_angle:
# adjust direction based on the floor normal to align with the slope
_direction = _direction.slide(normal)
linear_velocity = lerp(linear_velocity, _direction * ground_speed, .1)
if Input.is_action_just_pressed("jump_and_jet"):
if input.jumping:
linear_velocity.y = sqrt(2 * abs((mass * gravity * delta).y) * 1)
else:
pass
input.jumping = false

View file

@ -1,59 +1,103 @@
[gd_scene load_steps=9 format=3 uid="uid://cbhx1xme0sb7k"]
[gd_scene load_steps=11 format=3 uid="uid://cbhx1xme0sb7k"]
[ext_resource type="Script" path="res://characters/player/player.gd" id="1_ymjub"]
[ext_resource type="PackedScene" uid="uid://bcv81ku26xo" path="res://interfaces/hud/hud.tscn" id="2_5qvi2"]
[ext_resource type="PackedScene" uid="uid://c8co0qa2omjmh" path="res://weapons/space_gun/space_gun.tscn" id="2_ka38u"]
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_clur0"]
resource_local_to_scene = true
bounce = 1.0
absorbent = true
[sub_resource type="CapsuleMesh" id="CapsuleMesh_vmqfq"]
radius = 0.3
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_rm4oh"]
radius = 0.3
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_3l0v2"]
radius = 0.3
[sub_resource type="GDScript" id="GDScript_qcxi0"]
script/source = "extends SpringArm3D
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_rqdp6"]
properties/0/path = NodePath(".:linear_velocity")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:position")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:player_id")
properties/2/spawn = true
properties/2/replication_mode = 2
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_5j4ew"]
properties/0/path = NodePath(".:direction")
properties/0/spawn = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:jetting")
properties/1/spawn = false
properties/1/replication_mode = 2
properties/2/path = NodePath(".:camera_rotation")
properties/2/spawn = false
properties/2/replication_mode = 1
properties/3/path = NodePath(".:skiing")
properties/3/spawn = false
properties/3/replication_mode = 2
[sub_resource type="GDScript" id="GDScript_uy24w"]
script/source = "extends MultiplayerSynchronizer
@export var jumping = false
@export var jetting = false
@export var skiing = false
@export var direction = Vector2.ZERO
@export var camera_rotation : Vector3
@export var MOUSE_SENSITIVITY : float = 0.5
@export var WHEEL_SENSITIVITY : float = 0.5
var _mouse_position: Vector2
var _mouse_rotation : Vector3
# Called when the node enters the scene tree for the first time.
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
$Camera3D.current = true
var has_authority = get_multiplayer_authority() == multiplayer.get_unique_id()
set_process(has_authority)
set_process_unhandled_input(has_authority)
if has_authority:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent) -> void:
var mouse_mode = Input.get_mouse_mode()
# isolate mouse events
if event is InputEventMouseMotion:
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
# retrieve mouse position relative to last frame
_mouse_position = event.relative * MOUSE_SENSITIVITY
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
direction = Input.get_vector(\"left\", \"right\", \"forward\", \"backward\")
if Input.is_action_just_pressed(\"jump_and_jet\"):
jump.rpc()
if Input.is_action_just_pressed(\"fire_primary\"):
fire_primary.rpc()
jetting = Input.is_action_pressed(\"jump_and_jet\")
skiing = Input.is_action_pressed(\"ski\")
_update_camera(delta)
@rpc(\"call_local\")
func jump():
jumping = true
@rpc(\"call_local\")
func fire_primary():
$\"../SpringArm3D/Inventory/SpaceGun\".fire_primary()
func _update_camera(delta):
# clamp vertical rotation (head motion)
_mouse_rotation.y -= _mouse_position.y * delta
_mouse_rotation.y = clamp(_mouse_rotation.y, deg_to_rad(-90.0),deg_to_rad(90.0))
camera_rotation.y -= _mouse_position.y * delta
camera_rotation.y = clamp(camera_rotation.y, deg_to_rad(-90.0),deg_to_rad(90.0))
# wrap horizontal rotation (to prevent accumulation)
_mouse_rotation.x -= _mouse_position.x * delta
_mouse_rotation.x = wrapf(_mouse_rotation.x, deg_to_rad(0.0),deg_to_rad(360.0))
camera_rotation.x -= _mouse_position.x * delta
camera_rotation.x = wrapf(camera_rotation.x, deg_to_rad(0.0),deg_to_rad(360.0))
# reset mouse motion until next input event
_mouse_position = Vector2.ZERO
# update spring arm global rotation
global_transform.basis = Basis.from_euler(Vector3(_mouse_rotation.y, _mouse_rotation.x, 0.0))
"
[node name="Player" type="RigidBody3D"]
@ -66,7 +110,6 @@ continuous_cd = true
script = ExtResource("1_ymjub")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
visible = false
mesh = SubResource("CapsuleMesh_vmqfq")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
@ -84,7 +127,6 @@ collide_with_areas = true
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
spring_length = 0.0
script = SubResource("GDScript_qcxi0")
[node name="Camera3D" type="Camera3D" parent="SpringArm3D"]
fov = 100.0
@ -93,4 +135,12 @@ near = 0.1
[node name="Inventory" type="Node3D" parent="SpringArm3D"]
[node name="SpaceGun" parent="SpringArm3D/Inventory" instance=ExtResource("2_ka38u")]
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0.145, -0.095, -0.261784)
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0.281712, -0.095, -0.353461)
[node name="ServerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_rqdp6")
[node name="PlayerInput" type="MultiplayerSynchronizer" parent="."]
root_path = NodePath(".")
replication_config = SubResource("SceneReplicationConfig_5j4ew")
script = SubResource("GDScript_uy24w")

11
game_modes/demo.tscn Normal file
View file

@ -0,0 +1,11 @@
[gd_scene load_steps=3 format=3 uid="uid://boviiugcnfyrj"]
[ext_resource type="PackedScene" uid="uid://chbno00ugl6te" path="res://maps/genesis/genesis.tscn" id="2_lsep7"]
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://characters/player/player.tscn" id="3_j1li7"]
[node name="Demo" type="Node"]
[node name="Map" parent="." instance=ExtResource("2_lsep7")]
[node name="Player" parent="." instance=ExtResource("3_j1li7")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 92.7007, 0)

View file

@ -0,0 +1,68 @@
[gd_scene load_steps=2 format=3 uid="uid://c7ae4jw5d8mue"]
[sub_resource type="GDScript" id="GDScript_pj58d"]
script/source = "extends Node
@onready var map = $Map
@onready var players = $Players
const PLAYER : PackedScene = preload(\"res://characters/player/player.tscn\")
func start_server(peer):
multiplayer.multiplayer_peer = peer
print(\"Server started\")
load_map.call_deferred(load(\"res://maps/genesis/genesis.tscn\"))
multiplayer.peer_connected.connect(add_player)
multiplayer.peer_disconnected.connect(remove_player)
for id in multiplayer.get_peers():
add_player(id)
add_player(1)
func join_server(peer):
multiplayer.multiplayer_peer = peer
func load_map(scene : PackedScene):
map.add_child(scene.instantiate())
func add_player(peer_id : int):
var node_name = str(peer_id)
var player_scene_instance = PLAYER.instantiate()
player_scene_instance.name = node_name
player_scene_instance.player_id = peer_id
player_scene_instance.position = Vector3(0.0, 150.0, 0.0)
players.add_child(player_scene_instance)
print(\"Peer `%s` connected\" % node_name)
func remove_player(peer_id : int):
var node_name = str(peer_id)
players.get_node(node_name).queue_free()
print(\"Peer `%s` disconnected\" % node_name)
func _exit_tree():
if not multiplayer.is_server():
return
multiplayer.peer_connected.disconnect(add_player)
multiplayer.peer_disconnected.disconnect(remove_player)
"
[node name="Multiplayer" type="Node"]
script = SubResource("GDScript_pj58d")
[node name="Map" type="Node" parent="."]
[node name="MapSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://maps/genesis/genesis.tscn")
spawn_path = NodePath("../Map")
spawn_limit = 1
[node name="Players" type="Node" parent="."]
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://characters/player/player.tscn")
spawn_path = NodePath("../Players")

View file

@ -1,8 +1,31 @@
[gd_scene load_steps=5 format=3 uid="uid://bjctlqvs33nqy"]
[gd_scene load_steps=6 format=3 uid="uid://bjctlqvs33nqy"]
[ext_resource type="Texture2D" uid="uid://c1tjamjm8qjog" path="res://interfaces/menus/boot/background.webp" id="1_ph586"]
[ext_resource type="PackedScene" uid="uid://1seg8cvss7a7" path="res://interfaces/menus/boot/multiplayer.tscn" id="2_lcb0h"]
[sub_resource type="GDScript" id="GDScript_jd8xf"]
resource_name = "MainMenu"
script/source = "extends CanvasLayer
class_name MainMenu
signal start_demo
signal start_server(peer)
signal join_server(peer)
func _ready():
$Multiplayer.start_server.connect(_on_start_server)
$Multiplayer.join_server.connect(_on_join_server)
func _on_demo_pressed():
start_demo.emit()
func _on_start_server(peer):
start_server.emit(peer)
func _on_join_server(peer):
join_server.emit(peer)
"
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_snh7i"]
bg_color = Color(0, 0.5, 0.5, 0.25)
@ -11,18 +34,6 @@ script/source = "extends PanelContainer
const PLAYER = preload(\"res://characters/player/player.tscn\")
const MAP = preload(\"res://maps/genesis/genesis.tscn\")
func _on_demo_pressed():
# instantiate scenes
var player = PLAYER.instantiate()
var map = MAP.instantiate()
# set player initial position
player.position = Vector3(0,150,0)
# add as siblings in tree
for o in [map, player]:
owner.add_sibling(o)
# queue main_menu node for deletion
owner.queue_free()
func _on_multiplayer_pressed():
hide()
@ -33,6 +44,7 @@ func _on_quit_pressed():
"
[node name="MainMenu" type="CanvasLayer"]
script = SubResource("GDScript_jd8xf")
[node name="TextureRect" type="TextureRect" parent="."]
anchors_preset = 15
@ -81,6 +93,6 @@ layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "Quit"
[connection signal="pressed" from="Main/MarginContainer/VBoxContainer/Demo" to="Main" method="_on_demo_pressed"]
[connection signal="pressed" from="Main/MarginContainer/VBoxContainer/Demo" to="." method="_on_demo_pressed"]
[connection signal="pressed" from="Main/MarginContainer/VBoxContainer/Multiplayer" to="Main" method="_on_multiplayer_pressed"]
[connection signal="pressed" from="Main/MarginContainer/VBoxContainer/Quit" to="Main" method="_on_quit_pressed"]

View file

@ -13,6 +13,9 @@ const DEFAULT_HOST : String = \"localhost\"
var _join_address = RegEx.new()
var _registered_ports = RegEx.new()
signal start_server(peer)
signal join_server(peer)
func _ready():
# see https://datatracker.ietf.org/doc/html/rfc1700
_registered_ports.compile(r'^(?:102[4-9]|10[3-9]\\d|1[1-9]\\d{2}|[2-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$')
@ -38,7 +41,8 @@ func _on_join_pressed():
# create client
var peer = ENetMultiplayerPeer.new()
peer.create_client(addr[0], addr[1])
multiplayer.multiplayer_peer = peer
join_server.emit(peer)
func _on_host_pressed():
var port = DEFAULT_PORT
@ -52,16 +56,11 @@ func _on_host_pressed():
push_warning(\"A valid port number in the range 1024-65535 is required.\")
return
# create server
## create server
var peer = ENetMultiplayerPeer.new()
peer.create_server(port, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer
multiplayer.peer_connected.connect(_on_peer_connected)
func _on_peer_connected(peer_id):
print(\"Peer `%s` connected\" % peer_id)
start_server.emit(peer)
"
[node name="Multiplayer" type="PanelContainer"]

View file

@ -3,21 +3,56 @@
[ext_resource type="PackedScene" uid="uid://bjctlqvs33nqy" path="res://interfaces/menus/boot/boot.tscn" id="1_acy5o"]
[sub_resource type="GDScript" id="GDScript_e61dq"]
script/source = "extends Node3D
script/source = "
extends Node3D
class_name Main
@onready var main_menu = $MainMenu
@onready var game_mode = $GameMode
func _ready():
$MainMenu.start_demo.connect(_start_demo)
$MainMenu.start_server.connect(_start_server)
$MainMenu.join_server.connect(_join_server)
func _unhandled_input(event):
# exit the program
if event.is_action_pressed(\"exit\"):
get_tree().quit()
# switch window mode
# switch window mode
if event.is_action_pressed(\"window_mode\"):
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
func game_mode_cleanup():
for c in game_mode.get_children():
game_mode.remove_child(c)
c.queue_free()
func start_game_mode(game_mode_scene : PackedScene):
main_menu.hide()
game_mode_cleanup()
var scene_instance = game_mode_scene.instantiate()
game_mode.add_child(scene_instance)
return scene_instance
func _start_demo():
start_game_mode(load(\"res://game_modes/demo.tscn\"))
func _start_server(peer):
var server_scene = start_game_mode(load(\"res://game_modes/multiplayer.tscn\"))
server_scene.start_server(peer)
func _join_server(peer):
var client_scene = start_game_mode(load(\"res://game_modes/multiplayer.tscn\"))
client_scene.join_server(peer)
"
[node name="Game" type="Node3D"]
script = SubResource("GDScript_e61dq")
[node name="MainMenu" parent="." instance=ExtResource("1_acy5o")]
[node name="GameMode" type="Node3D" parent="."]

Binary file not shown.

View file

@ -1,11 +1,3 @@
[gd_resource type="ShaderMaterial" load_steps=3 format=3 uid="uid://de6t4olk7hrs1"]
[ext_resource type="Texture2D" uid="uid://cvtqt0k2ewd07" path="res://weapons/space_gun/assets/albedo.png" id="1_36iph"]
[ext_resource type="Shader" path="res://shaders/zclip.gdshader" id="1_c4uko"]
[gd_resource type="ShaderMaterial" format=3 uid="uid://de6t4olk7hrs1"]
[resource]
render_priority = 0
shader = ExtResource("1_c4uko")
shader_parameter/FOV = 70.0
shader_parameter/albedo = Color(1, 1, 1, 1)
shader_parameter/texture_albedo = ExtResource("1_36iph")

View file

@ -21,10 +21,10 @@ func print_node_properties(node):
func _ready():
shoot.connect(_on_shoot)
func _input(_event):
if Input.is_action_just_pressed("fire_primary"):
var projectile = PROJECTILE.instantiate()
shoot.emit(projectile, nozzle, inventory.owner)
func fire_primary():
var projectile = PROJECTILE.instantiate()
shoot.emit(projectile, nozzle, inventory.owner)
func _on_shoot(projectile, origin, player):
projectile.position = origin.global_position

View file

@ -1,16 +1,8 @@
[gd_scene load_steps=7 format=3 uid="uid://c8co0qa2omjmh"]
[gd_scene load_steps=5 format=3 uid="uid://c8co0qa2omjmh"]
[ext_resource type="Script" path="res://weapons/space_gun/space_gun.gd" id="1_6sm4s"]
[ext_resource type="Texture2D" uid="uid://cvtqt0k2ewd07" path="res://weapons/space_gun/assets/albedo.png" id="1_51bf5"]
[ext_resource type="Material" uid="uid://de6t4olk7hrs1" path="res://weapons/space_gun/assets/material.tres" id="1_uaehs"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ioife"]
resource_name = "dark"
albedo_color = Color(0, 0, 0, 1)
albedo_texture = ExtResource("1_51bf5")
metallic = 0.35
texture_filter = 1
[sub_resource type="ArrayMesh" id="ArrayMesh_2hpnh"]
_surfaces = [{
"aabb": AABB(-1.5172, -0.57, -4.4, 1.0692, 1.57, 4.6),
@ -55,7 +47,6 @@ _surfaces = [{
"format": 34896613399,
"index_count": 60,
"index_data": PackedByteArray(30, 4, 28, 4, 29, 4, 29, 4, 31, 4, 30, 4, 34, 4, 32, 4, 33, 4, 33, 4, 35, 4, 34, 4, 38, 4, 36, 4, 37, 4, 37, 4, 39, 4, 38, 4, 42, 4, 40, 4, 41, 4, 41, 4, 43, 4, 42, 4, 43, 4, 44, 4, 42, 4, 43, 4, 45, 4, 44, 4, 48, 4, 46, 4, 47, 4, 47, 4, 49, 4, 48, 4, 234, 3, 233, 3, 50, 4, 53, 4, 51, 4, 52, 4, 52, 4, 54, 4, 53, 4, 57, 4, 55, 4, 56, 4, 60, 4, 58, 4, 59, 4, 59, 4, 61, 4, 60, 4, 64, 4, 62, 4, 63, 4, 65, 4, 238, 3, 222, 3),
"material": SubResource("StandardMaterial3D_ioife"),
"name": "dark",
"primitive": 3,
"uv_scale": Vector4(8.8, 10.8, 0, 0),
@ -75,4 +66,4 @@ mesh = SubResource("ArrayMesh_hudfn")
skeleton = NodePath("")
[node name="Nozzle" type="Node3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.225)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.315549)