mirror of
https://gitlab.com/open-fpsz/open-fpsz.git
synced 2026-07-11 22:44:45 +00:00
👽 interstellar delivery
This commit is contained in:
parent
547c97bfba
commit
97c8292858
257 changed files with 7309 additions and 4637 deletions
50
types/multiplayer/match.gd
Normal file
50
types/multiplayer/match.gd
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
class_name Match extends Node
|
||||
|
||||
enum MatchState {
|
||||
READY,
|
||||
RUNNING,
|
||||
PAUSED,
|
||||
ENDED
|
||||
}
|
||||
|
||||
signal entered_state(state: MatchState)
|
||||
signal exiting_state(state: MatchState)
|
||||
|
||||
@export var state : MatchState = MatchState.READY:
|
||||
set = set_state
|
||||
|
||||
func set_state(new_state: MatchState) -> void:
|
||||
if state == new_state: return
|
||||
exiting_state.emit(state)
|
||||
state = new_state
|
||||
entered_state.emit(state)
|
||||
|
||||
func _ready() -> void:
|
||||
pass # Replace with function body.
|
||||
|
||||
func start() -> void:
|
||||
state = MatchState.RUNNING
|
||||
|
||||
func end() -> void:
|
||||
state = MatchState.ENDED
|
||||
|
||||
func pause() -> void:
|
||||
#state = MatchState.PAUSED
|
||||
push_warning("not implemented yet")
|
||||
|
||||
func is_ready() -> bool:
|
||||
return state == MatchState.READY
|
||||
315
types/multiplayer/multiplayer.gd
Normal file
315
types/multiplayer/multiplayer.gd
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
## This class defines the multiplayer game type.
|
||||
class_name Multiplayer extends Node
|
||||
|
||||
signal connected_to_server
|
||||
signal connection_failed
|
||||
|
||||
## Enumeration that defines supported game modes.
|
||||
enum Mode {
|
||||
## Free-for-all mode where players compete individually.
|
||||
FREE_FOR_ALL,
|
||||
## Rabbit mode where players chase and protect a designated "rabbit" player.
|
||||
RABBIT,
|
||||
## Capture the flag mode where teams compete to capture each other's flags.
|
||||
CAPTURE_THE_FLAG,
|
||||
## Arena mode where players engage in team deathmatch battles.
|
||||
ARENA,
|
||||
## Ball mode where teams aim to control the ball and score goals.
|
||||
BALL
|
||||
}
|
||||
|
||||
## The scoreboard to keep track of scores.
|
||||
@export var scoreboard : Scoreboard
|
||||
## The multiplayer mode.
|
||||
@export var mode : Mode = Mode.FREE_FOR_ALL
|
||||
## The current map node.
|
||||
@export var map : Node: set = set_map
|
||||
## The maximum number of clients.
|
||||
@export var MAX_CLIENTS := 24
|
||||
## The time it takes for a player to respawn when killed, secconds (s).
|
||||
@export var VICTIM_RESPAWN_TIME : float = 3.0
|
||||
## The total duration of a match, in secconds (s).
|
||||
@export var MATCH_DURATION := 1200
|
||||
## The total duration of the warmup phase of a match.
|
||||
@export var WARMUP_START_DURATION := 25
|
||||
|
||||
@export_group("Spawned Scenes")
|
||||
@export var _PLAYER : PackedScene
|
||||
@export var _FLAG : PackedScene
|
||||
|
||||
## This is the match timer.
|
||||
var timer:Timer = null
|
||||
|
||||
## The [Teams] manager.
|
||||
@onready var teams : Teams = $Teams
|
||||
## The spawn root for [Player] nodes.
|
||||
@onready var players : Node = $Players
|
||||
## The spawn root for [Flag] nodes.
|
||||
@onready var objectives : Node = $Objectives
|
||||
## The spawn root for Map nodes.
|
||||
@onready var map_root : Node = $Map
|
||||
# The spawn root for the scoreboard node.
|
||||
@onready var _scoreboard_spawn_root : Node = $Scoreboard
|
||||
## The [Ping] manager.
|
||||
@onready var ping : Ping = $Ping
|
||||
|
||||
func _unhandled_input(event : InputEvent) -> void:
|
||||
if event.is_action_pressed("exit"):
|
||||
if is_peer_connected():
|
||||
_leave_match.rpc_id(get_multiplayer_authority())
|
||||
|
||||
func is_peer_connected() -> bool:
|
||||
if not multiplayer.multiplayer_peer or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
|
||||
return false
|
||||
return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
|
||||
|
||||
func set_map(new_map: Node) -> void:
|
||||
map = new_map
|
||||
map_root.add_child(map)
|
||||
MapsManager.current_map = map
|
||||
|
||||
## This method starts a server.
|
||||
func start_server(port : int, map_scene : PackedScene, _mode: Mode = mode, username : String = "mercury") -> void:
|
||||
# setup enet server peer
|
||||
var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
||||
peer.create_server(port, MAX_CLIENTS)
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
timer = Timer.new()
|
||||
timer.one_shot = true
|
||||
add_child(timer)
|
||||
map = map_scene.instantiate()
|
||||
mode = _mode
|
||||
|
||||
ping.synchronized.connect(scoreboard._on_ping_sync)
|
||||
|
||||
match mode:
|
||||
Mode.RABBIT:
|
||||
var flag : Flag = _FLAG.instantiate()
|
||||
objectives.add_child(flag)
|
||||
var flagstand : Marker3D = map.get_node("FlagStand")
|
||||
if flagstand:
|
||||
flag.global_position = flagstand.global_position
|
||||
|
||||
teams.team_added.connect(_on_team_added)
|
||||
teams.team_erased.connect(_on_team_erased)
|
||||
teams.add_teams(["rabbit", "chasers"])
|
||||
flag.grabbed.connect(
|
||||
func(carry:FlagCarryComponent) -> void:
|
||||
carry.owner.hud.objective_label.set_visible(true)
|
||||
switch_team(carry.owner.peer_id, "rabbit"))
|
||||
flag.dropped.connect(
|
||||
func(carry:FlagCarryComponent) -> void:
|
||||
carry.owner.hud.objective_label.set_visible(false)
|
||||
switch_team(carry.owner.peer_id, "chasers"))
|
||||
|
||||
_scoreboard_spawn_root.add_child(
|
||||
RabbitScoringComponent.new(scoreboard, flag))
|
||||
_scoreboard_spawn_root.add_child(
|
||||
DeathmatchScoringComponent.new(scoreboard, players))
|
||||
|
||||
players.child_entered_tree.connect(func(_player:Player) -> void:
|
||||
teams["chasers"].add(_player.peer_id, _player.username)
|
||||
_player.damage.connect(_rabbit_damage_handler)
|
||||
)
|
||||
|
||||
Mode.FREE_FOR_ALL:
|
||||
scoreboard.add_panel()
|
||||
_scoreboard_spawn_root.add_child(
|
||||
DeathmatchScoringComponent.new(scoreboard, players))
|
||||
players.child_entered_tree.connect(func(_player:Player) -> void:
|
||||
var panel : ScorePanel = scoreboard.get_panel(0)
|
||||
panel.add_entry(_player.peer_id, _player.username)
|
||||
_player.damage.connect(_ffa_damage_handler)
|
||||
)
|
||||
|
||||
# when a new player is added into tree
|
||||
players.child_entered_tree.connect(func(_player:Player) -> void:
|
||||
# make sure we have enough players to start the match
|
||||
if players.get_child_count() > 1 and timer.is_stopped():
|
||||
|
||||
_start_match()
|
||||
)
|
||||
|
||||
print("mode `%s` loaded" % Mode.keys()[mode])
|
||||
|
||||
if DisplayServer.get_name() != "headless":
|
||||
add_player(1, username)
|
||||
|
||||
func _rabbit_damage_handler(source: Node, target: Node, amount: float) -> void:
|
||||
assert(target.find_children("*", "Health"))
|
||||
if source == target or source.team_id != target.team_id:
|
||||
target.health.damage.rpc(amount, source.peer_id)
|
||||
|
||||
func _ffa_damage_handler(source: Node, target: Node, amount: float) -> void:
|
||||
assert(target.find_children("*", "Health"))
|
||||
target.health.damage.rpc(amount, source.peer_id)
|
||||
|
||||
func _on_peer_connected(peer_id: int) -> void:
|
||||
print("peer `%d` connected" % peer_id)
|
||||
|
||||
func _on_peer_disconnected(peer_id : int) -> void:
|
||||
print("peer `%d` disconnected" % peer_id)
|
||||
if players.get_child_count() < 2:
|
||||
# stop timer when there is not enough players
|
||||
timer.stop()
|
||||
var player: Player = players.get_child(0)
|
||||
if player:
|
||||
player.hud.timer_label.text = "Warmup"
|
||||
|
||||
func _start_match() -> void:
|
||||
scoreboard.scoring = true
|
||||
timer.start(WARMUP_START_DURATION) # wait few seconds
|
||||
await timer.timeout # wait for the starting countdown to finish
|
||||
for player: Player in players.get_children():
|
||||
if player.has_flag():
|
||||
var flag: Flag = player.flag_carry_component._flag
|
||||
player.flag_carry_component.drop(player.linear_velocity)
|
||||
var flagstand : Marker3D = map.get_node("FlagStand")
|
||||
if flagstand:
|
||||
flag.global_position = flagstand.global_position
|
||||
scoreboard.reset_scores()
|
||||
timer.start(MATCH_DURATION) # restart timer with match duration
|
||||
players.respawn() # respawn everyone
|
||||
if not timer.timeout.is_connected(_on_post_match):
|
||||
timer.timeout.connect(_on_post_match)
|
||||
|
||||
func _on_post_match() -> void:
|
||||
# disconnect handler for timer reuse
|
||||
if timer.timeout.is_connected(_on_post_match):
|
||||
timer.timeout.disconnect(_on_post_match)
|
||||
# @TODO: display end of match stats with scoreboard data
|
||||
scoreboard.reset_scores()
|
||||
scoreboard.scoring = false
|
||||
for player: Player in players.get_children():
|
||||
if player.has_flag():
|
||||
var flag: Flag = player.flag_carry_component._flag
|
||||
player.flag_carry_component.drop(player.linear_velocity)
|
||||
var flagstand : Marker3D = map.get_node("FlagStand")
|
||||
if flagstand:
|
||||
flag.global_position = flagstand.global_position
|
||||
# restart match
|
||||
_start_match()
|
||||
|
||||
func join_server(host : String, port : int, username : String) -> void:
|
||||
var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
||||
peer.create_client(host, port)
|
||||
multiplayer.connected_to_server.connect(_on_connected_to_server.bind(username))
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
Game.exit_pressed.emit()
|
||||
queue_free()
|
||||
|
||||
func add_player(_peer_id : int, username : String) -> void:
|
||||
var player : Player = _PLAYER.instantiate()
|
||||
# @NOTE: MultiplayerSpawner needs valid names so we can use either
|
||||
# `add_child(node, true)` or `player.name = str(_peer_id)`.
|
||||
# > Unable to auto-spawn node with reserved name: @RigidBody3D@101.
|
||||
# > Make sure to add your replicated scenes via 'add_child(node, true)'
|
||||
# > to produce valid names.
|
||||
player.name = str(_peer_id)
|
||||
player.peer_id = _peer_id
|
||||
player.username = username
|
||||
# @NOTE: player scene requires both params prior being added to the tree
|
||||
players.add_child(player)
|
||||
# @TODO: get spawn location randomly instead of using points
|
||||
player.global_position = MapsManager.get_player_spawn().global_position
|
||||
player.killed.connect(_on_player_killed)
|
||||
|
||||
## This method switch a [param peer_id] to the specified team along with its score.
|
||||
func switch_team(peer_id: int, to_team_name: String) -> void:
|
||||
var score : Vector3i = scoreboard.get_score(peer_id)
|
||||
teams.switch_team(peer_id, to_team_name)
|
||||
scoreboard.set_score(peer_id, score)
|
||||
|
||||
# This method is called when the [member Teams.team_addded] signal is emitted.
|
||||
func _on_team_added(team_name: String) -> void:
|
||||
var panel : ScorePanel = scoreboard.add_panel(team_name)
|
||||
panel.title.show()
|
||||
var team : Team = teams[team_name]
|
||||
panel.title.set_text(team_name)
|
||||
team.renamed.connect(panel.title.set_text)
|
||||
team.player_added.connect(_on_team_player_added)
|
||||
team.player_erased.connect(_on_team_player_erased)
|
||||
|
||||
# This method is called when the [member Teams.team_erased] signal is emitted.
|
||||
func _on_team_erased(team_name: String) -> void:
|
||||
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
|
||||
scoreboard.remove_panel(panel)
|
||||
|
||||
# This method is called when the [member Team.player_added] signal is emitted.
|
||||
func _on_team_player_added(team_name: String, peer_id: int, username : String = "newblood") -> void:
|
||||
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
|
||||
panel.add_entry(peer_id, username)
|
||||
var player:Player = players.get_node(str(peer_id))
|
||||
player.team_id = teams[team_name].get_instance_id()
|
||||
|
||||
# This method is called when the [member Teams.Team.player_erased] signal is emitted.
|
||||
func _on_team_player_erased(team_name: String, peer_id: int) -> void:
|
||||
var panel : ScorePanel = scoreboard.panels.get_node(team_name)
|
||||
panel.remove_entry_by_peer_id(peer_id)
|
||||
var player:Player = players.get_node(str(peer_id))
|
||||
player.team_id = -1
|
||||
|
||||
func _on_connected_to_server(username : String) -> void:
|
||||
connected_to_server.emit()
|
||||
_join_match.rpc_id(1, username)
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
connection_failed.emit()
|
||||
|
||||
func _on_player_killed(victim: Player, _killer:int) -> void:
|
||||
await get_tree().create_timer(VICTIM_RESPAWN_TIME).timeout
|
||||
var spawn: Node3D = MapsManager.get_player_spawn()
|
||||
victim.respawn.rpc_id(1, spawn.global_position)
|
||||
|
||||
# This method notifies the server that a player wants to join the match. It
|
||||
# takes a single [param username] parameter and is invoked remotely by clients.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _join_match(username : String) -> void:
|
||||
if multiplayer.is_server():
|
||||
# add player to server with unique id and username
|
||||
add_player(multiplayer.get_remote_sender_id(), username)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _leave_match() -> void:
|
||||
if multiplayer.is_server():
|
||||
var peer_id:int = multiplayer.get_remote_sender_id()
|
||||
var player : Player = players.get_node(str(peer_id))
|
||||
if player:
|
||||
var team : Team = teams.get_peer_team(peer_id)
|
||||
if team:
|
||||
team.erase(peer_id)
|
||||
players.remove_child(player)
|
||||
player.queue_free()
|
||||
_cleanup_peer.rpc_id(peer_id)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _cleanup_peer() -> void:
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
queue_free()
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# @NOTE: The `is_multiplayer_authority` method in `_extree` push an error
|
||||
# about the multiplayer instance not being the currently active one.it_
|
||||
# see https://github.com/godotengine/godot/issues/77723
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
55
types/multiplayer/multiplayer.tscn
Normal file
55
types/multiplayer/multiplayer.tscn
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
[gd_scene load_steps=9 format=3 uid="uid://bvwxfgygm2xb8"]
|
||||
|
||||
[ext_resource type="Script" path="res://types/multiplayer/multiplayer.gd" id="1_r1kd6"]
|
||||
[ext_resource type="Script" path="res://types/multiplayer/ping.gd" id="1_tafn1"]
|
||||
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_og1vb"]
|
||||
[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="3_h0rie"]
|
||||
[ext_resource type="PackedScene" uid="uid://b8bosdd0o1lu7" path="res://interfaces/scoreboard/scoreboard.tscn" id="4_rhwwf"]
|
||||
[ext_resource type="Script" path="res://types/multiplayer/teams.gd" id="5_op0if"]
|
||||
[ext_resource type="Script" path="res://types/multiplayer/players.gd" id="7_ycqj1"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_g11jg"]
|
||||
properties/0/path = NodePath(".:mode")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 2
|
||||
|
||||
[node name="Multiplayer" type="Node" node_paths=PackedStringArray("scoreboard")]
|
||||
script = ExtResource("1_r1kd6")
|
||||
scoreboard = NodePath("Scoreboard/Scoreboard")
|
||||
_PLAYER = ExtResource("2_og1vb")
|
||||
_FLAG = ExtResource("3_h0rie")
|
||||
|
||||
[node name="MultiplayerSync" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("SceneReplicationConfig_g11jg")
|
||||
|
||||
[node name="Ping" type="Node" parent="."]
|
||||
script = ExtResource("1_tafn1")
|
||||
active = true
|
||||
sync_interval = 0.5
|
||||
|
||||
[node name="Teams" type="Node" parent="."]
|
||||
script = ExtResource("5_op0if")
|
||||
|
||||
[node name="Scoreboard" type="Node" parent="."]
|
||||
|
||||
[node name="Scoreboard" parent="Scoreboard" instance=ExtResource("4_rhwwf")]
|
||||
|
||||
[node name="Map" type="Node" parent="."]
|
||||
|
||||
[node name="MapSpawner" type="MultiplayerSpawner" parent="."]
|
||||
_spawnable_scenes = PackedStringArray("res://maps/desert/desert.tscn")
|
||||
spawn_path = NodePath("../Map")
|
||||
spawn_limit = 1
|
||||
|
||||
[node name="Players" type="Node" parent="."]
|
||||
script = ExtResource("7_ycqj1")
|
||||
|
||||
[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
|
||||
_spawnable_scenes = PackedStringArray("res://entities/player/player.tscn")
|
||||
spawn_path = NodePath("../Players")
|
||||
|
||||
[node name="Objectives" type="Node" parent="."]
|
||||
|
||||
[node name="ObjectiveSpawner" type="MultiplayerSpawner" parent="."]
|
||||
_spawnable_scenes = PackedStringArray("res://entities/flag/flag.tscn")
|
||||
spawn_path = NodePath("../Objectives")
|
||||
173
types/multiplayer/ping.gd
Normal file
173
types/multiplayer/ping.gd
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
## This defines a ping manager.
|
||||
##
|
||||
## When added to your scene and connect the [member synchronized] signal to your
|
||||
## custom handler in order to receive ping updates on a regular basis (as
|
||||
## defined by [param ping_interval] and [param sync_interval]) for each peer.
|
||||
## [codeblock]
|
||||
## func _ready() -> void:
|
||||
## $Ping.synchronized.connect(_on_ping_sync)
|
||||
##
|
||||
## func _on_ping_sync(state: Dictionary) -> void:
|
||||
## for peer_id in state:
|
||||
## print("peer `%s` latency is %d ms" % [peer_id, state[peer_id]])
|
||||
## [/codeblock]
|
||||
class_name Ping extends Node
|
||||
|
||||
## This class defines a ping information to keep track of ping results
|
||||
## and provide average ping.
|
||||
class PingInfo:
|
||||
const SIZE := 21
|
||||
var samples := PackedInt32Array()
|
||||
var pos := 0
|
||||
|
||||
func _init() -> void:
|
||||
samples.resize(SIZE)
|
||||
for i in range(SIZE):
|
||||
samples[i] = 0
|
||||
|
||||
func get_average() -> int:
|
||||
var value := 0
|
||||
for sample in samples:
|
||||
value += sample
|
||||
@warning_ignore("integer_division")
|
||||
return value / SIZE if value > 0 else 0
|
||||
|
||||
func set_next(value: int) -> void:
|
||||
samples[pos] = value
|
||||
pos = (pos + 1) % SIZE
|
||||
|
||||
## Emitted when the state is synchronized.
|
||||
signal synchronized(state : Dictionary)
|
||||
|
||||
## Controls activation state of the ping feature.
|
||||
@export var active: bool = false
|
||||
## The time interval (in milliseconds) between ping requests sent by the server to measure client latency.
|
||||
@export_range(0.,1.,.01) var ping_interval: float = .2 # ms
|
||||
## The time interval (in milliseconds) between state synchronization updates sent to connected clients.
|
||||
@export_range(0.,1.,.01) var sync_interval: float = 1. # ms
|
||||
## The size of the ping history buffer used to store past ping times for each client.
|
||||
@export var ping_history: int = 10:
|
||||
set = set_ping_history
|
||||
|
||||
# internal variables
|
||||
var _clients := {}
|
||||
var _state := {}
|
||||
var _last_ping := 0
|
||||
var _pings := []
|
||||
var _last_sync := 0
|
||||
|
||||
func _init() -> void:
|
||||
clear_pings()
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.peer_connected.connect(_add_peer)
|
||||
multiplayer.peer_disconnected.connect(_del_peer)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
multiplayer.peer_connected.disconnect(_add_peer)
|
||||
multiplayer.peer_disconnected.disconnect(_del_peer)
|
||||
|
||||
# synchronize state only on server
|
||||
func _process(_delta: float) -> void:
|
||||
if not active \
|
||||
or multiplayer.multiplayer_peer is OfflineMultiplayerPeer \
|
||||
or multiplayer.multiplayer_peer.get_connection_status() \
|
||||
!= MultiplayerPeer.CONNECTION_CONNECTED \
|
||||
or not multiplayer.is_server() \
|
||||
or not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
# sync state at regular intervals
|
||||
var now := Time.get_ticks_msec()
|
||||
if sync_interval * 1000 > 0 and now >= _last_sync + sync_interval * 1000:
|
||||
synchronize()
|
||||
_last_sync = now
|
||||
# check and update ping intervals
|
||||
if now >= _pings[_last_ping] + ping_interval * 1000:
|
||||
_last_ping = (_last_ping + 1) % ping_history
|
||||
_pings[_last_ping] = now
|
||||
_ping.rpc(now)
|
||||
|
||||
## Clears the ping history array.
|
||||
func clear_pings() -> void:
|
||||
_last_ping = 0
|
||||
_pings.resize(ping_history)
|
||||
_pings.fill(0)
|
||||
|
||||
## Sets the size of the ping history buffer and clears existing ping data.
|
||||
func set_ping_history(value: int) -> void:
|
||||
if value < 1:
|
||||
return
|
||||
ping_history = value
|
||||
clear_pings()
|
||||
|
||||
## Adds a new peer to the registry
|
||||
func _add_peer(peer_id: int) -> void:
|
||||
_clients[peer_id] = PingInfo.new()
|
||||
|
||||
## Deletes a peer from the registry
|
||||
func _del_peer(peer_id: int) -> void:
|
||||
_clients.erase(peer_id)
|
||||
|
||||
@rpc("unreliable")
|
||||
func _ping(time: int) -> void:
|
||||
_pong.rpc_id(1, time)
|
||||
|
||||
@rpc("any_peer")
|
||||
func _pong(time: int) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
# get id of the peer sending the pong
|
||||
var peer_id: int = multiplayer.get_remote_sender_id()
|
||||
# check if peer exists in the registered clients dictionary
|
||||
if not _clients.has(peer_id):
|
||||
return
|
||||
# init variables
|
||||
var now := Time.get_ticks_msec()
|
||||
var last := (_last_ping + 1) % ping_history
|
||||
var found := ping_history * ping_interval * 1000
|
||||
# search related ping in history
|
||||
for i in range(ping_history):
|
||||
# check if current ping matches received pong
|
||||
if time == _pings[last]:
|
||||
found = _pings[last]
|
||||
break
|
||||
# move to next ping in history
|
||||
last = (last + 1) % ping_history
|
||||
# compute round-trip time (RTT) and update ping info for this peer
|
||||
_clients[peer_id].set_next((now - found) / 2)
|
||||
|
||||
## This function sends the current state of ping information to all connected peers
|
||||
## and emits a signal indicating that the state has been synchronized.
|
||||
func synchronize() -> void:
|
||||
# check if server is active and has multiplayer peers connected
|
||||
if not active or not multiplayer.has_multiplayer_peer() or not multiplayer.is_server():
|
||||
return
|
||||
# clear current state
|
||||
_state.clear()
|
||||
# iterate over registered clients
|
||||
for peer_id:int in _clients:
|
||||
# set client average ping state
|
||||
_state[peer_id] = _clients[peer_id].get_average()
|
||||
# send state to connected peers
|
||||
_synchronize.rpc(_state)
|
||||
# emit signal to indicate that synchronization has occured
|
||||
synchronized.emit(_state)
|
||||
|
||||
@rpc("call_local", "unreliable")
|
||||
func _synchronize(state: Dictionary) -> void:
|
||||
_state = state
|
||||
synchronized.emit(_state)
|
||||
21
types/multiplayer/players.gd
Normal file
21
types/multiplayer/players.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
extends Node
|
||||
|
||||
func respawn() -> void:
|
||||
for player : Player in get_children():
|
||||
var spawn : Node3D = MapsManager.get_player_spawn()
|
||||
if spawn:
|
||||
player.respawn.rpc_id(get_multiplayer_authority(), spawn.global_position)
|
||||
85
types/multiplayer/team.gd
Normal file
85
types/multiplayer/team.gd
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
## This class defines a Team.
|
||||
class_name Team extends RefCounted
|
||||
|
||||
## Emitted when the [Team] is renamed.
|
||||
signal renamed(team_name: String)
|
||||
|
||||
## Emitted when a peer_id is added to the [Team].
|
||||
signal player_added(team_name: String, peer_id: int, username: String)
|
||||
|
||||
## Emitted when a peer_id is erased from the [Team].
|
||||
signal player_erased(team_name: String, peer_id: int)
|
||||
|
||||
var name : String:
|
||||
set(new_name):
|
||||
name = new_name
|
||||
renamed.emit(name)
|
||||
|
||||
var players: Dictionary = {}
|
||||
|
||||
## Constructor.
|
||||
func _init(team_name: String, team_players: Dictionary = {}) -> void:
|
||||
name = team_name
|
||||
for peer_id : int in team_players:
|
||||
add(peer_id, team_players[peer_id])
|
||||
|
||||
# This is the iterator index cursor.
|
||||
var _iter_cursor : int = 0
|
||||
|
||||
# This method is an iterator initializer.
|
||||
func _iter_init(_arg : Variant) -> bool:
|
||||
_iter_cursor = 0 # reset
|
||||
print(_iter_cursor < players.size())
|
||||
return _iter_cursor < players.size()
|
||||
|
||||
# This method checks if the iterator has a next value.
|
||||
func _iter_next(_arg : Variant) -> bool:
|
||||
_iter_cursor += 1
|
||||
return _iter_cursor < players.size()
|
||||
|
||||
# This method gets the next iterator value.
|
||||
func _iter_get(_arg : Variant) -> int:
|
||||
return players.keys()[_iter_cursor]
|
||||
|
||||
## Add [param peer_id] to the [Team] if not already present.
|
||||
func add(peer_id: int, username : String = "") -> void:
|
||||
if peer_id not in players:
|
||||
players[peer_id] = username
|
||||
player_added.emit(name, peer_id, username)
|
||||
|
||||
## Erase [param peer_id] from the [Team].
|
||||
func erase(peer_id: int) -> bool:
|
||||
if players.erase(peer_id):
|
||||
player_erased.emit(name, peer_id)
|
||||
return true
|
||||
return false
|
||||
|
||||
## This method pops out the related [param peer_id] string.
|
||||
func pop(peer_id: int) -> String:
|
||||
var value : String = players[peer_id]
|
||||
erase(peer_id)
|
||||
return value
|
||||
|
||||
## Returns the size of the team.
|
||||
func size() -> int:
|
||||
return players.size()
|
||||
|
||||
func serialize() -> Variant:
|
||||
return [name, players]
|
||||
|
||||
func _to_string() -> String:
|
||||
return "<Team(%s)#%s>" % [name, get_instance_id()]
|
||||
144
types/multiplayer/teams.gd
Normal file
144
types/multiplayer/teams.gd
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
## This class defines a Teams manager
|
||||
class_name Teams extends Node
|
||||
|
||||
## Emitted when a [Team] is created.
|
||||
signal team_added(team_name : String)
|
||||
|
||||
## Emitted when a [Team] is removed.
|
||||
signal team_erased(team_name : String)
|
||||
|
||||
## Emitted when a [Player] is added to a [Team].
|
||||
signal player_added(team_name: String, peer_id: int, username: String)
|
||||
|
||||
## Emitted when a [Player] is removed from a [Team].
|
||||
signal player_erased(team_name: String, peer_id: int)
|
||||
|
||||
# The synced teams state.
|
||||
@export var _state : Dictionary = {}:
|
||||
get = get_state, set = set_state
|
||||
|
||||
func get_state() -> Dictionary:
|
||||
for team : Team in _teams.values():
|
||||
_state[team.name] = team.players
|
||||
return _state
|
||||
|
||||
func set_state(new_state: Dictionary) -> void:
|
||||
_state = new_state
|
||||
|
||||
# The internal dictionary of teams.
|
||||
var _teams : Dictionary = {}
|
||||
|
||||
## Retrieves an [Array] of [int] by [param team_name].
|
||||
## [codeblock]
|
||||
## var teams := Teams.new()
|
||||
## teams.add_team("Phoenix")
|
||||
## print(teams["Phoenix"]) # this calls `_get`
|
||||
## [/codeblock]
|
||||
func _get(team_name : StringName) -> Variant:
|
||||
return _teams.get(team_name)
|
||||
|
||||
## Sets a [Team] by [param name] with an optional [param default].
|
||||
## [codeblock]
|
||||
## var teams := Teams.new()
|
||||
## teams["Phoenix"] = Teams.Team.new("Phoenix") # this calls `_set`
|
||||
## teams["Phoenix"] = null # refcount for Team is now 0
|
||||
## [/codeblock]
|
||||
func _set(key : StringName, team : Variant) -> bool:
|
||||
if team == null:
|
||||
return erase(key)
|
||||
if team is Team:
|
||||
_teams[key] = team
|
||||
team_added.emit(key)
|
||||
return true
|
||||
return false
|
||||
|
||||
## This method retrieves the array of managed [Team] names.
|
||||
func keys() -> Array:
|
||||
return _teams.keys()
|
||||
|
||||
## This method retrieves the array of managed [Team].
|
||||
func values() -> Array:
|
||||
return _teams.values()
|
||||
|
||||
# This is the iterator index cursor.
|
||||
var _iter_cursor : int = 0
|
||||
|
||||
# This method is an iterator initializer.
|
||||
func _iter_init(_arg : Variant) -> bool:
|
||||
_iter_cursor = 0 # reset
|
||||
return _iter_cursor < len(_teams)
|
||||
|
||||
# This method checks if the iterator has a next value.
|
||||
func _iter_next(_arg : Variant) -> bool:
|
||||
_iter_cursor += 1
|
||||
return _iter_cursor < len(_teams)
|
||||
|
||||
# This method gets the next iterator value.
|
||||
func _iter_get(_arg : Variant) -> Team:
|
||||
return _teams.values()[_iter_cursor]
|
||||
|
||||
## This method adds a new [Team] into the manager.
|
||||
func add_team(key: String) -> bool:
|
||||
if not _teams.has(key):
|
||||
var team : Team = Team.new(key)
|
||||
team.player_added.connect(_on_team_player_added)
|
||||
team.player_erased.connect(_on_team_player_erased)
|
||||
return _set(key, team)
|
||||
return false
|
||||
|
||||
## This method adds new [Team] in batch from [param team_names].
|
||||
func add_teams(team_names: Array[String]) -> Node:
|
||||
for team_name in team_names:
|
||||
add_team(team_name)
|
||||
return self
|
||||
|
||||
func get_peer_team(peer_id : int) -> Team:
|
||||
for team : Team in _teams.values():
|
||||
if peer_id in team.players:
|
||||
return team
|
||||
return null
|
||||
|
||||
func erase(team_name: String) -> bool:
|
||||
var res: bool = _teams.erase(team_name)
|
||||
if res:
|
||||
team_erased.emit(team_name)
|
||||
return res
|
||||
|
||||
func erase_player(peer_id: int) -> void:
|
||||
for team : Team in _teams.values():
|
||||
if team.erase(peer_id):
|
||||
break
|
||||
|
||||
## The number of [Team].
|
||||
func size() -> int:
|
||||
return len(_teams)
|
||||
|
||||
## This method eases peer team switching by moving them to the specified [param team_name].
|
||||
## It does not do anything if the peer is not already added to a [Team], see [method Team.add].
|
||||
func switch_team(peer_id: int, team_name: String) -> void:
|
||||
var lhs : Team = get_peer_team(peer_id)
|
||||
var rhs : Team = _get(team_name)
|
||||
if lhs and rhs:
|
||||
rhs.add(peer_id, lhs.pop(peer_id))
|
||||
|
||||
# This method runs when a Team emits `player_added` signal
|
||||
func _on_team_player_added(team_name: String, peer_id: int, username: String) -> void:
|
||||
player_added.emit(team_name, peer_id, username) # emit manager signal
|
||||
|
||||
# This method runs when a Team emits `player_erased` signal
|
||||
func _on_team_player_erased(team_name: String, peer_id: int) -> void:
|
||||
player_erased.emit(team_name, peer_id) # emit manager signal
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
class_name ArrayPackedSceneResource extends Resource
|
||||
|
||||
@export var _packed_scenes : Array[PackedScene] = []
|
||||
|
||||
func get_items() -> Array[PackedScene]:
|
||||
return _packed_scenes
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
[gd_resource type="Resource" script_class="ArrayPackedSceneResource" load_steps=2 format=3 uid="uid://cpnargstkucch"]
|
||||
|
||||
[ext_resource type="Script" path="res://types/resources/array_packed_scene.gd" id="1_r1ygd"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_r1ygd")
|
||||
_packed_scenes = Array[PackedScene]([])
|
||||
35
types/singleplayer/demo.gd
Normal file
35
types/singleplayer/demo.gd
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# This file is part of open-fpsz.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
class_name Singleplayer extends Node
|
||||
|
||||
@onready var player : Player = $Player
|
||||
@onready var flag : Flag = $Flag
|
||||
@onready var map : Node = $Desert
|
||||
|
||||
func _ready() -> void:
|
||||
player.health.exhausted.connect(_on_player_dead)
|
||||
var flagstand : Marker3D = map.get_node("FlagStand")
|
||||
if flagstand:
|
||||
flag.global_position = flagstand.position
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("exit"):
|
||||
queue_free()
|
||||
|
||||
func _on_player_dead(_player : Player) -> void:
|
||||
var spawns : Node = map.get_node("PlayerSpawns")
|
||||
var spawn : Marker3D = spawns.get_child(
|
||||
randi_range(0, spawns.get_child_count() - 1))
|
||||
player.respawn(spawn.global_position)
|
||||
27
types/singleplayer/demo.tscn
Normal file
27
types/singleplayer/demo.tscn
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[gd_scene load_steps=7 format=3 uid="uid://boviiugcnfyrj"]
|
||||
|
||||
[ext_resource type="Script" path="res://types/singleplayer/demo.gd" id="1_kkjqs"]
|
||||
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_6wbjq"]
|
||||
[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="4_1j2pw"]
|
||||
[ext_resource type="PackedScene" uid="uid://btlkog4b87p4x" path="res://maps/desert/desert.tscn" id="4_dogmv"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpnu1lvfncx6q" path="res://entities/dummy_target/dummy_target.tscn" id="5_euor2"]
|
||||
|
||||
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_c5jqv"]
|
||||
resource_local_to_scene = true
|
||||
bounce = 1.0
|
||||
absorbent = true
|
||||
|
||||
[node name="Demo" type="Node"]
|
||||
script = ExtResource("1_kkjqs")
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("2_6wbjq")]
|
||||
transform = Transform3D(-0.000506087, 0, -1, 0, 1, 0, 1, 0, -0.000506087, 612.497, 211.292, 853.632)
|
||||
physics_material_override = SubResource("PhysicsMaterial_c5jqv")
|
||||
|
||||
[node name="Desert" parent="." instance=ExtResource("4_dogmv")]
|
||||
|
||||
[node name="DummyTarget" parent="." instance=ExtResource("5_euor2")]
|
||||
transform = Transform3D(-0.997996, 0, -0.0632782, 0, 1, 0, 0.0632782, 0, -0.997996, 901.962, 145.367, 882.65)
|
||||
|
||||
[node name="Flag" parent="." instance=ExtResource("4_1j2pw")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 895.356, 145.367, 888.261)
|
||||
Loading…
Add table
Add a link
Reference in a new issue