t_p7.4_port: network_manager to netfox
- Fixed double-emission bug in network_manager.gd: ENet signal connections (peer_connected/peer_disconnected) are now only connected when netfox NetworkEvents is NOT available, preventing duplicate player_connected/ player_disconnected signal emissions. - Fixed stop() to only disconnect ENet signals when they were actually connected (mirrors the conditional connection in start_server()). - Fixed headless parse error in player_net_input.gd: replaced direct NetworkTime.before_tick_loop reference with Engine.get_singleton() call, avoiding unresolved type identifier in headless mode. - netfox_bootstrap.gd added as minimal autoload placeholder. - Dual-path architecture: netfox NetworkEvents when available (editor), ENet fallback when netfox unavailable (headless server). - Broadcast RPCs (spawn, round state, scores) kept as-is since they work identically through Godot's MultiplayerAPI in both paths.
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
## NetfoxBootstrap — minimal. All class_names should be registered by
|
||||||
|
## Godot's global script scanning. We don't reference any netfox
|
||||||
|
## class_name directly in our code.
|
||||||
|
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# netfox singletons are registered by the editor plugin only.
|
||||||
|
# In headless/export builds, Engine.has_singleton checks handle this.
|
||||||
|
pass
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cwipcjgj8cyym
|
||||||
@@ -1,16 +1,28 @@
|
|||||||
## NetworkManager — ENet Transport + Player Replication Singleton
|
## NetworkManager — netfox-aware transport with graceful ENet fallback
|
||||||
##
|
##
|
||||||
## Autoload that wraps Godot 4's ENetMultiplayerPeer and provides
|
## Dual-path architecture:
|
||||||
## player spawn/despawn broadcasting to all connected clients.
|
## Path A (netfox available) → NetworkEvents drives lifecycle signals
|
||||||
## Server creates RPC broadcasts; clients receive and emit signals.
|
## Path B (headless/export) → ENetMultiplayerPeer drives lifecycle signals
|
||||||
|
##
|
||||||
|
## The server always uses ENetMultiplayerPeer as the underlying transport
|
||||||
|
## (netfox layers on top of Godot's MultiplayerAPI). When netfox NetworkEvents
|
||||||
|
## is available, it provides lifecycle signals (on_peer_join, etc.) and the
|
||||||
|
## ENet signal connections are skipped to avoid double emissions. When netfox
|
||||||
|
## is unavailable (headless mode), the built-in ENet peer_connected/
|
||||||
|
## peer_disconnected signals are used directly.
|
||||||
|
##
|
||||||
|
## Broadcast RPCs (player spawn/despawn) work identically in both paths
|
||||||
|
## since they use Godot's built-in MultiplayerAPI, which netfox enhances
|
||||||
|
## without replacing. Round state/scores are now distributed via
|
||||||
|
## round_manager's netfox Self-RPC pattern directly.
|
||||||
##
|
##
|
||||||
## Architecture:
|
## Architecture:
|
||||||
## server mode → start_server(port) → ENetMultiplayerPeer server
|
## server mode → start_server(port) → ENetMultiplayerPeer server
|
||||||
## client mode → join_server(host,port)→ ENetMultiplayerPeer client
|
## client mode → join_server(host,port)→ ENetMultiplayerPeer client
|
||||||
## replication → _broadcast_spawn_player / _broadcast_despawn_player
|
## netfox path → NetworkEvents signals when available (editor)
|
||||||
## (server→all clients RPC)
|
## fallback → ENet peer signals in headless mode
|
||||||
##
|
##
|
||||||
## Channels (3-lane layout per Phase 0 research):
|
## Channels (3-lane layout):
|
||||||
## 0 unreliable-ordered → 128Hz input / transform deltas
|
## 0 unreliable-ordered → 128Hz input / transform deltas
|
||||||
## 1 reliable-ordered → game events, spawn, damage, chat
|
## 1 reliable-ordered → game events, spawn, damage, chat
|
||||||
## 2 unreliable → telemetry / VOIP metadata
|
## 2 unreliable → telemetry / VOIP metadata
|
||||||
@@ -42,18 +54,7 @@ signal server_state_received(entity_id: int, snapshot_dict: Dictionary)
|
|||||||
# Constants
|
# Constants
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
const DEFAULT_PORT: int = 34197
|
const DEFAULT_PORT: int = 34197
|
||||||
const CHANNELS: int = 3 # 0=input, 1=events, 2=telemetry
|
const CHANNELS: int = 3
|
||||||
|
|
||||||
# Max clients is read from ServerConfig at start_server() time.
|
|
||||||
# This default is used before ServerConfig is available.
|
|
||||||
var max_clients: int = 16
|
|
||||||
|
|
||||||
# Channel indices
|
|
||||||
enum Chan {
|
|
||||||
INPUT = 0,
|
|
||||||
EVENTS = 1,
|
|
||||||
TELEMETRY = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# State
|
# State
|
||||||
@@ -61,6 +62,11 @@ enum Chan {
|
|||||||
var is_server: bool = false : get = _is_server
|
var is_server: bool = false : get = _is_server
|
||||||
var is_client: bool = false : get = _is_client
|
var is_client: bool = false : get = _is_client
|
||||||
var peer: ENetMultiplayerPeer = null
|
var peer: ENetMultiplayerPeer = null
|
||||||
|
var max_clients: int = 16
|
||||||
|
|
||||||
|
# netfox overlay (optional — only available when netfox plugin is active)
|
||||||
|
var _netfox_events = null
|
||||||
|
var _netfox_events_connected: bool = false
|
||||||
|
|
||||||
func _is_server() -> bool:
|
func _is_server() -> bool:
|
||||||
return is_server
|
return is_server
|
||||||
@@ -68,12 +74,68 @@ func _is_server() -> bool:
|
|||||||
func _is_client() -> bool:
|
func _is_client() -> bool:
|
||||||
return is_client
|
return is_client
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Try to connect to netfox NetworkEvents singleton (non-critical)
|
||||||
|
# We use StringName for singleton access to avoid parser errors
|
||||||
|
# when netfox types aren't available (headless mode).
|
||||||
|
_try_connect_netfox()
|
||||||
|
|
||||||
|
func _try_connect_netfox() -> void:
|
||||||
|
# Use singleton name string to avoid referencing netfox types directly
|
||||||
|
if not Engine.has_singleton("NetworkEvents"):
|
||||||
|
_netfox_events_connected = false
|
||||||
|
print("[NetworkManager] netfox not available — using ENet directly")
|
||||||
|
return
|
||||||
|
|
||||||
|
var events = Engine.get_singleton("NetworkEvents")
|
||||||
|
if events == null or not events.has_signal("on_server_start"):
|
||||||
|
_netfox_events_connected = false
|
||||||
|
print("[NetworkManager] netfox singleton found but unexpected shape — using ENet directly")
|
||||||
|
return
|
||||||
|
|
||||||
|
_netfox_events = events
|
||||||
|
_netfox_events.on_server_start.connect(_on_netfox_server_start)
|
||||||
|
_netfox_events.on_server_stop.connect(_on_netfox_server_stop)
|
||||||
|
_netfox_events.on_client_start.connect(_on_netfox_client_start)
|
||||||
|
_netfox_events.on_client_stop.connect(_on_netfox_client_stop)
|
||||||
|
_netfox_events.on_peer_join.connect(_on_netfox_peer_join)
|
||||||
|
_netfox_events.on_peer_leave.connect(_on_netfox_peer_leave)
|
||||||
|
_netfox_events_connected = true
|
||||||
|
print("[NetworkManager] netfox NetworkEvents overlay active")
|
||||||
|
|
||||||
|
# netfox signal handlers
|
||||||
|
func _on_netfox_server_start() -> void:
|
||||||
|
print("[NetworkManager] Server started (netfox)")
|
||||||
|
is_server = true
|
||||||
|
|
||||||
|
func _on_netfox_server_stop() -> void:
|
||||||
|
print("[NetworkManager] Server stopped (netfox)")
|
||||||
|
is_server = false
|
||||||
|
|
||||||
|
func _on_netfox_client_start(id: int) -> void:
|
||||||
|
print("[NetworkManager] Client started (netfox, id=%d)" % id)
|
||||||
|
is_client = true
|
||||||
|
|
||||||
|
func _on_netfox_client_stop() -> void:
|
||||||
|
print("[NetworkManager] Client stopped (netfox)")
|
||||||
|
is_client = false
|
||||||
|
|
||||||
|
func _on_netfox_peer_join(id: int) -> void:
|
||||||
|
print("[NetworkManager] Peer joined (netfox, id=%d)" % id)
|
||||||
|
player_connected.emit(id)
|
||||||
|
|
||||||
|
func _on_netfox_peer_leave(id: int) -> void:
|
||||||
|
print("[NetworkManager] Peer left (netfox, id=%d)" % id)
|
||||||
|
player_disconnected.emit(id)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server API
|
# Server API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
## Start a dedicated server on [port].
|
|
||||||
## Uses max_clients (which should be set from ServerConfig before calling).
|
|
||||||
## Returns OK or ERR_* on failure.
|
|
||||||
func start_server(port: int = DEFAULT_PORT) -> Error:
|
func start_server(port: int = DEFAULT_PORT) -> Error:
|
||||||
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
|
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
|
||||||
stop()
|
stop()
|
||||||
@@ -84,30 +146,41 @@ func start_server(port: int = DEFAULT_PORT) -> Error:
|
|||||||
|
|
||||||
peer = ENetMultiplayerPeer.new()
|
peer = ENetMultiplayerPeer.new()
|
||||||
peer.set_bind_ip("*")
|
peer.set_bind_ip("*")
|
||||||
|
|
||||||
var err: Error = peer.create_server(port, max_clients, CHANNELS, 0, 0)
|
var err: Error = peer.create_server(port, max_clients, CHANNELS, 0, 0)
|
||||||
if err != OK:
|
if err != OK:
|
||||||
peer = null
|
peer = null
|
||||||
return err
|
return err
|
||||||
|
|
||||||
multiplayer.multiplayer_peer = peer
|
multiplayer.multiplayer_peer = peer
|
||||||
multiplayer.multiplayer_peer.peer_connected.connect(_on_peer_connected)
|
|
||||||
multiplayer.multiplayer_peer.peer_disconnected.connect(_on_peer_disconnected)
|
# Only connect ENet signals when netfox is unavailable to avoid
|
||||||
|
# double-emission with NetworkEvents.on_peer_join/on_peer_leave
|
||||||
|
if not _netfox_events_connected:
|
||||||
|
multiplayer.multiplayer_peer.peer_connected.connect(_on_peer_connected)
|
||||||
|
multiplayer.multiplayer_peer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
|
print("[NetworkManager] Using ENet peer signals (netfox not available)")
|
||||||
|
else:
|
||||||
|
print("[NetworkManager] Using netfox NetworkEvents for lifecycle signals")
|
||||||
|
|
||||||
is_server = true
|
is_server = true
|
||||||
server_started.emit(port)
|
server_started.emit(port)
|
||||||
print("[NetworkManager] Server started on port %d" % port)
|
print("[NetworkManager] Server started on port %d" % port)
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
## Stop the server / disconnect.
|
|
||||||
func stop() -> void:
|
func stop() -> void:
|
||||||
if not peer:
|
if not peer:
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_server:
|
if is_server:
|
||||||
multiplayer.multiplayer_peer.peer_connected.disconnect(_on_peer_connected)
|
# Only disconnect ENet signals if they were connected
|
||||||
multiplayer.multiplayer_peer.peer_disconnected.disconnect(_on_peer_disconnected)
|
# (when netfox was unavailable, see start_server)
|
||||||
|
if not _netfox_events_connected:
|
||||||
|
if multiplayer.multiplayer_peer.peer_connected.is_connected(_on_peer_connected):
|
||||||
|
multiplayer.multiplayer_peer.peer_connected.disconnect(_on_peer_connected)
|
||||||
|
if multiplayer.multiplayer_peer.peer_disconnected.is_connected(_on_peer_disconnected):
|
||||||
|
multiplayer.multiplayer_peer.peer_disconnected.disconnect(_on_peer_disconnected)
|
||||||
|
|
||||||
peer.close()
|
peer.close()
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
peer = null
|
peer = null
|
||||||
@@ -119,7 +192,7 @@ func stop() -> void:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Client API
|
# Client API
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
## Connect to a remote server.
|
|
||||||
func join_server(host: String, port: int = DEFAULT_PORT) -> Error:
|
func join_server(host: String, port: int = DEFAULT_PORT) -> Error:
|
||||||
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
|
if peer and peer.get_connection_status() != MultiplayerPeer.CONNECTION_DISCONNECTED:
|
||||||
stop()
|
stop()
|
||||||
@@ -140,20 +213,18 @@ func join_server(host: String, port: int = DEFAULT_PORT) -> Error:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Player Replication RPCs (broadcast server → all clients)
|
# Player Replication RPCs (broadcast server → all clients)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
## Server calls this when a new player joins.
|
|
||||||
## Broadcasts to all clients so they can create a visual player node.
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func broadcast_spawn_player(peer_id: int, pos: Vector3, is_team_a: bool) -> void:
|
func broadcast_spawn_player(peer_id: int, pos: Vector3, is_team_a: bool) -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
print("[NetworkManager] Client received spawn: peer=%d at (%.1f, %.1f)" % [peer_id, pos.x, pos.z])
|
print("[NetworkManager] Client received spawn: peer=%d at (%.1f, %.1f)" % [peer_id, pos.x, pos.z])
|
||||||
remote_player_spawned.emit(peer_id, pos)
|
remote_player_spawned.emit(peer_id, pos)
|
||||||
|
|
||||||
## Server calls this when a player leaves.
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func broadcast_despawn_player(peer_id: int) -> void:
|
func broadcast_despawn_player(peer_id: int) -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
print("[NetworkManager] Client received despawn: peer=%d" % peer_id)
|
print("[NetworkManager] Client received despawn: peer=%d" % peer_id)
|
||||||
remote_player_despawned.emit(peer_id)
|
remote_player_despawned.emit(peer_id)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Client Prediction RPCs (Phase 1 — client-side prediction)
|
# Client Prediction RPCs (Phase 1 — client-side prediction)
|
||||||
@@ -184,6 +255,7 @@ func send_server_state(entity_id: int, snapshot_dict: Dictionary) -> void:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Event handlers
|
# Event handlers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _on_peer_connected(id: int) -> void:
|
func _on_peer_connected(id: int) -> void:
|
||||||
print("[NetworkManager] Peer connected: %d" % id)
|
print("[NetworkManager] Peer connected: %d" % id)
|
||||||
player_connected.emit(id)
|
player_connected.emit(id)
|
||||||
@@ -193,8 +265,6 @@ func _on_peer_disconnected(id: int) -> void:
|
|||||||
player_disconnected.emit(id)
|
player_disconnected.emit(id)
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
# Godot does internal ENet polling via MultiplayerAPI;
|
|
||||||
# explicit polling is reserved for future custom packet handling.
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
## PlayerNetInput — netfox-compatible input gathering for tactical-shooter.
|
||||||
|
##
|
||||||
|
## Extends Node directly (avoids headless class_name issues with BaseNetInput)
|
||||||
|
## but follows the same pattern: connects to NetworkTime.before_tick_loop to
|
||||||
|
## call _gather() on each tick.
|
||||||
|
##
|
||||||
|
## RollbackSynchronizer references the exported properties as input_properties.
|
||||||
|
##
|
||||||
|
## Usage:
|
||||||
|
## Place as a child of the Player (Node3D). The parent node's
|
||||||
|
## RollbackSynchronizer will pick up the input_properties paths.
|
||||||
|
##
|
||||||
|
## Set hitscan_manager_ref to the RollbackHitscanManager child so that
|
||||||
|
## _gather() can call record_fire() for RewindableAction-based fire tracking.
|
||||||
|
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Input state — read by RollbackSynchronizer as input_properties
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
## Movement direction in local space (normalized, or zero).
|
||||||
|
@export var move_direction: Vector3 = Vector3.ZERO
|
||||||
|
|
||||||
|
## Mouse look: yaw (horizontal, radians).
|
||||||
|
@export var look_yaw: float = 0.0
|
||||||
|
|
||||||
|
## Mouse look: pitch (vertical, radians).
|
||||||
|
@export var look_pitch: float = 0.0
|
||||||
|
|
||||||
|
## Jump pressed this tick.
|
||||||
|
@export var jump: bool = false
|
||||||
|
|
||||||
|
## Sprint toggle active.
|
||||||
|
@export var sprint: bool = false
|
||||||
|
|
||||||
|
## Crouch toggle active.
|
||||||
|
@export var crouch: bool = false
|
||||||
|
|
||||||
|
## Shoot pressed this tick.
|
||||||
|
@export var shoot: bool = false
|
||||||
|
|
||||||
|
## Aim-down-sights pressed.
|
||||||
|
@export var aim: bool = false
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# RollbackHitscanManager reference (set by player.gd)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
## Reference to the RollbackHitscanManager for RewindableAction fire tracking.
|
||||||
|
var hitscan_mgr_ref: Node = null
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Same pattern as netfox's BaseNetInput:
|
||||||
|
# connect to NetworkTime.before_tick_loop to gather input each tick
|
||||||
|
# Use Engine.get_singleton instead of direct NetworkTime reference to
|
||||||
|
# avoid headless parse errors (NetworkTime is an editor-plugin type).
|
||||||
|
var nt = Engine.get_singleton(&"NetworkTime") if Engine.has_singleton(&"NetworkTime") else null
|
||||||
|
if nt:
|
||||||
|
nt.before_tick_loop.connect(_on_before_tick_loop)
|
||||||
|
else:
|
||||||
|
push_warning("[PlayerNetInput] netfox NetworkTime not available — " +
|
||||||
|
"input gathering disabled")
|
||||||
|
|
||||||
|
func _on_before_tick_loop() -> void:
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
_gather()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Input gathering (called before each tick on the authority peer)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func _gather() -> void:
|
||||||
|
# Movement direction
|
||||||
|
var dir := Vector3.ZERO
|
||||||
|
if Input.is_action_pressed(&"move_forward") or Input.is_action_pressed(&"move_up"):
|
||||||
|
dir.z -= 1.0
|
||||||
|
if Input.is_action_pressed(&"move_backward") or Input.is_action_pressed(&"move_down"):
|
||||||
|
dir.z += 1.0
|
||||||
|
if Input.is_action_pressed(&"move_left"):
|
||||||
|
dir.x -= 1.0
|
||||||
|
if Input.is_action_pressed(&"move_right"):
|
||||||
|
dir.x += 1.0
|
||||||
|
if dir.length_squared() > 0.0:
|
||||||
|
dir = dir.normalized()
|
||||||
|
move_direction = dir
|
||||||
|
|
||||||
|
# Read look direction from parent's FPSCharacterController if available
|
||||||
|
var parent := get_parent()
|
||||||
|
if parent and parent.has_method(&"get_current_yaw"):
|
||||||
|
look_yaw = parent.get_current_yaw()
|
||||||
|
if parent and parent.has_method(&"get_current_pitch"):
|
||||||
|
look_pitch = parent.get_current_pitch()
|
||||||
|
|
||||||
|
# Action inputs
|
||||||
|
jump = Input.is_action_just_pressed(&"jump")
|
||||||
|
sprint = Input.is_action_pressed(&"sprint")
|
||||||
|
crouch = Input.is_action_pressed(&"crouch")
|
||||||
|
shoot = Input.is_action_pressed(&"shoot")
|
||||||
|
aim = Input.is_action_pressed(&"aim")
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# Fire recording for RewindableAction rollback sync
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# When the client fires, record the shot in the RollbackHitscanManager's
|
||||||
|
# RewindableAction. This propagates to the server via netfox sync so
|
||||||
|
# the server can process the lag-compensated raycast at the correct tick.
|
||||||
|
#
|
||||||
|
# We compute the eye position and look direction at gather time for
|
||||||
|
# the RewindableAction context. The server also reconstructs these from
|
||||||
|
# the synced state (position + yaw/pitch) for authoritative hit detection.
|
||||||
|
if shoot and hitscan_mgr_ref != null and hitscan_mgr_ref.has_method(&"record_fire"):
|
||||||
|
var current_tick: int =_get_current_tick()
|
||||||
|
var eye_pos: Vector3 = parent.global_position + Vector3(0, 1.7, 0) if parent else Vector3.ZERO
|
||||||
|
var look_dir: Vector3 = _yaw_pitch_to_dir(look_yaw, look_pitch)
|
||||||
|
hitscan_mgr_ref.record_fire(current_tick, eye_pos, look_dir)
|
||||||
|
|
||||||
|
## Get the current network tick, or 0 if NetworkTime is not available.
|
||||||
|
func _get_current_tick() -> int:
|
||||||
|
var nt = Engine.get_singleton(&"NetworkTime") if Engine.has_singleton(&"NetworkTime") else null
|
||||||
|
if nt != null and nt.has_method(&"get_tick"):
|
||||||
|
return nt.get_tick()
|
||||||
|
if nt != null:
|
||||||
|
return nt.tick if nt.get("tick") != null else 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
## Convert yaw/pitch to a forward direction vector.
|
||||||
|
static func _yaw_pitch_to_dir(yaw: float, pitch: float) -> Vector3:
|
||||||
|
var cp := cos(pitch)
|
||||||
|
return Vector3(cp * sin(yaw), sin(pitch), cp * cos(yaw))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://syopmnypsnfo
|
||||||
Reference in New Issue
Block a user