Use static typing in all demos (#1063)

This leads to code that is easier to understand and runs
faster thanks to GDScript's typed instructions.

The untyped declaration warning is now enabled on all projects
where type hints were added. All projects currently run without
any untyped declration warnings.

Dodge the Creeps and Squash the Creeps demos intentionally don't
use type hints to match the documentation, where type hints haven't
been adopted yet (given its beginner focus).
This commit is contained in:
Hugo Locurcio
2024-06-01 12:12:18 +02:00
committed by GitHub
parent 8e9c180278
commit bac1e69164
498 changed files with 5218 additions and 4776 deletions

View File

@@ -4,13 +4,14 @@ var in_area: Array = []
var from_player: int
# Called from the animation.
func explode():
func explode() -> void:
if not is_multiplayer_authority():
# Explode only on authority.
return
for p in in_area:
for p: Object in in_area:
if p.has_method("exploded"):
# Checks if there is wall in between bomb and the object
# Checks if there is wall in between bomb and the object.
var world_state: PhysicsDirectSpaceState2D = get_world_2d().direct_space_state
var query := PhysicsRayQueryParameters2D.create(position, p.position)
query.hit_from_inside = true
@@ -20,15 +21,15 @@ func explode():
p.exploded.rpc(from_player)
func done():
func done() -> void:
if is_multiplayer_authority():
queue_free()
func _on_bomb_body_enter(body):
func _on_bomb_body_enter(body: Node2D) -> void:
if not body in in_area:
in_area.append(body)
func _on_bomb_body_exit(body):
func _on_bomb_body_exit(body: Node2D) -> void:
in_area.erase(body)

View File

@@ -125,10 +125,10 @@ angular_velocity_max = 188.35
scale_amount_curve = SubResource("Curve_4yges")
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
autoplay = "anim"
libraries = {
"": SubResource("AnimationLibrary_h2w7m")
}
autoplay = "anim"
[connection signal="body_entered" from="." to="." method="_on_bomb_body_enter"]
[connection signal="body_exited" from="." to="." method="_on_bomb_body_exit"]

View File

@@ -1,13 +1,14 @@
extends MultiplayerSpawner
func _init():
func _init() -> void:
spawn_function = _spawn_bomb
func _spawn_bomb(data):
func _spawn_bomb(data: Array) -> Area2D:
if data.size() != 2 or typeof(data[0]) != TYPE_VECTOR2 or typeof(data[1]) != TYPE_INT:
return null
var bomb = preload("res://bomb.tscn").instantiate()
var bomb: Area2D = preload("res://bomb.tscn").instantiate()
bomb.position = data[0]
bomb.from_player = data[1]
return bomb

View File

@@ -1,149 +1,156 @@
extends Node
# Default game server port. Can be any number between 1024 and 49151.
# Not on the list of registered or common ports as of November 2020:
# Not on the list of registered or common ports as of May 2024:
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
const DEFAULT_PORT = 10567
# Max number of players.
## The maximum number of players.
const MAX_PEERS = 12
var peer = null
var peer: ENetMultiplayerPeer
# Name for my player.
var player_name = "The Warrior"
## Our local player's name.
var player_name := "The Warrior"
# Names for remote players in id:name format.
var players = {}
var players_ready = []
var players := {}
var players_ready: Array[int] = []
# Signals to let lobby GUI know what's going on.
signal player_list_changed()
signal connection_failed()
signal connection_succeeded()
signal game_ended()
signal game_error(what)
signal game_error(what: int)
# Callback from SceneTree.
func _player_connected(id):
func _player_connected(id: int) -> void:
# Registration of a client beings here, tell the connected player that we are here.
register_player.rpc_id(id, player_name)
# Callback from SceneTree.
func _player_disconnected(id):
if has_node("/root/World"): # Game is in progress.
func _player_disconnected(id: int) -> void:
if has_node("/root/World"):
# Game is in progress.
if multiplayer.is_server():
game_error.emit("Player " + players[id] + " disconnected")
end_game()
else: # Game is not in progress.
else:
# Game is not in progress.
# Unregister this player.
unregister_player(id)
# Callback from SceneTree, only for clients (not server).
func _connected_ok():
func _connected_ok() -> void:
# We just connected to a server
connection_succeeded.emit()
# Callback from SceneTree, only for clients (not server).
func _server_disconnected():
func _server_disconnected() -> void:
game_error.emit("Server disconnected")
end_game()
# Callback from SceneTree, only for clients (not server).
func _connected_fail():
func _connected_fail() -> void:
multiplayer.set_network_peer(null) # Remove peer
connection_failed.emit()
# Lobby management functions.
@rpc("any_peer")
func register_player(new_player_name):
var id = multiplayer.get_remote_sender_id()
func register_player(new_player_name: String) -> void:
var id := multiplayer.get_remote_sender_id()
players[id] = new_player_name
player_list_changed.emit()
func unregister_player(id):
func unregister_player(id: int) -> void:
players.erase(id)
player_list_changed.emit()
@rpc("call_local")
func load_world():
func load_world() -> void:
# Change scene.
var world = load("res://world.tscn").instantiate()
var world: Node2D = load("res://world.tscn").instantiate()
get_tree().get_root().add_child(world)
get_tree().get_root().get_node("Lobby").hide()
# Set up score.
world.get_node("Score").add_player(multiplayer.get_unique_id(), player_name)
for pn in players:
for pn: int in players:
world.get_node("Score").add_player(pn, players[pn])
get_tree().set_pause(false) # Unpause and unleash the game!
# Unpause and unleash the game!
get_tree().paused = false
func host_game(new_player_name):
func host_game(new_player_name: String) -> void:
player_name = new_player_name
peer = ENetMultiplayerPeer.new()
peer.create_server(DEFAULT_PORT, MAX_PEERS)
multiplayer.set_multiplayer_peer(peer)
func join_game(ip, new_player_name):
func join_game(ip: String, new_player_name: String) -> void:
player_name = new_player_name
peer = ENetMultiplayerPeer.new()
peer.create_client(ip, DEFAULT_PORT)
multiplayer.set_multiplayer_peer(peer)
func get_player_list():
func get_player_list() -> Array:
return players.values()
func get_player_name():
return player_name
func begin_game():
func begin_game() -> void:
assert(multiplayer.is_server())
load_world.rpc()
var world = get_tree().get_root().get_node("World")
var player_scene = load("res://player.tscn")
var world: Node2D = get_tree().get_root().get_node("World")
var player_scene: PackedScene = load("res://player.tscn")
# Create a dictionary with peer id and respective spawn points, could be improved by randomizing.
var spawn_points = {}
spawn_points[1] = 0 # Server in spawn point 0.
var spawn_point_idx = 1
for p in players:
# Create a dictionary with peer ID. and respective spawn points.
# TODO: This could be improved by randomizing spawn points for players.
var spawn_points := {}
spawn_points[1] = 0 # Server in spawn point 0.
var spawn_point_idx := 1
for p: int in players:
spawn_points[p] = spawn_point_idx
spawn_point_idx += 1
for p_id in spawn_points:
var spawn_pos = world.get_node("SpawnPoints/" + str(spawn_points[p_id])).position
var player = player_scene.instantiate()
for p_id: int in spawn_points:
var spawn_pos: Vector2 = world.get_node("SpawnPoints/" + str(spawn_points[p_id])).position
var player := player_scene.instantiate()
player.synced_position = spawn_pos
player.name = str(p_id)
player.set_player_name(player_name if p_id == multiplayer.get_unique_id() else players[p_id])
world.get_node("Players").add_child(player)
# The RPC must be called after the player is added to the scene tree.
player.set_player_name.rpc(player_name if p_id == multiplayer.get_unique_id() else players[p_id])
func end_game():
if has_node("/root/World"): # Game is in progress.
# End it
func end_game() -> void:
if has_node("/root/World"):
# If the game is in progress, end it.
get_node("/root/World").queue_free()
game_ended.emit()
players.clear()
func _ready():
func _ready() -> void:
multiplayer.peer_connected.connect(_player_connected)
multiplayer.peer_disconnected.connect(_player_disconnected)
multiplayer.connected_to_server.connect(_connected_ok)
multiplayer.connection_failed.connect(_connected_fail)
multiplayer.server_disconnected.connect(_server_disconnected)
## Returns an unique-looking player color based on the name's hash.
func get_player_color(p_name: String) -> Color:
return Color.from_hsv(wrapf(p_name.hash() * 0.001, 0.0, 1.0), 0.6, 1.0)

View File

@@ -1,6 +1,6 @@
extends Control
func _ready():
func _ready() -> void:
# Called every time the node is added to the scene.
gamestate.connection_failed.connect(_on_connection_failed)
gamestate.connection_succeeded.connect(_on_connection_success)
@@ -11,11 +11,11 @@ func _ready():
if OS.has_environment("USERNAME"):
$Connect/Name.text = OS.get_environment("USERNAME")
else:
var desktop_path = OS.get_system_dir(0).replace("\\", "/").split("/")
var desktop_path := OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP).replace("\\", "/").split("/")
$Connect/Name.text = desktop_path[desktop_path.size() - 2]
func _on_host_pressed():
func _on_host_pressed() -> void:
if $Connect/Name.text == "":
$Connect/ErrorLabel.text = "Invalid name!"
return
@@ -24,17 +24,18 @@ func _on_host_pressed():
$Players.show()
$Connect/ErrorLabel.text = ""
var player_name = $Connect/Name.text
var player_name: String = $Connect/Name.text
gamestate.host_game(player_name)
get_window().title = ProjectSettings.get_setting("application/config/name") + ": Server (%s)" % $Connect/Name.text
refresh_lobby()
func _on_join_pressed():
func _on_join_pressed() -> void:
if $Connect/Name.text == "":
$Connect/ErrorLabel.text = "Invalid name!"
return
var ip = $Connect/IPAddress.text
var ip: String = $Connect/IPAddress.text
if not ip.is_valid_ip_address():
$Connect/ErrorLabel.text = "Invalid IP address!"
return
@@ -43,22 +44,23 @@ func _on_join_pressed():
$Connect/Host.disabled = true
$Connect/Join.disabled = true
var player_name = $Connect/Name.text
var player_name: String = $Connect/Name.text
gamestate.join_game(ip, player_name)
get_window().title = ProjectSettings.get_setting("application/config/name") + ": Client (%s)" % $Connect/Name.text
func _on_connection_success():
func _on_connection_success() -> void:
$Connect.hide()
$Players.show()
func _on_connection_failed():
func _on_connection_failed() -> void:
$Connect/Host.disabled = false
$Connect/Join.disabled = false
$Connect/ErrorLabel.set_text("Connection failed.")
func _on_game_ended():
func _on_game_ended() -> void:
show()
$Connect.show()
$Players.hide()
@@ -66,27 +68,27 @@ func _on_game_ended():
$Connect/Join.disabled = false
func _on_game_error(errtxt):
func _on_game_error(errtxt: String) -> void:
$ErrorDialog.dialog_text = errtxt
$ErrorDialog.popup_centered()
$Connect/Host.disabled = false
$Connect/Join.disabled = false
func refresh_lobby():
var players = gamestate.get_player_list()
func refresh_lobby() -> void:
var players := gamestate.get_player_list()
players.sort()
$Players/List.clear()
$Players/List.add_item(gamestate.get_player_name() + " (You)")
for p in players:
$Players/List.add_item(gamestate.player_name + " (you)")
for p: String in players:
$Players/List.add_item(p)
$Players/Start.disabled = not multiplayer.is_server()
func _on_start_pressed():
func _on_start_pressed() -> void:
gamestate.begin_game()
func _on_find_public_ip_pressed():
func _on_find_public_ip_pressed() -> void:
OS.shell_open("https://icanhazip.com/")

View File

@@ -1,27 +1,28 @@
extends CharacterBody2D
## The player's movement speed (in pixels per second).
const MOTION_SPEED = 90.0
## The delay before which you can place a new bomb (in seconds).
const BOMB_RATE = 0.5
@export
var synced_position := Vector2()
@export var synced_position := Vector2()
@export
var stunned = false
@export var stunned := false
@onready
var inputs = $Inputs
var last_bomb_time = BOMB_RATE
var current_anim = ""
var last_bomb_time := BOMB_RATE
var current_anim := ""
func _ready():
@onready var inputs: Node = $Inputs
func _ready() -> void:
stunned = false
position = synced_position
if str(name).is_valid_int():
get_node("Inputs/InputsSync").set_multiplayer_authority(str(name).to_int())
$"Inputs/InputsSync".set_multiplayer_authority(str(name).to_int())
func _physics_process(delta):
func _physics_process(delta: float) -> void:
if multiplayer.multiplayer_peer == null or str(multiplayer.get_unique_id()) == str(name):
# The client which this player represent will update the controls state, and notify it to everyone.
inputs.update()
@@ -33,43 +34,48 @@ func _physics_process(delta):
last_bomb_time += delta
if not stunned and is_multiplayer_authority() and inputs.bombing and last_bomb_time >= BOMB_RATE:
last_bomb_time = 0.0
get_node("../../BombSpawner").spawn([position, str(name).to_int()])
$"../../BombSpawner".spawn([position, str(name).to_int()])
else:
# The client simply updates the position to the last known one.
position = synced_position
if not stunned:
# Everybody runs physics. I.e. clients tries to predict where they will be during the next frame.
# Everybody runs physics. i.e. clients try to predict where they will be during the next frame.
velocity = inputs.motion * MOTION_SPEED
move_and_slide()
# Also update the animation based on the last known player input state
var new_anim = "standing"
# Also update the animation based on the last known player input state.
var new_anim := &"standing"
if inputs.motion.y < 0:
new_anim = "walk_up"
new_anim = &"walk_up"
elif inputs.motion.y > 0:
new_anim = "walk_down"
new_anim = &"walk_down"
elif inputs.motion.x < 0:
new_anim = "walk_left"
new_anim = &"walk_left"
elif inputs.motion.x > 0:
new_anim = "walk_right"
new_anim = &"walk_right"
if stunned:
new_anim = "stunned"
new_anim = &"stunned"
if new_anim != current_anim:
current_anim = new_anim
get_node("anim").play(current_anim)
func set_player_name(value):
get_node("label").text = value
$anim.play(current_anim)
@rpc("call_local")
func exploded(_by_who):
func set_player_name(value: String) -> void:
$label.text = value
# Assign a random color to the player based on its name.
$label.modulate = gamestate.get_player_color(value)
$sprite.modulate = Color(0.5, 0.5, 0.5) + gamestate.get_player_color(value)
@rpc("call_local")
func exploded(_by_who: int) -> void:
if stunned:
return
stunned = true
get_node("anim").play("stunned")
$anim.play("stunned")

View File

@@ -6,7 +6,7 @@
[ext_resource type="Script" path="res://player_controls.gd" id="4_k1vfr"]
[sub_resource type="CircleShape2D" id="1"]
radius = 20.0
radius = 16.0
[sub_resource type="Animation" id="2"]
resource_name = "standing"
@@ -149,22 +149,18 @@ outline_color = Color(0, 0, 0, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_sh64w"]
properties/0/path = NodePath(".:synced_position")
properties/0/spawn = true
properties/0/sync = true
properties/0/watch = false
properties/0/replication_mode = 1
properties/1/path = NodePath("label:text")
properties/1/spawn = true
properties/1/sync = false
properties/1/watch = false
properties/1/replication_mode = 0
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_w53uu"]
properties/0/path = NodePath(".:motion")
properties/0/spawn = true
properties/0/sync = true
properties/0/watch = false
properties/0/replication_mode = 1
properties/1/path = NodePath(".:bombing")
properties/1/spawn = true
properties/1/sync = true
properties/1/watch = false
properties/1/replication_mode = 1
[node name="player" type="CharacterBody2D"]
z_index = 10

View File

@@ -1,24 +1,22 @@
extends Node
@export
var motion = Vector2():
@export var motion := Vector2():
set(value):
# This will be sent by players, make sure values are within limits.
motion = clamp(value, Vector2(-1, -1), Vector2(1, 1))
@export
var bombing = false
@export var bombing := false
func update():
var m = Vector2()
if Input.is_action_pressed("move_left"):
func update() -> void:
var m := Vector2()
if Input.is_action_pressed(&"move_left"):
m += Vector2(-1, 0)
if Input.is_action_pressed("move_right"):
if Input.is_action_pressed(&"move_right"):
m += Vector2(1, 0)
if Input.is_action_pressed("move_up"):
if Input.is_action_pressed(&"move_up"):
m += Vector2(0, -1)
if Input.is_action_pressed("move_down"):
if Input.is_action_pressed(&"move_down"):
m += Vector2(0, 1)
motion = m
bombing = Input.is_action_pressed("set_bomb")
bombing = Input.is_action_pressed(&"set_bomb")

View File

@@ -23,6 +23,10 @@ config/icon="res://icon.webp"
gamestate="*res://gamestate.gd"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"
@@ -75,6 +79,8 @@ set_bomb={
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
2d/snap/snap_2d_transforms_to_pixel=true
2d/snap/snap_2d_vertices_to_pixel=true
[replication]

View File

@@ -1,6 +1,6 @@
extends CharacterBody2D
@rpc("call_local")
func exploded(by_who):
func exploded(by_who: int) -> void:
$"../../Score".increase_score(by_who)
$"AnimationPlayer".play("explode")

View File

@@ -1,13 +1,13 @@
extends HBoxContainer
var player_labels = {}
var player_labels := {}
func _process(_delta):
var rocks_left = $"../Rocks".get_child_count()
func _process(_delta: float) -> void:
var rocks_left := $"../Rocks".get_child_count()
if rocks_left == 0:
var winner_name = ""
var winner_score = 0
for p in player_labels:
var winner_name := ""
var winner_score := 0
for p: int in player_labels:
if player_labels[p].score > winner_score:
winner_score = player_labels[p].score
winner_name = player_labels[p].name
@@ -16,30 +16,36 @@ func _process(_delta):
$"../Winner".show()
func increase_score(for_who):
func increase_score(for_who: int) -> void:
assert(for_who in player_labels)
var pl = player_labels[for_who]
var pl: Dictionary = player_labels[for_who]
pl.score += 1
pl.label.set_text(pl.name + "\n" + str(pl.score))
func add_player(id, new_player_name):
var l = Label.new()
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
l.set_text(new_player_name + "\n" + "0")
l.set_h_size_flags(SIZE_EXPAND_FILL)
var font = preload("res://montserrat.otf")
l.set("custom_fonts/font", font)
l.set("custom_font_size/font_size", 18)
add_child(l)
func add_player(id: int, new_player_name: String) -> void:
var label := Label.new()
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.text = new_player_name + "\n" + "0"
label.modulate = gamestate.get_player_color(new_player_name)
label.size_flags_horizontal = SIZE_EXPAND_FILL
label.add_theme_font_override("font", preload("res://montserrat.otf"))
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 9)
label.add_theme_font_size_override("font_size", 18)
add_child(label)
player_labels[id] = { name = new_player_name, label = l, score = 0 }
player_labels[id] = {
name = new_player_name,
label = label,
score = 0,
}
func _ready():
func _ready() -> void:
$"../Winner".hide()
set_process(true)
func _on_exit_game_pressed():
func _on_exit_game_pressed() -> void:
gamestate.end_game()

View File

@@ -10,7 +10,7 @@
[node name="TileMap" type="TileMap" parent="."]
tile_set = ExtResource("1")
cell_quadrant_size = 48
rendering_quadrant_size = 48
format = 2
layer_0/tile_data = PackedInt32Array(0, 0, 0, 65536, 0, 0, 131072, 0, 0, 196608, 0, 0, 262144, 0, 0, 327680, 0, 0, 393216, 0, 0, 458752, 0, 0, 524288, 0, 0, 589824, 0, 0, 655360, 0, 0, 720896, 0, 0, 786432, 0, 0, 1, 0, 0, 65537, 65536, 0, 131073, 65536, 0, 196609, 65536, 0, 262145, 65536, 0, 327681, 65536, 0, 393217, 65536, 0, 458753, 65536, 0, 524289, 65536, 0, 589825, 65536, 0, 655361, 65536, 0, 720897, 65536, 0, 786433, 0, 0, 2, 0, 0, 65538, 65536, 0, 131074, 0, 0, 196610, 65536, 0, 262146, 0, 0, 327682, 65536, 0, 393218, 0, 0, 458754, 65536, 0, 524290, 0, 0, 589826, 65536, 0, 655362, 0, 0, 720898, 65536, 0, 786434, 0, 0, 3, 0, 0, 65539, 65536, 0, 131075, 65536, 0, 196611, 65536, 0, 262147, 65536, 0, 327683, 65536, 0, 393219, 65536, 0, 458755, 65536, 0, 524291, 0, 0, 589827, 65536, 0, 655363, 65536, 0, 720899, 65536, 0, 786435, 0, 0, 4, 0, 0, 65540, 65536, 0, 131076, 0, 0, 196612, 0, 0, 262148, 0, 0, 327684, 65536, 0, 393220, 0, 0, 458756, 65536, 0, 524292, 0, 0, 589828, 65536, 0, 655364, 0, 0, 720900, 65536, 0, 786436, 0, 0, 5, 0, 0, 65541, 65536, 0, 131077, 65536, 0, 196613, 65536, 0, 262149, 65536, 0, 327685, 65536, 0, 393221, 65536, 0, 458757, 65536, 0, 524293, 65536, 0, 589829, 65536, 0, 655365, 65536, 0, 720901, 65536, 0, 786437, 0, 0, 6, 0, 0, 65542, 65536, 0, 131078, 0, 0, 196614, 65536, 0, 262150, 0, 0, 327686, 0, 0, 393222, 0, 0, 458758, 65536, 0, 524294, 0, 0, 589830, 65536, 0, 655366, 0, 0, 720902, 65536, 0, 786438, 0, 0, 7, 0, 0, 65543, 65536, 0, 131079, 65536, 0, 196615, 65536, 0, 262151, 65536, 0, 327687, 65536, 0, 393223, 65536, 0, 458759, 65536, 0, 524295, 65536, 0, 589831, 65536, 0, 655367, 65536, 0, 720903, 65536, 0, 786439, 0, 0, 8, 0, 0, 65544, 65536, 0, 131080, 0, 0, 196616, 65536, 0, 262152, 0, 0, 327688, 65536, 0, 393224, 0, 0, 458760, 65536, 0, 524296, 0, 0, 589832, 65536, 0, 655368, 0, 0, 720904, 65536, 0, 786440, 0, 0, 9, 0, 0, 65545, 65536, 0, 131081, 65536, 0, 196617, 65536, 0, 262153, 65536, 0, 327689, 65536, 0, 393225, 65536, 0, 458761, 65536, 0, 524297, 65536, 0, 589833, 65536, 0, 655369, 65536, 0, 720905, 65536, 0, 786441, 0, 0, 10, 0, 0, 65546, 65536, 0, 131082, 0, 0, 196618, 0, 0, 262154, 0, 0, 327690, 65536, 0, 393226, 0, 0, 458762, 65536, 0, 524298, 0, 0, 589834, 65536, 0, 655370, 0, 0, 720906, 65536, 0, 786442, 0, 0, 11, 0, 0, 65547, 65536, 0, 131083, 0, 0, 196619, 65536, 0, 262155, 65536, 0, 327691, 65536, 0, 393227, 65536, 0, 458763, 65536, 0, 524299, 65536, 0, 589835, 65536, 0, 655371, 65536, 0, 720907, 65536, 0, 786443, 0, 0, 12, 0, 0, 65548, 65536, 0, 131084, 0, 0, 196620, 65536, 0, 262156, 0, 0, 327692, 65536, 0, 393228, 0, 0, 458764, 65536, 0, 524300, 0, 0, 589836, 65536, 0, 655372, 0, 0, 720908, 65536, 0, 786444, 0, 0, 13, 0, 0, 65549, 65536, 0, 131085, 0, 0, 196621, 65536, 0, 262157, 65536, 0, 327693, 65536, 0, 393229, 0, 0, 458765, 65536, 0, 524301, 0, 0, 589837, 65536, 0, 655373, 65536, 0, 720909, 65536, 0, 786445, 0, 0, 14, 0, 0, 65550, 65536, 0, 131086, 0, 0, 196622, 65536, 0, 262158, 0, 0, 327694, 65536, 0, 393230, 0, 0, 458766, 65536, 0, 524302, 0, 0, 589838, 65536, 0, 655374, 0, 0, 720910, 65536, 0, 786446, 0, 0, 15, 0, 0, 65551, 65536, 0, 131087, 65536, 0, 196623, 65536, 0, 262159, 65536, 0, 327695, 65536, 0, 393231, 0, 0, 458767, 65536, 0, 524303, 65536, 0, 589839, 65536, 0, 655375, 65536, 0, 720911, 65536, 0, 786447, 0, 0, 16, 0, 0, 65552, 65536, 0, 131088, 0, 0, 196624, 65536, 0, 262160, 0, 0, 327696, 65536, 0, 393232, 0, 0, 458768, 65536, 0, 524304, 0, 0, 589840, 65536, 0, 655376, 0, 0, 720912, 65536, 0, 786448, 0, 0, 17, 0, 0, 65553, 65536, 0, 131089, 65536, 0, 196625, 65536, 0, 262161, 65536, 0, 327697, 65536, 0, 393233, 65536, 0, 458769, 65536, 0, 524305, 65536, 0, 589841, 65536, 0, 655377, 65536, 0, 720913, 65536, 0, 786449, 0, 0, 18, 0, 0, 65554, 65536, 0, 131090, 0, 0, 196626, 65536, 0, 262162, 0, 0, 327698, 0, 0, 393234, 0, 0, 458770, 65536, 0, 524306, 0, 0, 589842, 65536, 0, 655378, 0, 0, 720914, 65536, 0, 786450, 0, 0, 19, 0, 0, 65555, 65536, 0, 131091, 65536, 0, 196627, 65536, 0, 262163, 65536, 0, 327699, 65536, 0, 393235, 65536, 0, 458771, 65536, 0, 524307, 65536, 0, 589843, 65536, 0, 655379, 65536, 0, 720915, 65536, 0, 786451, 0, 0, 20, 0, 0, 65556, 0, 0, 131092, 0, 0, 196628, 0, 0, 262164, 0, 0, 327700, 0, 0, 393236, 0, 0, 458772, 0, 0, 524308, 0, 0, 589844, 0, 0, 655380, 0, 0, 720916, 0, 0, 786452, 0, 0, 21, 0, 0, 65557, 0, 0, 131093, 0, 0, 196629, 0, 0, 262165, 0, 0, 327701, 0, 0, 393237, 0, 0, 458773, 0, 0, 524309, 0, 0, 589845, 0, 0, 655381, 0, 0, 720917, 0, 0, 786453, 0, 0)
@@ -266,6 +266,11 @@ position = Vector2(840, 456)
[node name="Players" type="Node2D" parent="."]
[node name="ColorRect" type="ColorRect" parent="."]
offset_right = 1056.0
offset_bottom = 48.0
color = Color(0, 0, 0, 0.501961)
[node name="Score" type="HBoxContainer" parent="."]
offset_right = 1024.0
offset_bottom = 40.0
@@ -298,7 +303,6 @@ text = "EXIT GAME"
[node name="Camera2D" type="Camera2D" parent="."]
offset = Vector2(512, 300)
current = true
[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("res://player.tscn")

View File

@@ -1,14 +1,14 @@
extends Area2D
const DEFAULT_SPEED = 100
const DEFAULT_SPEED = 100.0
var direction = Vector2.LEFT
var stopped = false
var _speed = DEFAULT_SPEED
var direction := Vector2.LEFT
var stopped := false
var _speed := DEFAULT_SPEED
@onready var _screen_size = get_viewport_rect().size
@onready var _screen_size := get_viewport_rect().size
func _process(delta):
func _process(delta: float) -> void:
_speed += delta
# Ball will move normally for both players,
# even if it's sightly out of sync between them,
@@ -17,24 +17,24 @@ func _process(delta):
translate(_speed * delta * direction)
# Check screen bounds to make ball bounce.
var ball_pos = position
var ball_pos := position
if (ball_pos.y < 0 and direction.y < 0) or (ball_pos.y > _screen_size.y and direction.y > 0):
direction.y = -direction.y
if is_multiplayer_authority():
# Only the master will decide when the ball is out in
# the left side (it's own side). This makes the game
# the left side (its own side). This makes the game
# playable even if latency is high and ball is going
# fast. Otherwise ball might be out in the other
# fast. Otherwise, the ball might be out in the other
# player's screen but not this one.
if ball_pos.x < 0:
get_parent().update_score.rpc(false)
_reset_ball.rpc(false)
else:
# Only the puppet will decide when the ball is out in
# the right side, which is it's own side. This makes
# the right side, which is its own side. This makes
# the game playable even if latency is high and ball
# is going fast. Otherwise ball might be out in the
# is going fast. Otherwise, the ball might be out in the
# other player's screen but not this one.
if ball_pos.x > _screen_size.x:
get_parent().update_score.rpc(true)
@@ -42,7 +42,7 @@ func _process(delta):
@rpc("any_peer", "call_local")
func bounce(left, random):
func bounce(left: bool, random: float) -> void:
# Using sync because both players can make it bounce.
if left:
direction.x = abs(direction.x)
@@ -55,12 +55,12 @@ func bounce(left, random):
@rpc("any_peer", "call_local")
func stop():
func stop() -> void:
stopped = true
@rpc("any_peer", "call_local")
func _reset_ball(for_left):
func _reset_ball(for_left: float) -> void:
position = _screen_size / 2
if for_left:
direction = Vector2.LEFT

View File

@@ -1,21 +1,21 @@
extends Control
# Default game server port. Can be any number between 1024 and 49151.
# Not present on the list of registered or common ports as of December 2022:
# Not present on the list of registered or common ports as of May 2024:
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
const DEFAULT_PORT = 8910
@onready var address = $Address
@onready var host_button = $HostButton
@onready var join_button = $JoinButton
@onready var status_ok = $StatusOk
@onready var status_fail = $StatusFail
@onready var port_forward_label = $PortForward
@onready var find_public_ip_button = $FindPublicIP
@onready var address: LineEdit = $Address
@onready var host_button: Button = $HostButton
@onready var join_button: Button = $JoinButton
@onready var status_ok: Label = $StatusOk
@onready var status_fail: Label = $StatusFail
@onready var port_forward_label: Label = $PortForward
@onready var find_public_ip_button: LinkButton = $FindPublicIP
var peer = null
var peer: ENetMultiplayerPeer
func _ready():
func _ready() -> void:
# Connect all the callbacks related to networking.
multiplayer.peer_connected.connect(_player_connected)
multiplayer.peer_disconnected.connect(_player_disconnected)
@@ -23,12 +23,11 @@ func _ready():
multiplayer.connection_failed.connect(_connected_fail)
multiplayer.server_disconnected.connect(_server_disconnected)
#### Network callbacks from SceneTree ####
#region Network callbacks from SceneTree
# Callback from SceneTree.
func _player_connected(_id):
func _player_connected(_id: int) -> void:
# Someone connected, start the game!
var pong = load("res://pong.tscn").instantiate()
var pong: Node2D = load("res://pong.tscn").instantiate()
# Connect deferred so we can safely erase it from the callback.
pong.game_finished.connect(_end_game, CONNECT_DEFERRED)
@@ -36,49 +35,49 @@ func _player_connected(_id):
hide()
func _player_disconnected(_id):
func _player_disconnected(_id: int) -> void:
if multiplayer.is_server():
_end_game("Client disconnected")
_end_game("Client disconnected.")
else:
_end_game("Server disconnected")
_end_game("Server disconnected.")
# Callback from SceneTree, only for clients (not server).
func _connected_ok():
func _connected_ok() -> void:
pass # This function is not needed for this project.
# Callback from SceneTree, only for clients (not server).
func _connected_fail():
func _connected_fail() -> void:
_set_status("Couldn't connect.", false)
multiplayer.set_multiplayer_peer(null) # Remove peer.
multiplayer.set_multiplayer_peer(null) # Remove peer.
host_button.set_disabled(false)
join_button.set_disabled(false)
func _server_disconnected():
func _server_disconnected() -> void:
_end_game("Server disconnected.")
#endregion
##### Game creation functions ######
func _end_game(with_error = ""):
#region Game creation methods
func _end_game(with_error: String = "") -> void:
if has_node("/root/Pong"):
# Erase immediately, otherwise network might show
# errors (this is why we connected deferred above).
get_node(^"/root/Pong").free()
show()
multiplayer.set_multiplayer_peer(null) # Remove peer.
multiplayer.set_multiplayer_peer(null) # Remove peer.
host_button.set_disabled(false)
join_button.set_disabled(false)
_set_status(with_error, false)
func _set_status(text, isok):
func _set_status(text: String, is_ok: bool) -> void:
# Simple way to show status.
if isok:
if is_ok:
status_ok.set_text(text)
status_fail.set_text("")
else:
@@ -86,9 +85,10 @@ func _set_status(text, isok):
status_fail.set_text(text)
func _on_host_pressed():
func _on_host_pressed() -> void:
peer = ENetMultiplayerPeer.new()
var err = peer.create_server(DEFAULT_PORT, 1) # Maximum of 1 peer, since it's a 2-player game.
# Set a maximum of 1 peer, since Pong is a 2-player game.
var err := peer.create_server(DEFAULT_PORT, 1)
if err != OK:
# Is another server running?
_set_status("Can't host, address in use.",false)
@@ -99,14 +99,15 @@ func _on_host_pressed():
host_button.set_disabled(true)
join_button.set_disabled(true)
_set_status("Waiting for player...", true)
get_window().title = ProjectSettings.get_setting("application/config/name") + ": Server"
# Only show hosting instructions when relevant.
port_forward_label.visible = true
find_public_ip_button.visible = true
func _on_join_pressed():
var ip = address.get_text()
func _on_join_pressed() -> void:
var ip := address.get_text()
if not ip.is_valid_ip_address():
_set_status("IP address is invalid.", false)
return
@@ -117,7 +118,8 @@ func _on_join_pressed():
multiplayer.set_multiplayer_peer(peer)
_set_status("Connecting...", true)
get_window().title = ProjectSettings.get_setting("application/config/name") + ": Client"
#endregion
func _on_find_public_ip_pressed():
func _on_find_public_ip_pressed() -> void:
OS.shell_open("https://icanhazip.com/")

View File

@@ -2,14 +2,14 @@ extends Area2D
const MOTION_SPEED = 150
@export var left = false
@export var left := false
var _motion = 0
var _you_hidden = false
var _motion := 0.0
var _you_hidden := false
@onready var _screen_size_y = get_viewport_rect().size.y
@onready var _screen_size_y := get_viewport_rect().size.y
func _process(delta):
func _process(delta: float) -> void:
# Is the master of the paddle.
if is_multiplayer_authority():
_motion = Input.get_axis(&"move_up", &"move_down")
@@ -26,25 +26,25 @@ func _process(delta):
if not _you_hidden:
_hide_you_label()
translate(Vector2(0, _motion * delta))
translate(Vector2(0.0, _motion * delta))
# Set screen limits.
position.y = clamp(position.y, 16, _screen_size_y - 16)
position.y = clampf(position.y, 16, _screen_size_y - 16)
# Synchronize position and speed to the other peers.
@rpc("unreliable")
func set_pos_and_motion(pos, motion):
func set_pos_and_motion(pos: Vector2, motion: float) -> void:
position = pos
_motion = motion
func _hide_you_label():
func _hide_you_label() -> void:
_you_hidden = true
get_node(^"You").hide()
$You.hide()
func _on_paddle_area_enter(area):
func _on_paddle_area_enter(area: Area2D) -> void:
if is_multiplayer_authority():
# Random for new direction generated checked each peer.
area.bounce.rpc(left, randf())

View File

@@ -4,16 +4,16 @@ signal game_finished()
const SCORE_TO_WIN = 10
var score_left = 0
var score_right = 0
var score_left := 0
var score_right := 0
@onready var player2 = $Player2
@onready var score_left_node = $ScoreLeft
@onready var score_right_node = $ScoreRight
@onready var winner_left = $WinnerLeft
@onready var winner_right = $WinnerRight
@onready var player2: Area2D = $Player2
@onready var score_left_node: Label = $ScoreLeft
@onready var score_right_node: Label = $ScoreRight
@onready var winner_left: Label = $WinnerLeft
@onready var winner_right: Label = $WinnerRight
func _ready():
func _ready() -> void:
# By default, all nodes in server inherit from master,
# while all nodes in clients inherit from puppet.
# set_multiplayer_authority is tree-recursive by default.
@@ -28,7 +28,7 @@ func _ready():
@rpc("any_peer", "call_local")
func update_score(add_to_left):
func update_score(add_to_left: int) -> void:
if add_to_left:
score_left += 1
score_left_node.set_text(str(score_left))
@@ -36,7 +36,7 @@ func update_score(add_to_left):
score_right += 1
score_right_node.set_text(str(score_right))
var game_ended = false
var game_ended := false
if score_left == SCORE_TO_WIN:
winner_left.show()
game_ended = true
@@ -49,5 +49,5 @@ func update_score(add_to_left):
$Ball.stop.rpc()
func _on_exit_game_pressed():
func _on_exit_game_pressed() -> void:
game_finished.emit()

View File

@@ -81,7 +81,6 @@ text = "Exit Game"
[node name="Camera2D" type="Camera2D" parent="."]
offset = Vector2(320, 200)
current = true
[connection signal="pressed" from="ExitGame" to="." method="_on_exit_game_pressed"]

View File

@@ -19,35 +19,38 @@ run/main_scene="res://lobby.tscn"
config/features=PackedStringArray("4.2")
config/icon="res://icon.webp"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/size/viewport_width=640
window/size/viewport_height=400
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/stretch/scale_mode="integer"
[input]
move_down={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":16777234,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":90,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
move_up={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":16777232,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":122,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null)
]
}
move_down={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"echo":false,"script":null)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null)
]
}
[rendering]
textures/canvas_textures/default_texture_filter=0
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"

View File

@@ -1,31 +1,32 @@
# A local signaling server. Add this to autoloads with name "Signaling" (/root/Signaling)
extends Node
# We will store the two peers here
var peers = []
# We will store the two peers here.
var peers: Array[String] = []
func register(path):
func register(path: String) -> void:
assert(peers.size() < 2)
peers.append(path)
peers.push_back(path)
if peers.size() == 2:
get_node(peers[0]).peer.create_offer()
func _find_other(path):
func _find_other(path: String) -> String:
# Find the other registered peer.
for p in peers:
if p != path:
return p
return ""
func send_session(path, type, sdp):
var other = _find_other(path)
assert(other != "")
func send_session(path: String, type: String, sdp: String) -> void:
var other := _find_other(path)
assert(not other.is_empty())
get_node(other).peer.set_remote_description(type, sdp)
func send_candidate(path, mid, index, sdp):
var other = _find_other(path)
assert(other != "")
get_node(other).peer.add_ice_candidate(mid, index, sdp)
func send_candidate(path: String, media: String, index: int, sdp: String) -> void:
var other := _find_other(path)
assert(not other.is_empty())
get_node(other).peer.add_ice_candidate(media, index, sdp)

View File

@@ -1,12 +1,12 @@
extends Node
# An example p2p chat client.
# An example peer-to-peer chat client.
var peer = WebRTCPeerConnection.new()
var peer := WebRTCPeerConnection.new()
# Create negotiated data channel.
var channel = peer.create_data_channel("chat", {"negotiated": true, "id": 1})
func _ready():
func _ready() -> void:
# Connect all functions.
peer.ice_candidate_created.connect(_on_ice_candidate)
peer.session_description_created.connect(_on_session)
@@ -15,19 +15,19 @@ func _ready():
Signaling.register(String(get_path()))
func _on_ice_candidate(mid, index, sdp):
# Send the ICE candidate to the other peer via signaling server.
Signaling.send_candidate(String(get_path()), mid, index, sdp)
func _on_ice_candidate(media: String, index: int, sdp: String) -> void:
# Send the ICE candidate to the other peer via the signaling server.
Signaling.send_candidate(String(get_path()), media, index, sdp)
func _on_session(type, sdp):
# Send the session to other peer via signaling server.
func _on_session(type: String, sdp: String) -> void:
# Send the session to other peer via the signaling server.
Signaling.send_session(String(get_path()), type, sdp)
# Set generated description as local.
peer.set_local_description(type, sdp)
func _process(delta):
func _process(delta: float) -> void:
# Always poll the connection frequently.
peer.poll()
if channel.get_ready_state() == WebRTCDataChannel.STATE_OPEN:
@@ -35,5 +35,5 @@ func _process(delta):
print(String(get_path()), " received: ", channel.get_packet().get_string_from_utf8())
func send_message(message):
func send_message(message: String) -> void:
channel.put_packet(message.to_utf8_buffer())

View File

@@ -1,4 +1,4 @@
extends LinkButton
func _on_LinkButton_pressed():
func _on_LinkButton_pressed() -> void:
OS.shell_open("https://github.com/godotengine/webrtc-native/releases")

View File

@@ -2,16 +2,16 @@ extends Node
const Chat = preload("res://chat.gd")
func _ready():
var p1 = Chat.new()
var p2 = Chat.new()
func _ready() -> void:
var p1 := Chat.new()
var p2 := Chat.new()
add_child(p1)
add_child(p2)
# Wait a second and send message from P1
await get_tree().create_timer(1).timeout
# Wait a second and send message from P1.
await get_tree().create_timer(1.0).timeout
p1.send_message("Hi from %s" % String(p1.get_path()))
# Wait a second and send message from P2
await get_tree().create_timer(1).timeout
# Wait a second and send message from P2.
await get_tree().create_timer(1.0).timeout
p2.send_message("Hi from %s" % String(p2.get_path()))

View File

@@ -2,13 +2,13 @@ extends Node
# Main scene.
# Create the two peers.
var p1 = WebRTCPeerConnection.new()
var p2 = WebRTCPeerConnection.new()
var ch1 = p1.create_data_channel("chat", {"id": 1, "negotiated": true})
var ch2 = p2.create_data_channel("chat", {"id": 1, "negotiated": true})
var p1 := WebRTCPeerConnection.new()
var p2 := WebRTCPeerConnection.new()
var ch1 := p1.create_data_channel("chat", { "id": 1, "negotiated": true })
var ch2 := p2.create_data_channel("chat", { "id": 1, "negotiated": true })
func _ready():
print(p1.create_data_channel("chat", {"id": 1, "negotiated": true}))
func _ready() -> void:
print(p1.create_data_channel("chat", { "id": 1, "negotiated": true }))
# Connect P1 session created to itself to set local description.
p1.session_description_created.connect(p1.set_local_description)
# Connect P1 session and ICE created to p2 set remote description and candidates.
@@ -32,7 +32,7 @@ func _ready():
ch2.put_packet("Hi from P2".to_utf8_buffer())
func _process(delta):
func _process(delta: float) -> void:
p1.poll()
p2.poll()
if ch1.get_ready_state() == ch1.STATE_OPEN and ch1.get_available_packet_count() > 0:

View File

@@ -20,6 +20,10 @@ config/features=PackedStringArray("4.2")
Signaling="*res://Signaling.gd"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"

View File

@@ -1,9 +1,9 @@
extends "ws_webrtc_client.gd"
var rtc_mp: WebRTCMultiplayerPeer = WebRTCMultiplayerPeer.new()
var rtc_mp := WebRTCMultiplayerPeer.new()
var sealed := false
func _init():
func _init() -> void:
connected.connect(_connected)
disconnected.connect(_disconnected)
@@ -17,7 +17,7 @@ func _init():
peer_disconnected.connect(_peer_disconnected)
func start(url, _lobby = "", _mesh := true):
func start(url: String, _lobby: String = "", _mesh: bool = true) -> void:
stop()
sealed = false
mesh = _mesh
@@ -25,30 +25,35 @@ func start(url, _lobby = "", _mesh := true):
connect_to_url(url)
func stop():
func stop() -> void:
multiplayer.multiplayer_peer = null
rtc_mp.close()
close()
func _create_peer(id):
func _create_peer(id: int) -> WebRTCPeerConnection:
var peer: WebRTCPeerConnection = WebRTCPeerConnection.new()
# Use a public STUN server for moderate NAT traversal.
# Note that STUN cannot punch through strict NATs (such as most mobile connections),
# in which case TURN is required. TURN generally does not have public servers available,
# as it requires much greater resources to host (all traffic goes through
# the TURN server, instead of only performing the initial connection).
peer.initialize({
"iceServers": [ { "urls": ["stun:stun.l.google.com:19302"] } ]
})
peer.session_description_created.connect(_offer_created.bind(id))
peer.ice_candidate_created.connect(_new_ice_candidate.bind(id))
rtc_mp.add_peer(peer, id)
if id < rtc_mp.get_unique_id(): # So lobby creator never creates offers.
if id < rtc_mp.get_unique_id(): # So lobby creator never creates offers.
peer.create_offer()
return peer
func _new_ice_candidate(mid_name, index_name, sdp_name, id):
func _new_ice_candidate(mid_name: String, index_name: int, sdp_name: String, id: int) -> void:
send_candidate(id, mid_name, index_name, sdp_name)
func _offer_created(type, data, id):
func _offer_created(type: String, data: String, id: int) -> void:
if not rtc_mp.has_peer(id):
return
print("created", type)
@@ -57,7 +62,7 @@ func _offer_created(type, data, id):
else: send_answer(id, data)
func _connected(id, use_mesh):
func _connected(id: int, use_mesh: bool) -> void:
print("Connected %d, mesh: %s" % [id, use_mesh])
if use_mesh:
rtc_mp.create_mesh(id)
@@ -68,41 +73,42 @@ func _connected(id, use_mesh):
multiplayer.multiplayer_peer = rtc_mp
func _lobby_joined(_lobby):
func _lobby_joined(_lobby: String) -> void:
lobby = _lobby
func _lobby_sealed():
func _lobby_sealed() -> void:
sealed = true
func _disconnected():
func _disconnected() -> void:
print("Disconnected: %d: %s" % [code, reason])
if not sealed:
stop() # Unexpected disconnect
func _peer_connected(id):
print("Peer connected %d" % id)
func _peer_connected(id: int) -> void:
print("Peer connected: %d" % id)
_create_peer(id)
func _peer_disconnected(id):
if rtc_mp.has_peer(id): rtc_mp.remove_peer(id)
func _peer_disconnected(id: int) -> void:
if rtc_mp.has_peer(id):
rtc_mp.remove_peer(id)
func _offer_received(id, offer):
func _offer_received(id: int, offer: int) -> void:
print("Got offer: %d" % id)
if rtc_mp.has_peer(id):
rtc_mp.get_peer(id).connection.set_remote_description("offer", offer)
func _answer_received(id, answer):
func _answer_received(id: int, answer: int) -> void:
print("Got answer: %d" % id)
if rtc_mp.has_peer(id):
rtc_mp.get_peer(id).connection.set_remote_description("answer", answer)
func _candidate_received(id, mid, index, sdp):
func _candidate_received(id: int, mid: String, index: int, sdp: String) -> void:
if rtc_mp.has_peer(id):
rtc_mp.get_peer(id).connection.add_ice_candidate(mid, index, sdp)

View File

@@ -1,41 +1,50 @@
extends Node
enum Message {JOIN, ID, PEER_CONNECT, PEER_DISCONNECT, OFFER, ANSWER, CANDIDATE, SEAL}
enum Message {
JOIN,
ID,
PEER_CONNECT,
PEER_DISCONNECT,
OFFER,
ANSWER,
CANDIDATE,
SEAL,
}
@export var autojoin := true
@export var lobby := "" # Will create a new lobby if empty.
@export var mesh := true # Will use the lobby host as relay otherwise.
@export var lobby := "" # Will create a new lobby if empty.
@export var mesh := true # Will use the lobby host as relay otherwise.
var ws: WebSocketPeer = WebSocketPeer.new()
var code = 1000
var reason = "Unknown"
var old_state = WebSocketPeer.STATE_CLOSED
var ws := WebSocketPeer.new()
var code := 1000
var reason := "Unknown"
var old_state := WebSocketPeer.STATE_CLOSED
signal lobby_joined(lobby)
signal connected(id, use_mesh)
signal lobby_joined(lobby: String)
signal connected(id: int, use_mesh: bool)
signal disconnected()
signal peer_connected(id)
signal peer_disconnected(id)
signal offer_received(id, offer)
signal answer_received(id, answer)
signal candidate_received(id, mid, index, sdp)
signal peer_connected(id: int)
signal peer_disconnected(id: int)
signal offer_received(id: int, offer: int)
signal answer_received(id: int, answer: int)
signal candidate_received(id: int, mid: String, index: int, sdp: String)
signal lobby_sealed()
func connect_to_url(url):
func connect_to_url(url: String) -> void:
close()
code = 1000
reason = "Unknown"
ws.connect_to_url(url)
func close():
func close() -> void:
ws.close()
func _process(delta):
func _process(_delta: float) -> void:
ws.poll()
var state = ws.get_ready_state()
var state := ws.get_ready_state()
if state != old_state and state == WebSocketPeer.STATE_OPEN and autojoin:
join_lobby(lobby)
while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count():
@@ -48,8 +57,8 @@ func _process(delta):
old_state = state
func _parse_msg():
var parsed = JSON.parse_string(ws.get_packet().get_string_from_utf8())
func _parse_msg() -> bool:
var parsed: Dictionary = JSON.parse_string(ws.get_packet().get_string_from_utf8())
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("type") or not parsed.has("id") or \
typeof(parsed.get("data")) != TYPE_STRING:
return false
@@ -68,19 +77,19 @@ func _parse_msg():
elif type == Message.SEAL:
lobby_sealed.emit()
elif type == Message.PEER_CONNECT:
# Client connected
# Client connected.
peer_connected.emit(src_id)
elif type == Message.PEER_DISCONNECT:
# Client connected
# Client connected.
peer_disconnected.emit(src_id)
elif type == Message.OFFER:
# Offer received
# Offer received.
offer_received.emit(src_id, msg.data)
elif type == Message.ANSWER:
# Answer received
# Answer received.
answer_received.emit(src_id, msg.data)
elif type == Message.CANDIDATE:
# Candidate received
# Candidate received.
var candidate: PackedStringArray = msg.data.split("\n", false)
if candidate.size() != 3:
return false
@@ -89,32 +98,33 @@ func _parse_msg():
candidate_received.emit(src_id, candidate[0], candidate[1].to_int(), candidate[2])
else:
return false
return true # Parsed
return true # Parsed.
func join_lobby(lobby: String):
func join_lobby(lobby: String) -> Error:
return _send_msg(Message.JOIN, 0 if mesh else 1, lobby)
func seal_lobby():
func seal_lobby() -> Error:
return _send_msg(Message.SEAL, 0)
func send_candidate(id, mid, index, sdp) -> int:
func send_candidate(id: int, mid: String, index: int, sdp: String) -> Error:
return _send_msg(Message.CANDIDATE, id, "\n%s\n%d\n%s" % [mid, index, sdp])
func send_offer(id, offer) -> int:
func send_offer(id: int, offer: String) -> Error:
return _send_msg(Message.OFFER, id, offer)
func send_answer(id, answer) -> int:
func send_answer(id: int, answer: String) -> Error:
return _send_msg(Message.ANSWER, id, answer)
func _send_msg(type: int, id: int, data:="") -> int:
func _send_msg(type: int, id: int, data: String = "") -> Error:
return ws.send_text(JSON.stringify({
"type": type,
"id": id,
"data": data
"data": data,
}))

View File

@@ -1,11 +1,11 @@
extends Control
@onready var client = $Client
@onready var host = $VBoxContainer/Connect/Host
@onready var room = $VBoxContainer/Connect/RoomSecret
@onready var mesh = $VBoxContainer/Connect/Mesh
@onready var client: Node = $Client
@onready var host: LineEdit = $VBoxContainer/Connect/Host
@onready var room: LineEdit = $VBoxContainer/Connect/RoomSecret
@onready var mesh: CheckBox = $VBoxContainer/Connect/Mesh
func _ready():
func _ready() -> void:
client.lobby_joined.connect(_lobby_joined)
client.lobby_sealed.connect(_lobby_sealed)
client.connected.connect(_connected)
@@ -19,62 +19,62 @@ func _ready():
@rpc("any_peer", "call_local")
func ping(argument):
func ping(argument: String) -> void:
_log("[Multiplayer] Ping from peer %d: arg: %s" % [multiplayer.get_remote_sender_id(), argument])
func _mp_server_connected():
func _mp_server_connected() -> void:
_log("[Multiplayer] Server connected (I am %d)" % client.rtc_mp.get_unique_id())
func _mp_server_disconnect():
func _mp_server_disconnect() -> void:
_log("[Multiplayer] Server disconnected (I am %d)" % client.rtc_mp.get_unique_id())
func _mp_peer_connected(id: int):
func _mp_peer_connected(id: int) -> void:
_log("[Multiplayer] Peer %d connected" % id)
func _mp_peer_disconnected(id: int):
func _mp_peer_disconnected(id: int) -> void:
_log("[Multiplayer] Peer %d disconnected" % id)
func _connected(id, use_mesh):
func _connected(id: int, use_mesh: bool) -> void:
_log("[Signaling] Server connected with ID: %d. Mesh: %s" % [id, use_mesh])
func _disconnected():
func _disconnected() -> void:
_log("[Signaling] Server disconnected: %d - %s" % [client.code, client.reason])
func _lobby_joined(lobby):
func _lobby_joined(lobby: String) -> void:
_log("[Signaling] Joined lobby %s" % lobby)
func _lobby_sealed():
func _lobby_sealed() -> void:
_log("[Signaling] Lobby has been sealed")
func _log(msg):
func _log(msg: String) -> void:
print(msg)
$VBoxContainer/TextEdit.text += str(msg) + "\n"
func _on_peers_pressed():
_log(multiplayer.get_peers())
func _on_peers_pressed() -> void:
_log(str(multiplayer.get_peers()))
func _on_ping_pressed():
func _on_ping_pressed() -> void:
ping.rpc(randf())
func _on_seal_pressed():
func _on_seal_pressed() -> void:
client.seal_lobby()
func _on_start_pressed():
func _on_start_pressed() -> void:
client.start(host.text, room.text, mesh.button_pressed)
func _on_stop_pressed():
func _on_stop_pressed() -> void:
client.stop()

View File

@@ -25,92 +25,55 @@ grow_vertical = 2
[node name="Connect" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
offset_right = 1024.0
offset_bottom = 31.0
[node name="Label" type="Label" parent="VBoxContainer/Connect"]
layout_mode = 2
offset_top = 2.0
offset_right = 89.0
offset_bottom = 28.0
text = "Connect to:"
[node name="Host" type="LineEdit" parent="VBoxContainer/Connect"]
layout_mode = 2
offset_left = 93.0
offset_right = 829.0
offset_bottom = 31.0
size_flags_horizontal = 3
text = "ws://localhost:9080"
[node name="Room" type="Label" parent="VBoxContainer/Connect"]
layout_mode = 2
offset_left = 833.0
offset_right = 879.0
offset_bottom = 31.0
size_flags_vertical = 5
text = "Room"
[node name="RoomSecret" type="LineEdit" parent="VBoxContainer/Connect"]
layout_mode = 2
offset_left = 883.0
offset_right = 950.0
offset_bottom = 31.0
placeholder_text = "secret"
[node name="Mesh" type="CheckBox" parent="VBoxContainer/Connect"]
layout_mode = 2
offset_left = 954.0
offset_right = 1024.0
offset_bottom = 31.0
button_pressed = true
text = "Mesh"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
offset_top = 35.0
offset_right = 1024.0
offset_bottom = 66.0
[node name="Start" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
offset_right = 46.0
offset_bottom = 31.0
text = "Start"
[node name="Stop" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
offset_left = 50.0
offset_right = 93.0
offset_bottom = 31.0
text = "Stop"
[node name="Seal" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
offset_left = 97.0
offset_right = 137.0
offset_bottom = 31.0
text = "Seal"
[node name="Ping" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
offset_left = 141.0
offset_right = 183.0
offset_bottom = 31.0
text = "Ping"
[node name="Peers" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
offset_left = 187.0
offset_right = 280.0
offset_bottom = 31.0
text = "Print peers"
[node name="TextEdit" type="TextEdit" parent="VBoxContainer"]
layout_mode = 2
offset_top = 70.0
offset_right = 1024.0
offset_bottom = 600.0
size_flags_vertical = 3
[connection signal="pressed" from="VBoxContainer/HBoxContainer/Start" to="." method="_on_start_pressed"]

View File

@@ -1,24 +1,25 @@
extends Control
func _enter_tree():
func _enter_tree() -> void:
for c in $VBoxContainer/Clients.get_children():
# So each child gets its own separate MultiplayerAPI.
get_tree().set_multiplayer(
MultiplayerAPI.create_default_interface(),
NodePath("%s/VBoxContainer/Clients/%s" % [get_path(), c.name])
MultiplayerAPI.create_default_interface(),
NodePath("%s/VBoxContainer/Clients/%s" % [get_path(), c.name])
)
func _ready():
func _ready() -> void:
if OS.get_name() == "Web":
$VBoxContainer/Signaling.hide()
func _on_listen_toggled(button_pressed):
func _on_listen_toggled(button_pressed: bool) -> void:
if button_pressed:
$Server.listen(int($VBoxContainer/Signaling/Port.value))
else:
$Server.stop()
func _on_LinkButton_pressed():
func _on_LinkButton_pressed() -> void:
OS.shell_open("https://github.com/godotengine/webrtc-native/releases")

View File

@@ -22,6 +22,7 @@ config/features=PackedStringArray("4.2")
[debug]
gdscript/warnings/shadowed_variable=false
gdscript/warnings/untyped_declaration=1
gdscript/warnings/unused_argument=false
[display]

View File

@@ -1,12 +1,26 @@
extends Node
enum Message {JOIN, ID, PEER_CONNECT, PEER_DISCONNECT, OFFER, ANSWER, CANDIDATE, SEAL}
enum Message {
JOIN,
ID,
PEER_CONNECT,
PEER_DISCONNECT,
OFFER,
ANSWER,
CANDIDATE,
SEAL,
}
const TIMEOUT = 1000 # Unresponsive clients times out after 1 sec
const SEAL_TIME = 10000 # A sealed room will be closed after this time
## Unresponsive clients time out after this time (in milliseconds).
const TIMEOUT = 1000
## A sealed room will be closed after this time (in milliseconds).
const SEAL_TIME = 10000
## All alphanumeric characters.
const ALFNUM = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
var _alfnum = ALFNUM.to_ascii_buffer()
var _alfnum := ALFNUM.to_ascii_buffer()
var rand: RandomNumberGenerator = RandomNumberGenerator.new()
var lobbies: Dictionary = {}
@@ -14,13 +28,13 @@ var tcp_server := TCPServer.new()
var peers: Dictionary = {}
class Peer extends RefCounted:
var id = -1
var lobby = ""
var time = Time.get_ticks_msec()
var ws = WebSocketPeer.new()
var id := -1
var lobby := ""
var time := Time.get_ticks_msec()
var ws := WebSocketPeer.new()
func _init(peer_id, tcp):
func _init(peer_id: int, tcp: StreamPeer) -> void:
id = peer_id
ws.accept_stream(tcp)
@@ -29,7 +43,7 @@ class Peer extends RefCounted:
return ws.get_ready_state() == WebSocketPeer.STATE_OPEN
func send(type: int, id: int, data:=""):
func send(type: int, id: int, data: String = "") -> void:
return ws.send_text(JSON.stringify({
"type": type,
"id": id,
@@ -38,13 +52,13 @@ class Peer extends RefCounted:
class Lobby extends RefCounted:
var peers: = {}
var host: int = -1
var sealed: bool = false
var time = 0
var peers := {}
var host := -1
var sealed := false
var time := 0 # Value is in milliseconds.
var mesh := true
func _init(host_id: int, use_mesh: bool):
func _init(host_id: int, use_mesh: bool) -> void:
host = host_id
mesh = use_mesh
@@ -52,7 +66,7 @@ class Lobby extends RefCounted:
if sealed: return false
if not peer.is_ws_open(): return false
peer.send(Message.ID, (1 if peer.id == host else peer.id), "true" if mesh else "")
for p in peers.values():
for p: Peer in peers.values():
if not p.is_ws_open():
continue
if not mesh and p.id != host:
@@ -65,15 +79,19 @@ class Lobby extends RefCounted:
func leave(peer: Peer) -> bool:
if not peers.has(peer.id): return false
if not peers.has(peer.id):
return false
peers.erase(peer.id)
var close = false
var close := false
if peer.id == host:
# The room host disconnected, will disconnect all peers.
close = true
if sealed: return close
if sealed:
return close
# Notify other peers.
for p in peers.values():
for p: Peer in peers.values():
if not p.is_ws_open():
continue
if close:
@@ -82,53 +100,59 @@ class Lobby extends RefCounted:
else:
# Notify disconnection.
p.send(Message.PEER_DISCONNECT, peer.id)
return close
func seal(peer_id: int) -> bool:
# Only host can seal the room.
if host != peer_id: return false
if host != peer_id:
return false
sealed = true
for p in peers.values():
for p: Peer in peers.values():
if not p.is_ws_open():
continue
p.send(Message.SEAL, 0)
time = Time.get_ticks_msec()
peers.clear()
return true
func _process(delta):
func _process(_delta: float) -> void:
poll()
func listen(port):
if OS.get_name() == "Web":
func listen(port: int) -> void:
if OS.has_feature("web"):
OS.alert("Cannot create WebSocket servers in Web exports due to browsers' limitations.")
return
stop()
rand.seed = Time.get_unix_time_from_system()
rand.seed = int(Time.get_unix_time_from_system())
tcp_server.listen(port)
func stop():
func stop() -> void:
tcp_server.stop()
peers.clear()
func poll():
func poll() -> void:
if not tcp_server.is_listening():
return
if tcp_server.is_connection_available():
var id = randi() % (1 << 31)
var id := randi() % (1 << 31)
peers[id] = Peer.new(id, tcp_server.take_connection())
# Poll peers.
var to_remove := []
for p in peers.values():
for p: Peer in peers.values():
# Peers timeout.
if p.lobby == "" and Time.get_ticks_msec() - p.time > TIMEOUT:
if p.lobby.is_empty() and Time.get_ticks_msec() - p.time > TIMEOUT:
p.ws.close()
p.ws.poll()
while p.is_ws_open() and p.ws.get_available_packet_count():
@@ -137,7 +161,7 @@ func poll():
to_remove.push_back(p.id)
p.ws.close()
break
var state = p.ws.get_ready_state()
var state := p.ws.get_ready_state()
if state == WebSocketPeer.STATE_CLOSED:
print("Peer %d disconnected from lobby: '%s'" % [p.id, p.lobby])
# Remove from lobby (and lobby itself if host).
@@ -148,24 +172,24 @@ func poll():
to_remove.push_back(p.id)
# Lobby seal.
for k in lobbies:
for k: String in lobbies:
if not lobbies[k].sealed:
continue
if lobbies[k].time + SEAL_TIME < Time.get_ticks_msec():
# Close lobby.
for p in lobbies[k].peers:
for p: Peer in lobbies[k].peers:
p.ws.close()
to_remove.push_back(p.id)
# Remove stale peers
for id in to_remove:
for id: int in to_remove:
peers.erase(id)
func _join_lobby(peer: Peer, lobby: String, mesh: bool) -> bool:
if lobby == "":
for _i in range(0, 32):
lobby += char(_alfnum[rand.randi_range(0, ALFNUM.length()-1)])
if lobby.is_empty():
for _i in 32:
lobby += char(_alfnum[rand.randi_range(0, ALFNUM.length() - 1)])
lobbies[lobby] = Lobby.new(peer.id, mesh)
elif not lobbies.has(lobby):
return false
@@ -179,7 +203,7 @@ func _join_lobby(peer: Peer, lobby: String, mesh: bool) -> bool:
func _parse_msg(peer: Peer) -> bool:
var pkt_str: String = peer.ws.get_packet().get_string_from_utf8()
var parsed = JSON.parse_string(pkt_str)
var parsed: Dictionary = JSON.parse_string(pkt_str)
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("type") or not parsed.has("id") or \
typeof(parsed.get("data")) != TYPE_STRING:
return false
@@ -189,36 +213,37 @@ func _parse_msg(peer: Peer) -> bool:
var msg := {
"type": str(parsed.type).to_int(),
"id": str(parsed.id).to_int(),
"data": parsed.data
"data": parsed.data,
}
if msg.type == Message.JOIN:
if peer.lobby: # Peer must not have joined a lobby already!
if peer.lobby: # Peer must not have joined a lobby already!
return false
return _join_lobby(peer, msg.data, msg.id == 0)
if not lobbies.has(peer.lobby): # Lobby not found?
if not lobbies.has(peer.lobby): # Lobby not found?
return false
var lobby = lobbies[peer.lobby]
var lobby: Peer = lobbies[peer.lobby]
if msg.type == Message.SEAL:
# Client is sealing the room
# Client is sealing the room.
return lobby.seal(peer.id)
var dest_id: int = msg.id
if dest_id == MultiplayerPeer.TARGET_PEER_SERVER:
dest_id = lobby.host
if not peers.has(dest_id): # Destination ID not connected
if not peers.has(dest_id): # Destination ID not connected.
return false
if peers[dest_id].lobby != peer.lobby: # Trying to contact someone not in same lobby
if peers[dest_id].lobby != peer.lobby: # Trying to contact someone not in same lobby.
return false
if msg.type in [Message.OFFER, Message.ANSWER, Message.CANDIDATE]:
var source = MultiplayerPeer.TARGET_PEER_SERVER if peer.id == lobby.host else peer.id
var source := MultiplayerPeer.TARGET_PEER_SERVER if peer.id == lobby.host else peer.id
peers[dest_id].send(msg.type, source, msg.data)
return true
return false # Unknown message
return false # Unknown message.

View File

@@ -1,32 +1,32 @@
extends Control
@onready var _client: WebSocketClient = $WebSocketClient
@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit
@onready var _host = $Panel/VBoxContainer/Connect/Host
@onready var _log_dest: RichTextLabel = $Panel/VBoxContainer/RichTextLabel
@onready var _line_edit: LineEdit = $Panel/VBoxContainer/Send/LineEdit
@onready var _host: LineEdit = $Panel/VBoxContainer/Connect/Host
func info(msg):
func info(msg: String) -> void:
print(msg)
_log_dest.add_text(str(msg) + "\n")
# Client signals
func _on_web_socket_client_connection_closed():
var ws = _client.get_socket()
#region Client signals
func _on_web_socket_client_connection_closed() -> void:
var ws := _client.get_socket()
info("Client just disconnected with code: %s, reson: %s" % [ws.get_close_code(), ws.get_close_reason()])
func _on_web_socket_client_connected_to_server():
func _on_web_socket_client_connected_to_server() -> void:
info("Client just connected with protocol: %s" % _client.get_socket().get_selected_protocol())
func _on_web_socket_client_message_received(message):
func _on_web_socket_client_message_received(message: String) -> void:
info("%s" % message)
#endregion
# UI signals.
func _on_send_pressed():
if _line_edit.text == "":
#region UI signals
func _on_send_pressed() -> void:
if _line_edit.text.is_empty():
return
info("Sending message: %s" % [_line_edit.text])
@@ -34,14 +34,17 @@ func _on_send_pressed():
_line_edit.text = ""
func _on_connect_toggled(pressed):
func _on_connect_toggled(pressed: bool) -> void:
if not pressed:
_client.close()
return
if _host.text == "":
if _host.text.is_empty():
return
info("Connecting to host: %s." % [_host.text])
var err = _client.connect_to_url(_host.text)
var err := _client.connect_to_url(_host.text)
if err != OK:
info("Error connecting to host: %s" % [_host.text])
return
#endregion

View File

@@ -17,6 +17,10 @@ run/main_scene="res://combo.tscn"
config/features=PackedStringArray("4.2")
config/icon="res://icon.webp"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"

View File

@@ -1,35 +1,35 @@
extends Control
@onready var _server: WebSocketServer = $WebSocketServer
@onready var _log_dest = $Panel/VBoxContainer/RichTextLabel
@onready var _line_edit = $Panel/VBoxContainer/Send/LineEdit
@onready var _listen_port = $Panel/VBoxContainer/Connect/Port
@onready var _log_dest: RichTextLabel = $Panel/VBoxContainer/RichTextLabel
@onready var _line_edit: LineEdit = $Panel/VBoxContainer/Send/LineEdit
@onready var _listen_port: SpinBox = $Panel/VBoxContainer/Connect/Port
func info(msg):
func info(msg: String) -> void:
print(msg)
_log_dest.add_text(str(msg) + "\n")
# Server signals
func _on_web_socket_server_client_connected(peer_id):
#region Server signals
func _on_web_socket_server_client_connected(peer_id: int) -> void:
var peer: WebSocketPeer = _server.peers[peer_id]
info("Remote client connected: %d. Protocol: %s" % [peer_id, peer.get_selected_protocol()])
_server.send(-peer_id, "[%d] connected" % peer_id)
func _on_web_socket_server_client_disconnected(peer_id):
func _on_web_socket_server_client_disconnected(peer_id: int) -> void:
var peer: WebSocketPeer = _server.peers[peer_id]
info("Remote client disconnected: %d. Code: %d, Reason: %s" % [peer_id, peer.get_close_code(), peer.get_close_reason()])
_server.send(-peer_id, "[%d] disconnected" % peer_id)
func _on_web_socket_server_message_received(peer_id, message):
func _on_web_socket_server_message_received(peer_id: int, message: String) -> void:
info("Server received data from peer %d: %s" % [peer_id, message])
_server.send(-peer_id, "[%d] Says: %s" % [peer_id, message])
#endregion
# UI signals.
func _on_send_pressed():
#region UI signals
func _on_send_pressed() -> void:
if _line_edit.text == "":
return
@@ -38,14 +38,17 @@ func _on_send_pressed():
_line_edit.text = ""
func _on_listen_toggled(pressed):
func _on_listen_toggled(pressed: bool) -> void:
if not pressed:
_server.stop()
info("Server stopped")
return
var port = int(_listen_port.value)
var err = _server.listen(port)
var port := int(_listen_port.value)
var err := _server.listen(port)
if err != OK:
info("Error listing on port %s" % port)
return
info("Listing on port %s, supported protocols: %s" % [port, _server.supported_protocols])
#endregion

View File

@@ -1,31 +1,30 @@
extends Node
class_name WebSocketClient
class_name Node
extends WebSocketClient
@export var handshake_headers: PackedStringArray
@export var supported_protocols: PackedStringArray
var tls_options: TLSOptions = null
var socket = WebSocketPeer.new()
var last_state = WebSocketPeer.STATE_CLOSED
var socket := WebSocketPeer.new()
var last_state := WebSocketPeer.STATE_CLOSED
signal connected_to_server()
signal connection_closed()
signal message_received(message: Variant)
func connect_to_url(url) -> int:
func connect_to_url(url: String) -> int:
socket.supported_protocols = supported_protocols
socket.handshake_headers = handshake_headers
var err = socket.connect_to_url(url, tls_options)
var err := socket.connect_to_url(url, tls_options)
if err != OK:
return err
last_state = socket.get_ready_state()
return OK
func send(message) -> int:
func send(message: String) -> int:
if typeof(message) == TYPE_STRING:
return socket.send_text(message)
return socket.send(var_to_bytes(message))
@@ -34,13 +33,13 @@ func send(message) -> int:
func get_message() -> Variant:
if socket.get_available_packet_count() < 1:
return null
var pkt = socket.get_packet()
var pkt := socket.get_packet()
if socket.was_string_packet():
return pkt.get_string_from_utf8()
return bytes_to_var(pkt)
func close(code := 1000, reason := "") -> void:
func close(code: int = 1000, reason: String = "") -> void:
socket.close(code, reason)
last_state = socket.get_ready_state()
@@ -57,7 +56,9 @@ func get_socket() -> WebSocketPeer:
func poll() -> void:
if socket.get_ready_state() != socket.STATE_CLOSED:
socket.poll()
var state = socket.get_ready_state()
var state := socket.get_ready_state()
if last_state != state:
last_state = state
if state == socket.STATE_OPEN:
@@ -68,5 +69,5 @@ func poll() -> void:
message_received.emit(get_message())
func _process(delta):
func _process(_delta: float) -> void:
poll()

View File

@@ -1,12 +1,12 @@
extends Node
class_name WebSocketServer
class_name Node
extends WebSocketServer
signal message_received(peer_id: int, message)
signal message_received(peer_id: int, message: String)
signal client_connected(peer_id: int)
signal client_disconnected(peer_id: int)
@export var handshake_headers := PackedStringArray()
@export var supported_protocols: PackedStringArray
@export var supported_protocols := PackedStringArray()
@export var handshake_timout := 3000
@export var use_tls := false
@export var tls_cert: X509Certificate
@@ -23,7 +23,7 @@ class PendingPeer:
var connection: StreamPeer
var ws: WebSocketPeer
func _init(p_tcp: StreamPeerTCP):
func _init(p_tcp: StreamPeerTCP) -> void:
tcp = p_tcp
connection = p_tcp
connect_time = Time.get_ticks_msec()
@@ -39,17 +39,17 @@ func listen(port: int) -> int:
return tcp_server.listen(port)
func stop():
func stop() -> void:
tcp_server.stop()
pending_peers.clear()
peers.clear()
func send(peer_id, message) -> int:
var type = typeof(message)
func send(peer_id: int, message: String) -> int:
var type := typeof(message)
if peer_id <= 0:
# Send to multiple peers, (zero = brodcast, negative = exclude one)
for id in peers:
# Send to multiple peers, (zero = broadcast, negative = exclude one).
for id: int in peers:
if id == -peer_id:
continue
if type == TYPE_STRING:
@@ -59,30 +59,30 @@ func send(peer_id, message) -> int:
return OK
assert(peers.has(peer_id))
var socket = peers[peer_id]
var socket: WebSocketPeer = peers[peer_id]
if type == TYPE_STRING:
return socket.send_text(message)
return socket.send(var_to_bytes(message))
func get_message(peer_id) -> Variant:
func get_message(peer_id: int) -> Variant:
assert(peers.has(peer_id))
var socket = peers[peer_id]
var socket: WebSocketPeer = peers[peer_id]
if socket.get_available_packet_count() < 1:
return null
var pkt = socket.get_packet()
var pkt: PackedByteArray = socket.get_packet()
if socket.was_string_packet():
return pkt.get_string_from_utf8()
return bytes_to_var(pkt)
func has_message(peer_id) -> bool:
func has_message(peer_id: int) -> bool:
assert(peers.has(peer_id))
return peers[peer_id].get_available_packet_count() > 0
func _create_peer() -> WebSocketPeer:
var ws = WebSocketPeer.new()
var ws := WebSocketPeer.new()
ws.supported_protocols = supported_protocols
ws.handshake_headers = handshake_headers
return ws
@@ -91,72 +91,83 @@ func _create_peer() -> WebSocketPeer:
func poll() -> void:
if not tcp_server.is_listening():
return
while not refuse_new_connections and tcp_server.is_connection_available():
var conn = tcp_server.take_connection()
var conn: StreamPeerTCP = tcp_server.take_connection()
assert(conn != null)
pending_peers.append(PendingPeer.new(conn))
var to_remove := []
for p in pending_peers:
if not _connect_pending(p):
if p.connect_time + handshake_timout < Time.get_ticks_msec():
# Timeout
# Timeout.
to_remove.append(p)
continue # Still pending
continue # Still pending.
to_remove.append(p)
for r in to_remove:
for r: RefCounted in to_remove:
pending_peers.erase(r)
to_remove.clear()
for id in peers:
for id: int in peers:
var p: WebSocketPeer = peers[id]
var packets = p.get_available_packet_count()
p.poll()
if p.get_ready_state() != WebSocketPeer.STATE_OPEN:
client_disconnected.emit(id)
to_remove.append(id)
continue
while p.get_available_packet_count():
message_received.emit(id, get_message(id))
for r in to_remove:
for r: int in to_remove:
peers.erase(r)
to_remove.clear()
func _connect_pending(p: PendingPeer) -> bool:
if p.ws != null:
# Poll websocket client if doing handshake
# Poll websocket client if doing handshake.
p.ws.poll()
var state = p.ws.get_ready_state()
var state := p.ws.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
var id = randi_range(2, 1 << 30)
var id := randi_range(2, 1 << 30)
peers[id] = p.ws
client_connected.emit(id)
return true # Success.
return true # Success.
elif state != WebSocketPeer.STATE_CONNECTING:
return true # Failure.
return false # Still connecting.
return true # Failure.
return false # Still connecting.
elif p.tcp.get_status() != StreamPeerTCP.STATUS_CONNECTED:
return true # TCP disconnected.
return true # TCP disconnected.
elif not use_tls:
# TCP is ready, create WS peer
# TCP is ready, create WS peer.
p.ws = _create_peer()
p.ws.accept_stream(p.tcp)
return false # WebSocketPeer connection is pending.
return false # WebSocketPeer connection is pending.
else:
if p.connection == p.tcp:
assert(tls_key != null and tls_cert != null)
var tls = StreamPeerTLS.new()
var tls := StreamPeerTLS.new()
tls.accept_stream(p.tcp, TLSOptions.server(tls_key, tls_cert))
p.connection = tls
p.connection.poll()
var status = p.connection.get_status()
var status: StreamPeerTLS.Status = p.connection.get_status()
if status == StreamPeerTLS.STATUS_CONNECTED:
p.ws = _create_peer()
p.ws.accept_stream(p.connection)
return false # WebSocketPeer connection is pending.
return false # WebSocketPeer connection is pending.
if status != StreamPeerTLS.STATUS_HANDSHAKING:
return true # Failure.
return true # Failure.
return false
func _process(delta):
func _process(_delta: float) -> void:
poll()

View File

@@ -1,22 +1,22 @@
extends Node
# The URL we will connect to.
var websocket_url = "ws://localhost:9080"
## The URL we will connect to.
var websocket_url := "ws://localhost:9080"
var socket := WebSocketPeer.new()
func log_message(message):
var time = "[color=#aaaaaa] %s [/color]" % Time.get_time_string_from_system()
func log_message(message: String) -> void:
var time := "[color=#aaaaaa] %s |[/color] " % Time.get_time_string_from_system()
%TextClient.text += time + message + "\n"
func _ready():
func _ready() -> void:
if socket.connect_to_url(websocket_url) != OK:
log_message("Unable to connect.")
set_process(false)
func _process(_delta):
func _process(_delta: float) -> void:
socket.poll()
if socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
@@ -24,9 +24,9 @@ func _process(_delta):
log_message(socket.get_packet().get_string_from_ascii())
func _exit_tree():
func _exit_tree() -> void:
socket.close()
func _on_button_ping_pressed():
func _on_button_ping_pressed() -> void:
socket.send_text("Ping")

View File

@@ -16,6 +16,10 @@ config/tags=PackedStringArray("demo", "network", "official")
run/main_scene="res://Main.tscn"
config/features=PackedStringArray("4.2")
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/size/viewport_width=600

View File

@@ -1,23 +1,23 @@
extends Node
# The port we will listen to.
## The port the server will listen on.
const PORT = 9080
var tcp_server := TCPServer.new()
var socket := WebSocketPeer.new()
func log_message(message):
var time = "[color=#aaaaaa] %s [/color]" % Time.get_time_string_from_system()
func log_message(message: String) -> void:
var time := "[color=#aaaaaa] %s |[/color] " % Time.get_time_string_from_system()
%TextServer.text += time + message + "\n"
func _ready():
func _ready() -> void:
if tcp_server.listen(PORT) != OK:
log_message("Unable to start server.")
set_process(false)
func _process(_delta):
func _process(_delta: float) -> void:
while tcp_server.is_connection_available():
var conn: StreamPeerTCP = tcp_server.take_connection()
assert(conn != null)
@@ -30,10 +30,10 @@ func _process(_delta):
log_message(socket.get_packet().get_string_from_ascii())
func _exit_tree():
func _exit_tree() -> void:
socket.close()
tcp_server.stop()
func _on_button_pong_pressed():
func _on_button_pong_pressed() -> void:
socket.send_text("Pong")

View File

@@ -17,6 +17,10 @@ run/main_scene="res://scene/combo.tscn"
config/features=PackedStringArray("4.2")
config/icon="res://icon.webp"
[debug]
gdscript/warnings/untyped_declaration=1
[display]
window/stretch/mode="canvas_items"

View File

@@ -1,50 +1,42 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=2 format=3 uid="uid://dqxum77awcw6u"]
[ext_resource path="res://script/game.gd" type="Script" id=1]
[ext_resource type="Script" path="res://script/game.gd" id="1"]
[node name="Game" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 1
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
mouse_filter = 1
script = ExtResource("1")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="RichTextLabel" type="RichTextLabel" parent="HBoxContainer"]
offset_right = 510.0
offset_bottom = 600.0
layout_mode = 2
size_flags_horizontal = 3
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
offset_left = 514.0
offset_right = 1024.0
offset_bottom = 600.0
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="HBoxContainer/VBoxContainer"]
offset_right = 510.0
offset_bottom = 14.0
layout_mode = 2
text = "Players:"
[node name="ItemList" type="ItemList" parent="HBoxContainer/VBoxContainer"]
offset_top = 18.0
offset_right = 510.0
offset_bottom = 576.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
same_column_width = true
[node name="Action" type="Button" parent="HBoxContainer/VBoxContainer"]
offset_top = 580.0
offset_right = 510.0
offset_bottom = 600.0
layout_mode = 2
disabled = true
text = "Do Action!"

View File

@@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://c240icwf4uov8"]
[ext_resource type="Script" path="res://script/main.gd" id="1"]
[ext_resource type="PackedScene" path="res://scene/game.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://dqxum77awcw6u" path="res://scene/game.tscn" id="2"]
[node name="Main" type="Control"]
layout_mode = 3
@@ -15,14 +15,14 @@ size_flags_vertical = 3
script = ExtResource("1")
[node name="Panel" type="Panel" parent="."]
anchors_preset = 15
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Panel"]
anchors_preset = 15
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
@@ -33,84 +33,55 @@ grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer"]
offset_right = 1112.0
offset_bottom = 31.0
layout_mode = 2
[node name="Label" type="Label" parent="Panel/VBoxContainer/HBoxContainer"]
offset_top = 2.0
offset_right = 369.0
offset_bottom = 28.0
layout_mode = 2
size_flags_horizontal = 3
text = "Name"
[node name="NameEdit" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer"]
offset_left = 373.0
offset_right = 1112.0
offset_bottom = 31.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "A Godot User"
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/VBoxContainer"]
offset_top = 35.0
offset_right = 1112.0
offset_bottom = 66.0
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/VBoxContainer/HBoxContainer2"]
offset_right = 369.0
offset_bottom = 31.0
layout_mode = 2
size_flags_horizontal = 3
[node name="Host" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
offset_right = 44.0
offset_bottom = 31.0
layout_mode = 2
text = "Host"
[node name="Control" type="Control" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
layout_mode = 3
anchors_preset = 0
offset_left = 48.0
offset_right = 273.0
offset_bottom = 31.0
layout_mode = 2
size_flags_horizontal = 3
[node name="Connect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
offset_left = 277.0
offset_right = 369.0
offset_bottom = 31.0
layout_mode = 2
text = "Connect to"
[node name="Disconnect" type="Button" parent="Panel/VBoxContainer/HBoxContainer2/HBoxContainer"]
visible = false
offset_left = 68.0
offset_right = 152.0
offset_bottom = 24.0
layout_mode = 2
text = "Disconnect"
[node name="Hostname" type="LineEdit" parent="Panel/VBoxContainer/HBoxContainer2"]
offset_left = 373.0
offset_right = 1112.0
offset_bottom = 31.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "localhost"
placeholder_text = "localhost"
[node name="Control" type="Control" parent="Panel/VBoxContainer"]
layout_mode = 3
anchors_preset = 0
offset_top = 70.0
offset_right = 1112.0
offset_bottom = 70.0
layout_mode = 2
[node name="Game" parent="Panel/VBoxContainer" instance=ExtResource("2")]
layout_mode = 3
anchors_preset = 0
anchor_right = 0.0
anchor_bottom = 0.0
offset_top = 74.0
offset_right = 1112.0
offset_bottom = 608.0
layout_mode = 2
[node name="AcceptDialog" type="AcceptDialog" parent="."]
dialog_text = "Connection closed"

View File

@@ -1,8 +1,9 @@
extends Control
var paths := []
var paths: Array[NodePath] = []
func _enter_tree():
func _enter_tree() -> void:
for ch in $GridContainer.get_children():
paths.append(NodePath(str(get_path()) + "/GridContainer/" + str(ch.name)))
# Sets a dedicated Multiplayer API for each branch.
@@ -10,7 +11,7 @@ func _enter_tree():
get_tree().set_multiplayer(MultiplayerAPI.create_default_interface(), path)
func _exit_tree():
func _exit_tree() -> void:
# Clear the branch-specific Multiplayer API.
for path in paths:
get_tree().set_multiplayer(null, path)

View File

@@ -2,39 +2,40 @@ extends Control
const _crown = preload("res://img/crown.png")
@onready var _list = $HBoxContainer/VBoxContainer/ItemList
@onready var _action = $HBoxContainer/VBoxContainer/Action
@onready var _list: ItemList = $HBoxContainer/VBoxContainer/ItemList
@onready var _action: Button = $HBoxContainer/VBoxContainer/Action
const ACTIONS = ["roll", "pass"]
var _players = []
var _turn = -1
var _players: Array[int] = []
var _turn := -1
@rpc
func _log(what):
$HBoxContainer/RichTextLabel.add_text(what + "\n")
func _log(message: String) -> void:
$HBoxContainer/RichTextLabel.add_text(message + "\n")
@rpc("any_peer")
func set_player_name(p_name):
func set_player_name(p_name: String) -> void:
if not is_multiplayer_authority():
return
var sender = multiplayer.get_remote_sender_id()
var sender := multiplayer.get_remote_sender_id()
update_player_name.rpc(sender, p_name)
@rpc("call_local")
func update_player_name(player, p_name):
var pos = _players.find(player)
func update_player_name(player: int, p_name: String) -> void:
var pos := _players.find(player)
if pos != -1:
_list.set_item_text(pos, p_name)
@rpc("any_peer")
func request_action(action):
func request_action(action: String) -> void:
if not is_multiplayer_authority():
return
var sender = multiplayer.get_remote_sender_id()
var sender := multiplayer.get_remote_sender_id()
if _players[_turn] != sender:
_log.rpc("Someone is trying to cheat! %s" % str(sender))
return
@@ -46,88 +47,97 @@ func request_action(action):
next_turn()
func do_action(action):
var player_name = _list.get_item_text(_turn)
var val = randi() % 100
func do_action(action: String) -> void:
var player_name := _list.get_item_text(_turn)
var val := randi() % 100
_log.rpc("%s: %ss %d" % [player_name, action, val])
@rpc("call_local")
func set_turn(turn):
func set_turn(turn: int) -> void:
_turn = turn
if turn >= _players.size():
return
for i in range(0, _players.size()):
for i in _players.size():
if i == turn:
_list.set_item_icon(i, _crown)
else:
_list.set_item_icon(i, null)
_action.disabled = _players[turn] != multiplayer.get_unique_id()
@rpc("call_local")
func del_player(id):
var pos = _players.find(id)
func del_player(id: int) -> void:
var pos := _players.find(id)
if pos == -1:
return
_players.remove_at(pos)
_list.remove_item(pos)
if _turn > pos:
_turn -= 1
if multiplayer.is_server():
set_turn.rpc(_turn)
@rpc("call_local")
func add_player(id, pname=""):
func add_player(id: int, p_name: String = "") -> void:
_players.append(id)
if pname == "":
if p_name == "":
_list.add_item("... connecting ...", null, false)
else:
_list.add_item(pname, null, false)
_list.add_item(p_name, null, false)
func get_player_name(pos):
func get_player_name(pos: int) -> String:
if pos < _list.get_item_count():
return _list.get_item_text(pos)
else:
return "Error!"
func next_turn():
func next_turn() -> void:
_turn += 1
if _turn >= _players.size():
_turn = 0
set_turn.rpc(_turn)
func start():
func start() -> void:
set_turn(0)
func stop():
func stop() -> void:
_players.clear()
_list.clear()
_turn = 0
_action.disabled = true
func on_peer_add(id):
func on_peer_add(id: int) -> void:
if not multiplayer.is_server():
return
for i in range(0, _players.size()):
for i in _players.size():
add_player.rpc_id(id, _players[i], get_player_name(i))
add_player.rpc(id)
set_turn.rpc_id(id, _turn)
func on_peer_del(id):
func on_peer_del(id: int) -> void:
if not multiplayer.is_server():
return
del_player.rpc(id)
func _on_Action_pressed():
func _on_Action_pressed() -> void:
if multiplayer.is_server():
if _turn != 0:
return

View File

@@ -3,21 +3,21 @@ extends Control
const DEF_PORT = 8080
const PROTO_NAME = "ludus"
@onready var _host_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Host
@onready var _connect_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Connect
@onready var _disconnect_btn = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Disconnect
@onready var _name_edit = $Panel/VBoxContainer/HBoxContainer/NameEdit
@onready var _host_edit = $Panel/VBoxContainer/HBoxContainer2/Hostname
@onready var _game = $Panel/VBoxContainer/Game
@onready var _host_btn: Button = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Host
@onready var _connect_btn: Button = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Connect
@onready var _disconnect_btn: Button = $Panel/VBoxContainer/HBoxContainer2/HBoxContainer/Disconnect
@onready var _name_edit: LineEdit = $Panel/VBoxContainer/HBoxContainer/NameEdit
@onready var _host_edit: LineEdit = $Panel/VBoxContainer/HBoxContainer2/Hostname
@onready var _game: Control = $Panel/VBoxContainer/Game
var peer = WebSocketMultiplayerPeer.new()
var peer := WebSocketMultiplayerPeer.new()
func _init():
func _init() -> void:
peer.supported_protocols = ["ludus"]
func _ready():
func _ready() -> void:
multiplayer.peer_connected.connect(_peer_connected)
multiplayer.peer_disconnected.connect(_peer_disconnected)
multiplayer.server_disconnected.connect(_close_network)
@@ -30,11 +30,11 @@ func _ready():
if OS.has_environment("USERNAME"):
_name_edit.text = OS.get_environment("USERNAME")
else:
var desktop_path = OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP).replace("\\", "/").split("/")
var desktop_path := OS.get_system_dir(OS.SYSTEM_DIR_DESKTOP).replace("\\", "/").split("/")
_name_edit.text = desktop_path[desktop_path.size() - 2]
func start_game():
func start_game() -> void:
_host_btn.disabled = true
_name_edit.editable = false
_host_edit.editable = false
@@ -43,7 +43,7 @@ func start_game():
_game.start()
func stop_game():
func stop_game() -> void:
_host_btn.disabled = false
_name_edit.editable = true
_host_edit.editable = true
@@ -52,7 +52,7 @@ func stop_game():
_game.stop()
func _close_network():
func _close_network() -> void:
stop_game()
$AcceptDialog.popup_centered()
$AcceptDialog.get_ok_button().grab_focus()
@@ -60,20 +60,20 @@ func _close_network():
peer.close()
func _connected():
func _connected() -> void:
_game.set_player_name.rpc(_name_edit.text)
func _peer_connected(id):
func _peer_connected(id: int) -> void:
_game.on_peer_add(id)
func _peer_disconnected(id):
func _peer_disconnected(id: int) -> void:
print("Disconnected %d" % id)
_game.on_peer_del(id)
func _on_Host_pressed():
func _on_Host_pressed() -> void:
multiplayer.multiplayer_peer = null
peer.create_server(DEF_PORT)
multiplayer.multiplayer_peer = peer
@@ -81,11 +81,11 @@ func _on_Host_pressed():
start_game()
func _on_Disconnect_pressed():
func _on_Disconnect_pressed() -> void:
_close_network()
func _on_Connect_pressed():
func _on_Connect_pressed() -> void:
multiplayer.multiplayer_peer = null
peer.create_client("ws://" + _host_edit.text + ":" + str(DEF_PORT))
multiplayer.multiplayer_peer = peer