Skip to content

[WIP] Add Multiplayer Example #905

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e3d63b7
start replacing gdscript classes with rust classes
ValorZard Sep 23, 2024
3e8e3fa
replicated most of the gameplay from the gdscript side
ValorZard Sep 23, 2024
96821ba
replace gdscript version of player with rust
ValorZard Sep 23, 2024
2931157
commiting to save progress, but theres some weird bugs cropping up no…
ValorZard Sep 24, 2024
9eb685c
redid scene manager, but still getting error regarding player desync
ValorZard Sep 24, 2024
d3ab6b6
fix networking again, did some refactors
ValorZard Sep 24, 2024
b814869
converted everything, now time to debug all this
ValorZard Sep 24, 2024
d14981c
refactor a bit while trying to hunt down bug
ValorZard Sep 24, 2024
33b7080
fixed connection bug, but now we have bigger problems
ValorZard Sep 24, 2024
6098b09
it works now i guess
ValorZard Sep 24, 2024
30258c4
fixed edge cases with making game wait
ValorZard Sep 24, 2024
be54093
attempt to turn this into an actual game, but im tired
ValorZard Sep 24, 2024
e1f74f9
revert accidental changes to dodge the creeps
ValorZard Sep 24, 2024
0d0cc6e
remove unnecessary GDScript and scenes
ValorZard Sep 24, 2024
71bab84
remove game_manager and just pass around player_database instead
ValorZard Sep 24, 2024
58c250f
remove binaries that accidentally got generated inside repo
ValorZard Sep 24, 2024
e6e7210
remove assets and replace them with dodge the creep player sprite for…
ValorZard Sep 24, 2024
2e69a8a
make all instances of NetworkId called network_id for sanity sake + a…
ValorZard Sep 24, 2024
abc1612
made server in charge of starting the game
ValorZard Sep 24, 2024
2f9098b
did some refactoring, still can't get rid of inconsistent connection bug
ValorZard Sep 24, 2024
57ea430
no more weird sync errors! but now the players have dissapeared
ValorZard Sep 24, 2024
2e67f77
add more debugging tools
ValorZard Sep 24, 2024
f67e73b
add more comments, and add callback on death
ValorZard Sep 25, 2024
9cd0859
add address bar for custom address + added basic README
ValorZard Sep 25, 2024
1f0dd45
fix ci
ValorZard Sep 25, 2024
f89291e
address review comments
ValorZard Sep 27, 2024
4116339
fix formatting besides one weird cliipy error
ValorZard Sep 27, 2024
043db66
Replace every instance of magic number 1 with MultiplayerPeer::TARGET…
ValorZard Sep 27, 2024
9679781
restarting from scratch, look at previous commit if you want to pick …
ValorZard Sep 28, 2024
a1d7b30
this is where i stop
ValorZard Sep 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ members = [
"itest/rust",
"itest/repo-tweak",
"examples/dodge-the-creeps/rust",
"examples/hot-reload/rust",
"examples/hot-reload/rust", "examples/multiplayer-lan/rust",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you forget newline

]

# Note about Jetbrains IDEs: "IDE Sync" (Refresh Cargo projects) may cause static analysis errors such as
Expand Down
1 change: 1 addition & 0 deletions examples/multiplayer-lan/godot/.godot/extension_list.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
res://rust.gdextension
15 changes: 15 additions & 0 deletions examples/multiplayer-lan/godot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Multiplayer Bomber

A multiplayer implementation of the classic bomberman game.
One of the players should press **Host**, while other player(s)
should type in the host's IP address and press **Join**.

Language: GDScript

Renderer: Compatibility

Check out this demo on the asset library: https://godotengine.org/asset-library/asset/139

## Screenshots

![Screenshot](screenshots/bomber.png)
34 changes: 34 additions & 0 deletions examples/multiplayer-lan/godot/bomb.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
extends Area2D

var in_area: Array = []
var from_player: int

# Called from the animation.
func explode():
if not is_multiplayer_authority():
# Explode only on authority.
return
for p in in_area:
if p.has_method("exploded"):
# 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
var result: Dictionary = world_state.intersect_ray(query)
if not result.collider is TileMap:
# Exploded can only be called by the authority, but will also be called locally.
p.exploded.rpc(from_player)


func done():
if is_multiplayer_authority():
queue_free()


func _on_bomb_body_enter(body):
if not body in in_area:
in_area.append(body)


func _on_bomb_body_exit(body):
in_area.erase(body)
134 changes: 134 additions & 0 deletions examples/multiplayer-lan/godot/bomb.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
[gd_scene load_steps=9 format=3 uid="uid://enwoaqi0rnei"]

[ext_resource type="Script" path="res://bomb.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://bdomqql6y50po" path="res://brickfloor.png" id="2"]
[ext_resource type="Texture2D" uid="uid://drfbkdqmj0gu2" path="res://explosion.png" id="3"]

[sub_resource type="RectangleShape2D" id="RectangleShape2D_1ih13"]
size = Vector2(16, 192)

[sub_resource type="RectangleShape2D" id="RectangleShape2D_whso6"]
size = Vector2(192, 16)

[sub_resource type="Curve" id="Curve_4yges"]
max_value = 2.0
_data = [Vector2(0.00150494, 0.398437), 0.0, 0.0, 0, 0, Vector2(0.0152287, 1.42969), 0.0, 0.0, 0, 0, Vector2(0.478607, 1.30078), 0.0, 0.0, 0, 0, Vector2(1, 0.291016), 0.0, 0.0, 0, 0]
point_count = 4

[sub_resource type="Animation" id="Animation_21j5c"]
length = 4.0
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Sprite:self_modulate")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.4, 0.6, 0.8, 1.1, 1.3, 1.5, 1.8, 1.9, 2, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 3),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 0,
"values": [Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(8, 8, 8, 1), Color(1, 1, 1, 1), Color(1, 1, 1, 0)]
}
tracks/1/type = "method"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath(".")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(2.8, 3.4),
"transitions": PackedFloat32Array(1, 1),
"values": [{
"args": [],
"method": &"explode"
}, {
"args": [],
"method": &"done"
}]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Explosion1:emitting")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 2.8),
"transitions": PackedFloat32Array(1, 1),
"update": 1,
"values": [false, true]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Explosion2:emitting")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 2.8),
"transitions": PackedFloat32Array(1, 1),
"update": 1,
"values": [false, true]
}

[sub_resource type="AnimationLibrary" id="AnimationLibrary_h2w7m"]
_data = {
"anim": SubResource("Animation_21j5c")
}

[node name="Bomb" type="Area2D"]
monitorable = false
script = ExtResource("1")

[node name="Sprite" type="Sprite2D" parent="."]
self_modulate = Color(1, 1, 1, 0)
position = Vector2(-2.92606, -2.92606)
texture = ExtResource("2")
region_enabled = true
region_rect = Rect2(144, 0, 48, 48)

[node name="Shape1" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_1ih13")

[node name="Shape2" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_whso6")

[node name="Explosion1" type="CPUParticles2D" parent="."]
emitting = false
lifetime = 0.5
one_shot = true
explosiveness = 0.95
texture = ExtResource("3")
emission_shape = 3
emission_rect_extents = Vector2(80, 1)
gravity = Vector2(0, 0)
initial_velocity_min = 1.0
initial_velocity_max = 1.0
angular_velocity_min = 187.35
angular_velocity_max = 188.35
scale_amount_curve = SubResource("Curve_4yges")

[node name="Explosion2" type="CPUParticles2D" parent="."]
rotation = 1.57162
emitting = false
lifetime = 0.5
one_shot = true
explosiveness = 0.95
texture = ExtResource("3")
emission_shape = 3
emission_rect_extents = Vector2(80, 1)
gravity = Vector2(0, 0)
initial_velocity_min = 1.0
initial_velocity_max = 1.0
angular_velocity_min = 187.35
angular_velocity_max = 188.35
scale_amount_curve = SubResource("Curve_4yges")

[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
autoplay = "anim"
libraries = {
"": SubResource("AnimationLibrary_h2w7m")
}

[connection signal="body_entered" from="." to="." method="_on_bomb_body_enter"]
[connection signal="body_exited" from="." to="." method="_on_bomb_body_exit"]
14 changes: 14 additions & 0 deletions examples/multiplayer-lan/godot/bomb_spawner.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
extends MultiplayerSpawner

func _init():
spawn_function = _spawn_bomb


func _spawn_bomb(data):
print(data)
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()
bomb.position = data[0]
bomb.from_player = data[1]
return bomb
Binary file added examples/multiplayer-lan/godot/brickfloor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/multiplayer-lan/godot/charwalk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/multiplayer-lan/godot/explosion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 149 additions & 0 deletions examples/multiplayer-lan/godot/gamestate.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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:
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
const DEFAULT_PORT = 10567

# Max number of players.
const MAX_PEERS = 12

var peer = null

# Name for my player.
var player_name = "The Warrior"

# Names for remote players in id:name format.
var players = {}
var players_ready = []

# 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)

# Callback from SceneTree.
func _player_connected(id):
# 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.
if multiplayer.is_server():
game_error.emit("Player " + players[id] + " disconnected")
end_game()
else: # Game is not in progress.
# Unregister this player.
unregister_player(id)


# Callback from SceneTree, only for clients (not server).
func _connected_ok():
# We just connected to a server
connection_succeeded.emit()


# Callback from SceneTree, only for clients (not server).
func _server_disconnected():
game_error.emit("Server disconnected")
end_game()


# Callback from SceneTree, only for clients (not server).
func _connected_fail():
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()
players[id] = new_player_name
player_list_changed.emit()


func unregister_player(id):
players.erase(id)
player_list_changed.emit()


@rpc("call_local")
func load_world():
# Change scene.
var world = 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:
world.get_node("Score").add_player(pn, players[pn])
get_tree().set_pause(false) # Unpause and unleash the game!


func host_game(new_player_name):
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):
player_name = new_player_name
peer = ENetMultiplayerPeer.new()
peer.create_client(ip, DEFAULT_PORT)
multiplayer.set_multiplayer_peer(peer)


func get_player_list():
return players.values()


func get_player_name():
return player_name


func begin_game():
assert(multiplayer.is_server())
load_world.rpc()

var world = get_tree().get_root().get_node("World")
var player_scene = 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:
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()
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)


func end_game():
if has_node("/root/World"): # Game is in progress.
# End it
get_node("/root/World").queue_free()

game_ended.emit()
players.clear()


func _ready():
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)
Binary file added examples/multiplayer-lan/godot/icon.webp
Binary file not shown.
Loading
Loading