mirror of
https://codeberg.org/sunder/sunder.git
synced 2026-04-29 05:05:22 +00:00
first commit 🎉
This commit is contained in:
commit
6e724f67fe
805 changed files with 62098 additions and 0 deletions
53
scripts/multiplayer/extensions/log.gd
Normal file
53
scripts/multiplayer/extensions/log.gd
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
extends MultiplayerAPIExtension
|
||||
class_name LogMultiplayer
|
||||
|
||||
# We want to extend the default SceneMultiplayer.
|
||||
var base_multiplayer = SceneMultiplayer.new()
|
||||
|
||||
func _init():
|
||||
# Just passthrough base signals (copied to var to avoid cyclic reference)
|
||||
var cts = connected_to_server
|
||||
var cf = connection_failed
|
||||
var pc = peer_connected
|
||||
var pd = peer_disconnected
|
||||
base_multiplayer.connected_to_server.connect(func(): cts.emit())
|
||||
base_multiplayer.connection_failed.connect(func(): cf.emit())
|
||||
base_multiplayer.peer_connected.connect(func(id): pc.emit(id))
|
||||
base_multiplayer.peer_disconnected.connect(func(id): pd.emit(id))
|
||||
|
||||
func _poll():
|
||||
return base_multiplayer.poll()
|
||||
|
||||
# Log RPC being made and forward it to the default multiplayer.
|
||||
func _rpc(peer: int, object: Object, method: StringName, args: Array) -> Error:
|
||||
print("Got RPC for %d: %s::%s(%s)" % [peer, object, method, args])
|
||||
return base_multiplayer.rpc(peer, object, method, args)
|
||||
|
||||
# Log configuration add. E.g. root path (nullptr, NodePath), replication (Node, Spawner|Synchronizer), custom.
|
||||
func _object_configuration_add(object, config: Variant) -> Error:
|
||||
if config is MultiplayerSynchronizer:
|
||||
print("Adding synchronization configuration for %s. Synchronizer: %s" % [object, config])
|
||||
elif config is MultiplayerSpawner:
|
||||
print("Adding node %s to the spawn list. Spawner: %s" % [object, config])
|
||||
return base_multiplayer.object_configuration_add(object, config)
|
||||
|
||||
# Log configuration remove. E.g. root path (nullptr, NodePath), replication (Node, Spawner|Synchronizer), custom.
|
||||
func _object_configuration_remove(object, config: Variant) -> Error:
|
||||
if config is MultiplayerSynchronizer:
|
||||
print("Removing synchronization configuration for %s. Synchronizer: %s" % [object, config])
|
||||
elif config is MultiplayerSpawner:
|
||||
print("Removing node %s from the spawn list. Spawner: %s" % [object, config])
|
||||
return base_multiplayer.object_configuration_remove(object, config)
|
||||
|
||||
# These can be optional, but in our case we want to extend SceneMultiplayer, so forward everything.
|
||||
func _set_multiplayer_peer(p_peer: MultiplayerPeer):
|
||||
base_multiplayer.multiplayer_peer = p_peer
|
||||
|
||||
func _get_multiplayer_peer() -> MultiplayerPeer:
|
||||
return base_multiplayer.multiplayer_peer
|
||||
|
||||
func _get_unique_id() -> int:
|
||||
return base_multiplayer.get_unique_id()
|
||||
|
||||
func _get_peer_ids() -> PackedInt32Array:
|
||||
return base_multiplayer.get_peers()
|
||||
1
scripts/multiplayer/extensions/log.gd.uid
Normal file
1
scripts/multiplayer/extensions/log.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dfc88e4xhn53e
|
||||
87
scripts/multiplayer/extensions/match.gd
Normal file
87
scripts/multiplayer/extensions/match.gd
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
class_name MultiplayerMatchExtension extends MultiplayerAPIExtension
|
||||
|
||||
var _multiplayer := SceneMultiplayer.new()
|
||||
|
||||
## The maximum number of concurrent connections to accept
|
||||
@export_range(1, 4095) var max_peers:int = 32
|
||||
|
||||
signal scene_changed
|
||||
|
||||
func _init() -> void:
|
||||
# Just passthrough base signals (copied to var to avoid cyclic reference)
|
||||
var cs := connected_to_server
|
||||
var cf := connection_failed
|
||||
var pc := peer_connected
|
||||
var pd := peer_disconnected
|
||||
_multiplayer.connected_to_server.connect(cs.emit)
|
||||
_multiplayer.connection_failed.connect(cf.emit)
|
||||
_multiplayer.peer_connected.connect(pc.emit)
|
||||
_multiplayer.peer_disconnected.connect(pd.emit)
|
||||
|
||||
func _poll() -> Error:
|
||||
return _multiplayer.poll()
|
||||
|
||||
# Log RPC being made and forward it to the default multiplayer.
|
||||
func _rpc(peer:int, object:Object, method:StringName, args:Array) -> Error:
|
||||
print("Got RPC for %d: %s::%s(%s)" % [peer, object, method, args])
|
||||
return _multiplayer.rpc(peer, object, method, args)
|
||||
|
||||
## Log configuration add. E.g. root path (nullptr, NodePath), replication (Node, Spawner|Synchronizer), custom.
|
||||
#func _object_configuration_add(object, config: Variant) -> Error:
|
||||
#if config is MultiplayerSynchronizer:
|
||||
#print("Adding synchronization configuration for %s. Synchronizer: %s" % [object, config])
|
||||
#elif config is MultiplayerSpawner:
|
||||
#print("Adding node %s to the spawn list. Spawner: %s" % [object, config])
|
||||
#return _multiplayer.object_configuration_add(object, config)
|
||||
#
|
||||
## Log configuration remove. E.g. root path (nullptr, NodePath), replication (Node, Spawner|Synchronizer), custom.
|
||||
#func _object_configuration_remove(object, config: Variant) -> Error:
|
||||
#if config is MultiplayerSynchronizer:
|
||||
#print("Removing synchronization configuration for %s. Synchronizer: %s" % [object, config])
|
||||
#elif config is MultiplayerSpawner:
|
||||
#print("Removing node %s from the spawn list. Spawner: %s" % [object, config])
|
||||
#return _multiplayer.object_configuration_remove(object, config)
|
||||
|
||||
# These can be optional, but in our case we want to extend SceneMultiplayer, so forward everything.
|
||||
func _set_multiplayer_peer(p_peer:MultiplayerPeer) -> void:
|
||||
_multiplayer.multiplayer_peer = p_peer
|
||||
|
||||
func _get_multiplayer_peer() -> MultiplayerPeer:
|
||||
return _multiplayer.multiplayer_peer
|
||||
|
||||
func _get_unique_id() -> int:
|
||||
return _multiplayer.get_unique_id()
|
||||
|
||||
func _get_remote_sender_id() -> int:
|
||||
return _multiplayer.get_remote_sender_id()
|
||||
|
||||
func _get_peer_ids() -> PackedInt32Array:
|
||||
return _multiplayer.get_peers()
|
||||
|
||||
## Creates a server peer and listens for connections on all intefaces for specifed [param port]
|
||||
func serve(port:int = 9000) -> Error:
|
||||
var peer:ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
||||
peer_connected.connect(_on_peer_connected)
|
||||
peer_disconnected.connect(_on_peer_disconnected)
|
||||
var err:Error = peer.create_server(port, max_peers)
|
||||
if err == Error.OK:
|
||||
print("listening for connections on *:%s" % port)
|
||||
_set_multiplayer_peer(peer)
|
||||
return err
|
||||
|
||||
## Creates a client peer and connects to specified [param host] and [param port]
|
||||
func join(host:String = "localhost", port:int = 9000) -> void:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
peer.create_client(host, port)
|
||||
if peer.get_connection_status() == MultiplayerPeer.CONNECTION_DISCONNECTED:
|
||||
push_error("failed to start multiplayer client")
|
||||
return
|
||||
_set_multiplayer_peer(peer)
|
||||
|
||||
|
||||
## Emitted when a multiplayer peer successfully connects to a server. Only emitted on the server.
|
||||
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)
|
||||
1
scripts/multiplayer/extensions/match.gd.uid
Normal file
1
scripts/multiplayer/extensions/match.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cae2lsprl15be
|
||||
98
scripts/multiplayer/fifo.gd
Normal file
98
scripts/multiplayer/fifo.gd
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
class_name OldQueue
|
||||
extends RefCounted
|
||||
|
||||
var capacity:int
|
||||
var head:int = 0
|
||||
var tail:int = 0
|
||||
var _block_size:int
|
||||
var _data:Array = []
|
||||
var _length:int = 0
|
||||
|
||||
func _init(p_capacity:int = 128) -> void:
|
||||
_block_size = p_capacity
|
||||
capacity = _block_size
|
||||
var _block := []
|
||||
_block.resize(_block_size)
|
||||
_data = [_block]
|
||||
_data.resize(len(_data))
|
||||
|
||||
func _expand() -> void:
|
||||
capacity += _block_size
|
||||
var _block := []
|
||||
_block.resize(_block_size)
|
||||
_data.resize(len(_data) + 1)
|
||||
|
||||
func enqueue(item:Variant) -> void:
|
||||
if (_length + 1) % capacity == 0:
|
||||
_expand()
|
||||
@warning_ignore("integer_division")
|
||||
_data[tail / _block_size][tail % _block_size] = item
|
||||
tail = (tail + 1) % capacity
|
||||
_length += 1
|
||||
|
||||
func dequeue(shrink:bool = true) -> Variant:
|
||||
if is_empty():
|
||||
return null
|
||||
@warning_ignore("integer_division")
|
||||
var item:Variant = _data[head / _block_size][head % _block_size]
|
||||
head = (head + 1) % capacity
|
||||
_length -= 1
|
||||
if shrink:
|
||||
_shrink()
|
||||
return item
|
||||
|
||||
func _shrink() -> void:
|
||||
if (_data.size() <= 1) or (capacity <= _block_size) or (_length > capacity - _block_size):
|
||||
return
|
||||
|
||||
prints("old head / tail", head, tail)
|
||||
|
||||
var lhs:int = _block_size - (head % _block_size)
|
||||
var rhs:int = abs((tail % _block_size) - _block_size)
|
||||
var head_block:int = head / _block_size
|
||||
var tail_block:int = tail / _block_size
|
||||
var move_tail:bool = (head < tail and lhs > rhs) or (head > tail and lhs >= rhs)
|
||||
|
||||
if move_tail:
|
||||
for i in range(rhs):
|
||||
_data[head_block][i] = _data[tail_block][i]
|
||||
tail = (tail - rhs + capacity) % capacity
|
||||
_data.erase(tail_block)
|
||||
else:
|
||||
for i in range(lhs):
|
||||
enqueue(dequeue(false))
|
||||
_data.erase(head_block)
|
||||
head = (head + lhs) % capacity
|
||||
|
||||
prints("new head / tail", head, tail)
|
||||
capacity -= _block_size
|
||||
_data.resize(capacity)
|
||||
|
||||
func peek() -> Variant:
|
||||
if is_empty():
|
||||
return null
|
||||
return _data[head]
|
||||
|
||||
func is_empty() -> bool:
|
||||
return head == tail
|
||||
|
||||
func size() -> int:
|
||||
return _length
|
||||
|
||||
# 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 = head # reset
|
||||
return head != tail
|
||||
|
||||
# This method checks if the iterator has a next value.
|
||||
func _iter_next(_arg:Variant) -> bool:
|
||||
_iter_cursor = (_iter_cursor + 1) % capacity
|
||||
return _iter_cursor < _length
|
||||
|
||||
# This method gets the next iterator value.
|
||||
func _iter_get(_arg:Variant) -> Variant:
|
||||
@warning_ignore("integer_division")
|
||||
return _data[_iter_cursor / _block_size][_iter_cursor % _block_size]
|
||||
1
scripts/multiplayer/fifo.gd.uid
Normal file
1
scripts/multiplayer/fifo.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://qwgu1g1yifsl
|
||||
33
scripts/multiplayer/motion.gd
Normal file
33
scripts/multiplayer/motion.gd
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
class_name MotionPack extends RefCounted
|
||||
|
||||
static var tick:int = 0
|
||||
var direction := Vector2.ZERO
|
||||
var mask:Array[bool]
|
||||
|
||||
func _init() -> void:
|
||||
mask.resize(256)
|
||||
mask.fill(false)
|
||||
|
||||
func pack() -> PackedByteArray:
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.put_32(tick)
|
||||
buffer.put_float(direction.x)
|
||||
buffer.put_float(direction.y)
|
||||
# bitpack mask array into single byte
|
||||
var packed:int = 0
|
||||
for i in mask.size():
|
||||
if mask[i]: packed |= (1 << i)
|
||||
buffer.put_u8(packed)
|
||||
return buffer.data_array
|
||||
|
||||
static func unpack(data:PackedByteArray) -> MotionPack:
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
buffer.data_array = data
|
||||
var move := MotionPack.new()
|
||||
move.tick = buffer.get_32()
|
||||
move.direction = Vector2(buffer.get_float(), buffer.get_float())
|
||||
# unpack mask array
|
||||
var packed:int = buffer.get_u8()
|
||||
for i in range(256): # 0 to 255
|
||||
move.mask[i] = bool(packed & (1 << i))
|
||||
return move
|
||||
1
scripts/multiplayer/motion.gd.uid
Normal file
1
scripts/multiplayer/motion.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b6mtcibai0qaa
|
||||
81
scripts/multiplayer/motion_service.gd
Normal file
81
scripts/multiplayer/motion_service.gd
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
class_name MotionService extends RefCounted
|
||||
|
||||
@export var RATE_HZ := 64
|
||||
|
||||
var running := false
|
||||
|
||||
var delta:float:
|
||||
get: return 1.0 / RATE_HZ
|
||||
set(value): pass
|
||||
|
||||
var _threads:Dictionary[int, Thread]
|
||||
var _clock:Thread
|
||||
var _semaphore:Semaphore
|
||||
var _mutex:Mutex
|
||||
|
||||
var tick := 0:
|
||||
set(value):
|
||||
tick = value % RATE_HZ
|
||||
|
||||
func _init() -> void:
|
||||
_clock = Thread.new()
|
||||
_mutex = Mutex.new()
|
||||
_semaphore = Semaphore.new()
|
||||
#multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
#multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
|
||||
func register(id:int) -> void:
|
||||
if !running:
|
||||
running = true
|
||||
_clock.start(_loop)
|
||||
prints("motion_service: starting clock")
|
||||
prints("motion_service: register peer", id)
|
||||
_threads[id] = Thread.new()
|
||||
_threads[id].start(_worker.bind(id))
|
||||
|
||||
func unregister(id:int) -> void:
|
||||
if running:
|
||||
prints("motion_service: unregistering peer", id)
|
||||
if _threads[id].is_alive():
|
||||
_threads[id].wait_to_finish()
|
||||
_threads.erase(id)
|
||||
if len(_threads) == 0:
|
||||
running = false
|
||||
if _clock.is_alive():
|
||||
_clock.wait_to_finish()
|
||||
_clock = null
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if running:
|
||||
for id:int in _threads.keys():
|
||||
unregister(id)
|
||||
|
||||
func _loop() -> void:
|
||||
var next_time:float = Time.get_ticks_msec() / 1000.
|
||||
while running:
|
||||
_semaphore.post(len(_threads))
|
||||
_mutex.lock()
|
||||
next_time += delta
|
||||
_mutex.unlock()
|
||||
var sleep_time:float = next_time - (Time.get_ticks_msec() / 1000.)
|
||||
if sleep_time > 0:
|
||||
# delay current thread
|
||||
OS.delay_msec(int(sleep_time * 1000))
|
||||
else:
|
||||
_mutex.lock()
|
||||
# we are behind; reset next_time to avoid drift
|
||||
next_time = Time.get_ticks_msec() / 1000.
|
||||
_mutex.unlock()
|
||||
|
||||
_mutex.lock()
|
||||
tick += 1
|
||||
_mutex.unlock()
|
||||
|
||||
func _worker(peer_id:int) -> void:
|
||||
while running:
|
||||
_semaphore.wait() # wait until posted
|
||||
#prints("tick", tick, "for peer", peer_id)
|
||||
|
||||
#var player:Player = owner.players.get_node(str(peer_id))
|
||||
#if player:
|
||||
#player.send_position.rpc(player.global_position)
|
||||
1
scripts/multiplayer/motion_service.gd.uid
Normal file
1
scripts/multiplayer/motion_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://gejmvnktjldp
|
||||
291
scripts/multiplayer/multiplayer.gd
Normal file
291
scripts/multiplayer/multiplayer.gd
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero 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 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
|
||||
}
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# Sets our custom multiplayer as the main one in SceneTree.
|
||||
get_tree().set_multiplayer(MultiplayerMatchExtension.new())
|
||||
multiplayer.hello()
|
||||
|
||||
## The [Match] manager.
|
||||
@export var match:Match
|
||||
## The [Teams] manager.
|
||||
@export var teams:Teams
|
||||
|
||||
var motion_service:MotionService
|
||||
|
||||
## The scoreboard to keep track of scores.
|
||||
@export var scoreboard:Scoreboard
|
||||
### The multiplayer mode.
|
||||
#@export var mode:Mode = Mode.FREE_FOR_ALL
|
||||
## 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
|
||||
|
||||
## The spawn root for [Player] nodes.
|
||||
@onready var players:Node = $Match/Players
|
||||
## The spawn root for [Flag] nodes.
|
||||
@onready var objectives:Node = $Match/Objectives
|
||||
|
||||
@onready var player_spawner:MultiplayerSpawner = $Match/PlayerSpawner
|
||||
@onready var map_spawner:MultiplayerSpawner = $Match/MapSpawner
|
||||
@onready var objectives_spawner:MultiplayerSpawner = $Match/ObjectiveSpawner
|
||||
|
||||
func _ready() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player_spawner.spawn_function = _on_player_spawn
|
||||
map_spawner.spawn_function = _on_map_spawn
|
||||
|
||||
func _on_player_spawn(data:Dictionary) -> Player:
|
||||
var player:Player = _PLAYER.instantiate()
|
||||
# @NOTE: MultiplayerSpawner needs valid names so we need to set the node name
|
||||
# > 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.peer_id = data.peer_id
|
||||
player.name = str(player.peer_id)
|
||||
player.username = data.username
|
||||
if "global_position" in data:
|
||||
player.set_deferred("global_position", data.global_position)
|
||||
return player
|
||||
|
||||
func _on_map_spawn(data:Variant = null) -> Map:
|
||||
return Game.maps[data.index].instantiate()
|
||||
|
||||
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
|
||||
|
||||
## This method starts a server.
|
||||
func start_server(port:int, _p_mode:int = 0, map_index:int = 0, _username:String = "Mercury") -> void:
|
||||
#mode = p_mode
|
||||
motion_service = MotionService.new()
|
||||
|
||||
#multiplayer.peer_connected.connect(motion_service.register)
|
||||
#multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
#multiplayer.peer_disconnected.connect(motion_service.unregister)
|
||||
Game.network.serve(port)
|
||||
Game.ping.synchronized.connect(match.scoreboard._on_ping_sync)
|
||||
Game.ping.start()
|
||||
|
||||
map_spawner.spawn({ "index": map_index })
|
||||
|
||||
# start ping timer to ping connected clients and synchronize its state
|
||||
|
||||
#match mode:
|
||||
#Mode.RABBIT:
|
||||
#var flag:Flag = _FLAG.instantiate()
|
||||
#objectives.add_child(flag)
|
||||
#var spawn:Node3D = map.get_spawn_position("objectives")
|
||||
#flag.global_position = spawn.global_position
|
||||
#flag.respawn_timer.timeout.connect(func() -> void:
|
||||
#if spawn:
|
||||
#flag.waypoint.text = ""
|
||||
#flag.global_position = spawn.global_position
|
||||
#flag.state = flag.FlagState.ON_STAND
|
||||
#)
|
||||
#
|
||||
#teams.team_added.connect(_on_team_added)
|
||||
#teams.team_erased.connect(_on_team_erased)
|
||||
#teams.add_teams(["rabbit", "chasers"])
|
||||
#flag.grabbed.connect(
|
||||
#func(carry:FlagHolder) -> void:
|
||||
#carry.owner.hud.objective_label.set_visible(true)
|
||||
#switch_team(carry.owner.peer_id, "rabbit")
|
||||
#flag.respawn_timer.stop()
|
||||
#)
|
||||
#flag.dropped.connect(
|
||||
#func(carry:FlagHolder) -> void:
|
||||
#carry.owner.hud.objective_label.set_visible(false)
|
||||
#switch_team(carry.owner.peer_id, "chasers")
|
||||
#flag.respawn_timer.start()
|
||||
#)
|
||||
#scoreboard.add_child(
|
||||
#RabbitScoringComponent.new(scoreboard, flag))
|
||||
#scoreboard.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.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_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_holder.flag
|
||||
#player.flag_holder.drop.rpc_id(get_multiplayer_authority())
|
||||
#var spawn:Node3D = map.get_objective_spawn()
|
||||
#flag.global_position = spawn.global_position
|
||||
#scoreboard.reset_scores()
|
||||
#timer.start(MATCH_DURATION) # restart timer with match duration
|
||||
#
|
||||
#players.respawn() # respawn everyone
|
||||
#for player:Player in players:
|
||||
#player.respawn.rpc_id(get_multiplayer_authority(), Game.map.get_spawn_position("players"))
|
||||
#
|
||||
#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_holder.flag
|
||||
#player.flag_holder.drop.rpc_id(get_multiplayer_authority())
|
||||
#var spawn:Node3D = map.get_objective_spawn()
|
||||
#flag.global_position = spawn.global_position
|
||||
## restart match
|
||||
#_start_match()
|
||||
|
||||
func join_server(host:String, port:int, username:String) -> void:
|
||||
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)
|
||||
Game.network.join(host, port)
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
Game.change_scene() # back to boot menu
|
||||
|
||||
func add_player(peer_id:int, username:String) -> void:
|
||||
player_spawner.spawn({
|
||||
"peer_id": peer_id,
|
||||
"username": username,
|
||||
# @NOTE: match would include map and teams to deduce where to spawn
|
||||
#"global_position": party.get_spawn(peer_id)
|
||||
})
|
||||
|
||||
func _on_connected_to_server(username:String) -> void:
|
||||
_join_match.rpc_id(get_multiplayer_authority(), 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 = Game.map.get_player_spawn()
|
||||
victim.respawn.rpc_id(get_multiplayer_authority(), 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():
|
||||
var peer_id:int = multiplayer.get_remote_sender_id()
|
||||
# make sure remote peer cannot add more than one player
|
||||
if not players.has_node(str(peer_id)):
|
||||
# spawn player on all connected peers
|
||||
add_player(peer_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)
|
||||
|
||||
# @TODO: verify that a peer disconnecting out of
|
||||
# network problems drops the flag
|
||||
if player.has_flag():
|
||||
player.flag_holder.drop()
|
||||
|
||||
players.remove_child(player)
|
||||
player.queue_free()
|
||||
|
||||
multiplayer.disconnect_peer(peer_id)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if not is_multiplayer_authority():
|
||||
_leave_match.rpc_id(get_multiplayer_authority())
|
||||
1
scripts/multiplayer/multiplayer.gd.uid
Normal file
1
scripts/multiplayer/multiplayer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d4c6vseetdfpi
|
||||
53
scripts/multiplayer/network.gd
Normal file
53
scripts/multiplayer/network.gd
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
class_name Network extends Node
|
||||
|
||||
## The maximum number of concurrent connections to accept
|
||||
@export_range(1, 4095) var max_peers:int = 32
|
||||
|
||||
## Emitted when a [MultiplayerPeer] sends a [member leave] rpc, right before it is disconnected
|
||||
signal peer_disconnecting(peer_id:int)
|
||||
|
||||
## Creates a server peer and listens for connections on all intefaces for specifed [param port]
|
||||
func serve(port:int = 9000) -> void:
|
||||
var peer:ENetMultiplayerPeer = ENetMultiplayerPeer.new()
|
||||
var err:Error = peer.create_server(port, max_peers)
|
||||
if err != Error.OK:
|
||||
Game.change_scene(Game.GameType.NONE)
|
||||
return
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
print("listening for connections on *:%s" % port)
|
||||
|
||||
## Creates a client peer and connects to specified [param host] and [param port]
|
||||
func join(host:String = "localhost", port:int = 9000) -> void:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
peer.create_client(host, port)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
#@rpc("any_peer", "call_remote", "reliable")
|
||||
#func leave() -> void:
|
||||
#if multiplayer.is_server():
|
||||
#var peer_id:int = multiplayer.get_remote_sender_id()
|
||||
#peer_disconnecting.emit(peer_id)
|
||||
#multiplayer.disconnect_peer(peer_id)
|
||||
|
||||
## Emitted when a multiplayer peer successfully connects to a server. Only emitted on the server.
|
||||
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)
|
||||
1
scripts/multiplayer/network.gd.uid
Normal file
1
scripts/multiplayer/network.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b38or3gq60cdq
|
||||
145
scripts/multiplayer/ping.gd
Normal file
145
scripts/multiplayer/ping.gd
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero 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 synchronize the state 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]
|
||||
@icon("res://assets/icons/ping.svg")
|
||||
class_name Ping extends Timer
|
||||
|
||||
## Emitted when the state is synchronized.
|
||||
signal synchronized(state : Dictionary)
|
||||
|
||||
## The size of the ping history buffer used to store past ping times for each client.
|
||||
@export var ping_history: int = 8:
|
||||
set = set_ping_history
|
||||
|
||||
## The ping state being synchronized to connected peers
|
||||
@export var _state := {}
|
||||
|
||||
## The dictionary of [PingInfo] for each connected peer id
|
||||
var _infos := {}
|
||||
|
||||
## The array of times when a ping request occured
|
||||
var _pings := []
|
||||
var _last_ping := 0
|
||||
|
||||
## This class defines a ping information to keep track of ping results
|
||||
## and provide average ping.
|
||||
class PingInfo:
|
||||
const SIZE := 32
|
||||
var samples := PackedInt32Array()
|
||||
var pos := 0
|
||||
|
||||
func _init() -> void:
|
||||
samples.resize(SIZE)
|
||||
samples.fill(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
|
||||
|
||||
func _ready() -> void:
|
||||
clear_pings()
|
||||
multiplayer.peer_connected.connect(_add_peer)
|
||||
multiplayer.peer_disconnected.connect(_del_peer)
|
||||
timeout.connect(_on_timeout)
|
||||
|
||||
func _on_timeout() -> void:
|
||||
if _infos:
|
||||
# check and update ping intervals
|
||||
var now := Time.get_ticks_msec()
|
||||
if now >= _pings[_last_ping] + wait_time:
|
||||
_last_ping = (_last_ping + 1) % ping_history
|
||||
_pings[_last_ping] = now
|
||||
_ping.rpc(now)
|
||||
# reset state
|
||||
_state.clear()
|
||||
# iterate over registered clients
|
||||
for peer_id:int in _infos:
|
||||
# set client average ping state
|
||||
_state[peer_id] = _infos[peer_id].get_average()
|
||||
# send state to connected peers
|
||||
_synchronize.rpc(_state)
|
||||
|
||||
## 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:
|
||||
_infos[peer_id] = PingInfo.new()
|
||||
|
||||
## Deletes a peer from the registry
|
||||
func _del_peer(peer_id: int) -> void:
|
||||
_infos.erase(peer_id)
|
||||
|
||||
@rpc("authority", "call_remote", "unreliable")
|
||||
func _ping(time: int) -> void:
|
||||
_pong.rpc_id(get_multiplayer_authority(), time)
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable")
|
||||
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 _infos.has(peer_id):
|
||||
return
|
||||
# init variables
|
||||
var now := Time.get_ticks_msec()
|
||||
var last := (_last_ping + 1) % ping_history
|
||||
var found := ping_history * wait_time
|
||||
# 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
|
||||
_infos[peer_id].set_next((now - found) / 2)
|
||||
|
||||
@rpc("authority", "call_local", "unreliable")
|
||||
func _synchronize(state: Dictionary) -> void:
|
||||
_state = state
|
||||
synchronized.emit(_state)
|
||||
1
scripts/multiplayer/ping.gd.uid
Normal file
1
scripts/multiplayer/ping.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cyobq87tprm0d
|
||||
55
scripts/multiplayer/server.gd
Normal file
55
scripts/multiplayer/server.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
extends Node
|
||||
|
||||
const TICK_RATE := 128
|
||||
const TICK_INTERVAL := 1.0 / TICK_RATE
|
||||
|
||||
var tick_timer := 0.0
|
||||
var server_time := 0.0
|
||||
|
||||
# Keep track of players
|
||||
var players := {}
|
||||
|
||||
func _start_server(port:int = 9000) -> void:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
peer.create_server(port, 1024)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
prints("server started on port", port)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
tick_timer += delta
|
||||
while tick_timer >= TICK_INTERVAL:
|
||||
_server_tick()
|
||||
tick_timer -= TICK_INTERVAL
|
||||
|
||||
func _server_tick() -> void:
|
||||
server_time += TICK_INTERVAL
|
||||
# for each player, update logic
|
||||
for id:int in players.keys():
|
||||
var player:Player = players[id]
|
||||
player["position"] += player["velocity"] * TICK_INTERVAL
|
||||
# broadcast game state to all clients
|
||||
update_player_states.rpc(players)
|
||||
|
||||
func _on_peer_connected(peer_id:int) -> void:
|
||||
print("Player connected:", peer_id)
|
||||
players[peer_id] = {
|
||||
"position": Vector2.ZERO,
|
||||
"velocity": Vector2.ZERO
|
||||
}
|
||||
|
||||
func _on_peer_disconnected(peer_id:int) -> void:
|
||||
print("Player disconnected:", peer_id)
|
||||
players.erase(peer_id)
|
||||
|
||||
@rpc("any_peer")
|
||||
func send_input(input_velocity: Vector2) -> void:
|
||||
var id:int = multiplayer.get_remote_sender_id()
|
||||
if players.has(id):
|
||||
players[id]["velocity"] = input_velocity
|
||||
|
||||
@rpc("authority", "unreliable")
|
||||
func update_player_states(state:Array[Dictionary]) -> void:
|
||||
print(state)
|
||||
pass # No-op on server
|
||||
1
scripts/multiplayer/server.gd.uid
Normal file
1
scripts/multiplayer/server.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://con7k2g4jmaht
|
||||
85
scripts/multiplayer/team.gd
Normal file
85
scripts/multiplayer/team.gd
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero 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()]
|
||||
1
scripts/multiplayer/team.gd.uid
Normal file
1
scripts/multiplayer/team.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d4dn67d01lrkr
|
||||
141
scripts/multiplayer/teams.gd
Normal file
141
scripts/multiplayer/teams.gd
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero 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 Resource
|
||||
|
||||
## 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
|
||||
|
||||
func get_state() -> Dictionary:
|
||||
for team : Team in _teams.values():
|
||||
_state[team.name] = team.players
|
||||
return _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]) -> Teams:
|
||||
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
scripts/multiplayer/teams.gd.uid
Normal file
1
scripts/multiplayer/teams.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dgapvl7gqfd5r
|
||||
43
scripts/singleplayer/singleplayer.gd
Normal file
43
scripts/singleplayer/singleplayer.gd
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# This file is part of sunder.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
class_name Singleplayer extends Node
|
||||
|
||||
@export var player:Player
|
||||
@export var flag:Flag
|
||||
@export var map:Map
|
||||
|
||||
func _ready() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
player.health.exhausted.connect(_on_player_dead)
|
||||
var spawn:Node = map.get_objective_spawn()
|
||||
flag.global_position = spawn.global_position
|
||||
player.damage.connect(
|
||||
func(_source: Node, target: Node, amount: int) -> void:
|
||||
target.health.damage(amount, 0))
|
||||
|
||||
var _on_exit := func() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
Game.change_scene()
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("exit"):
|
||||
if OS.is_debug_build():
|
||||
_on_exit.call()
|
||||
else:
|
||||
player.hud.ask("Are you sure you want to leave?", _on_exit)
|
||||
|
||||
func _on_player_dead(_player:Player) -> void:
|
||||
var spawn:Node3D = map.get_player_spawn()
|
||||
player.respawn(spawn.global_position)
|
||||
1
scripts/singleplayer/singleplayer.gd.uid
Normal file
1
scripts/singleplayer/singleplayer.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bjcxla4jmjui1
|
||||
Loading…
Add table
Add a link
Reference in a new issue