Improve controller freecam and network movement
This commit is contained in:
parent
a3e0830bd1
commit
79a399674a
4 changed files with 263 additions and 22 deletions
|
|
@ -9,6 +9,8 @@ const CONNECTION_TIMEOUT_SECONDS: float = 10.0
|
||||||
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
|
const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
|
||||||
const INPUT_INTERVAL: float = 1.0 / 30.0
|
const INPUT_INTERVAL: float = 1.0 / 30.0
|
||||||
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
|
||||||
|
const MOVEMENT_SNAPSHOT_BATCH_SIZE: int = 8
|
||||||
|
const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 12
|
||||||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||||
|
|
||||||
signal state_changed(state: State)
|
signal state_changed(state: State)
|
||||||
|
|
@ -1775,24 +1777,122 @@ func _is_valid_movement_input(data: Dictionary) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func _broadcast_movement_snapshots() -> void:
|
func _broadcast_movement_snapshots() -> void:
|
||||||
var snapshots: Array[Dictionary] = []
|
var snapshots: Array = []
|
||||||
for peer_id: int in _registry.get_peer_ids():
|
for peer_id: int in _registry.get_peer_ids():
|
||||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
if avatar == null:
|
if avatar == null:
|
||||||
continue
|
continue
|
||||||
snapshots.append(avatar.make_network_snapshot(peer_id))
|
var encoded: Array = _encode_movement_snapshot(
|
||||||
receive_movement_snapshots.rpc(snapshots)
|
avatar.make_network_snapshot(peer_id)
|
||||||
|
)
|
||||||
|
if not encoded.is_empty():
|
||||||
|
snapshots.append(encoded)
|
||||||
|
# The compact v5 representation keeps a normal eight-player update near
|
||||||
|
# 1 KiB instead of the roughly 3.7 KiB dictionary representation. Rooms
|
||||||
|
# configured above the normal cap are divided into the same safe size.
|
||||||
|
for start_index: int in range(
|
||||||
|
0,
|
||||||
|
snapshots.size(),
|
||||||
|
MOVEMENT_SNAPSHOT_BATCH_SIZE,
|
||||||
|
):
|
||||||
|
receive_movement_snapshots.rpc(
|
||||||
|
snapshots.slice(
|
||||||
|
start_index,
|
||||||
|
mini(
|
||||||
|
start_index + MOVEMENT_SNAPSHOT_BATCH_SIZE,
|
||||||
|
snapshots.size(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _encode_movement_snapshot(snapshot: Dictionary) -> Array:
|
||||||
|
var position: Variant = snapshot.get("position")
|
||||||
|
var snapshot_velocity: Variant = snapshot.get("velocity")
|
||||||
|
var animation_value: Variant = snapshot.get("animation_state")
|
||||||
|
if (
|
||||||
|
typeof(position) != TYPE_ARRAY
|
||||||
|
or position.size() != 3
|
||||||
|
or typeof(snapshot_velocity) != TYPE_ARRAY
|
||||||
|
or snapshot_velocity.size() != 3
|
||||||
|
or not NetworkPlayerAnimationProtocol.validate_state(animation_value)
|
||||||
|
):
|
||||||
|
return []
|
||||||
|
var animation: Dictionary = animation_value
|
||||||
|
var action: Dictionary = animation["action"]
|
||||||
|
return [
|
||||||
|
int(snapshot.get("peer_id", 0)),
|
||||||
|
int(snapshot.get("acknowledged_input", 0)),
|
||||||
|
Vector3(float(position[0]), float(position[1]), float(position[2])),
|
||||||
|
Vector3(
|
||||||
|
float(snapshot_velocity[0]),
|
||||||
|
float(snapshot_velocity[1]),
|
||||||
|
float(snapshot_velocity[2]),
|
||||||
|
),
|
||||||
|
float(snapshot.get("visual_yaw", 0.0)),
|
||||||
|
str(animation["locomotion_id"]),
|
||||||
|
bool(animation["grounded"]),
|
||||||
|
str(action["id"]),
|
||||||
|
int(action["sequence"]),
|
||||||
|
float(action["elapsed"]),
|
||||||
|
bool(snapshot.get("sitting", false)),
|
||||||
|
bool(snapshot.get("casting", false)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func _decode_movement_snapshot(value: Variant) -> Dictionary:
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return {}
|
||||||
|
var fields: Array = value
|
||||||
|
if (
|
||||||
|
fields.size() != MOVEMENT_SNAPSHOT_FIELD_COUNT
|
||||||
|
or typeof(fields[0]) != TYPE_INT
|
||||||
|
or typeof(fields[1]) != TYPE_INT
|
||||||
|
or typeof(fields[2]) != TYPE_VECTOR3
|
||||||
|
or typeof(fields[3]) != TYPE_VECTOR3
|
||||||
|
or typeof(fields[4]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or typeof(fields[5]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
or typeof(fields[6]) != TYPE_BOOL
|
||||||
|
or typeof(fields[7]) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||||
|
or typeof(fields[8]) != TYPE_INT
|
||||||
|
or typeof(fields[9]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or typeof(fields[10]) != TYPE_BOOL
|
||||||
|
or typeof(fields[11]) != TYPE_BOOL
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var position: Vector3 = fields[2]
|
||||||
|
var snapshot_velocity: Vector3 = fields[3]
|
||||||
|
return {
|
||||||
|
"peer_id": int(fields[0]),
|
||||||
|
"acknowledged_input": int(fields[1]),
|
||||||
|
"position": [position.x, position.y, position.z],
|
||||||
|
"velocity": [
|
||||||
|
snapshot_velocity.x,
|
||||||
|
snapshot_velocity.y,
|
||||||
|
snapshot_velocity.z,
|
||||||
|
],
|
||||||
|
"visual_yaw": float(fields[4]),
|
||||||
|
"animation_state": NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
StringName(str(fields[5])),
|
||||||
|
bool(fields[6]),
|
||||||
|
StringName(str(fields[7])),
|
||||||
|
int(fields[8]),
|
||||||
|
float(fields[9]),
|
||||||
|
),
|
||||||
|
"sitting": bool(fields[10]),
|
||||||
|
"casting": bool(fields[11]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "unreliable_ordered", 2)
|
@rpc("authority", "call_remote", "unreliable_ordered", 2)
|
||||||
func receive_movement_snapshots(snapshots: Array) -> void:
|
func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||||
if state != State.JOINED_CLIENT:
|
if state != State.JOINED_CLIENT:
|
||||||
return
|
return
|
||||||
var local_peer_id: int = multiplayer.get_unique_id()
|
var local_peer_id: int = multiplayer.get_unique_id()
|
||||||
for value: Variant in snapshots:
|
for value: Variant in encoded_snapshots:
|
||||||
if typeof(value) != TYPE_DICTIONARY:
|
var snapshot: Dictionary = _decode_movement_snapshot(value)
|
||||||
|
if snapshot.is_empty():
|
||||||
continue
|
continue
|
||||||
var snapshot: Dictionary = value
|
|
||||||
if typeof(snapshot.get("peer_id")) != TYPE_INT:
|
if typeof(snapshot.get("peer_id")) != TYPE_INT:
|
||||||
continue
|
continue
|
||||||
var peer_id: int = snapshot["peer_id"]
|
var peer_id: int = snapshot["peer_id"]
|
||||||
|
|
@ -1800,7 +1900,11 @@ func receive_movement_snapshots(snapshots: Array) -> void:
|
||||||
if avatar == null:
|
if avatar == null:
|
||||||
continue
|
continue
|
||||||
if peer_id == local_peer_id:
|
if peer_id == local_peer_id:
|
||||||
avatar.apply_local_prediction_correction(snapshot)
|
avatar.apply_local_prediction_correction(
|
||||||
|
snapshot,
|
||||||
|
_input_sequence,
|
||||||
|
INPUT_INTERVAL,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
avatar.push_network_snapshot(snapshot)
|
avatar.push_network_snapshot(snapshot)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,11 @@ const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
||||||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||||
const BASE_REEL_SPEED: float = 0.16
|
const BASE_REEL_SPEED: float = 0.16
|
||||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||||
|
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.5
|
||||||
|
const LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
||||||
|
const LOCAL_PREDICTION_CORRECTION_THRESHOLD: float = 0.12
|
||||||
|
const LOCAL_PREDICTION_SNAP_DISTANCE: float = 2.0
|
||||||
|
const LOCAL_PREDICTION_CORRECTION_WEIGHT: float = 0.18
|
||||||
# The target Android handheld exposes its physical right trigger through
|
# The target Android handheld exposes its physical right trigger through
|
||||||
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
# Godot's left-trigger axis. Keep the role named here so the platform mapping
|
||||||
# remains isolated from camera behavior.
|
# remains isolated from camera behavior.
|
||||||
|
|
@ -767,6 +772,13 @@ func _process(delta: float) -> void:
|
||||||
if not local_control_enabled:
|
if not local_control_enabled:
|
||||||
return
|
return
|
||||||
if _free_camera_active:
|
if _free_camera_active:
|
||||||
|
if _is_camera_input_enabled():
|
||||||
|
_rotate_free_camera(
|
||||||
|
_scale_controller_camera_input(
|
||||||
|
_get_controller_camera_stick(),
|
||||||
|
delta,
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if _camera_dragging and not Input.is_action_pressed("camera_drag"):
|
if _camera_dragging and not Input.is_action_pressed("camera_drag"):
|
||||||
|
|
@ -785,15 +797,8 @@ func _process(delta: float) -> void:
|
||||||
+ vertical_zoom_input * controller_zoom_speed * delta
|
+ vertical_zoom_input * controller_zoom_speed * delta
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
var adjusted_strength: float = (
|
|
||||||
(stick.length() - controller_camera_deadzone)
|
|
||||||
/ (1.0 - controller_camera_deadzone)
|
|
||||||
)
|
|
||||||
_rotate_camera(
|
_rotate_camera(
|
||||||
stick.normalized()
|
_scale_controller_camera_input(stick, delta)
|
||||||
* adjusted_strength
|
|
||||||
* controller_camera_speed
|
|
||||||
* delta
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
|
var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
|
||||||
|
|
@ -884,6 +889,25 @@ func _get_controller_camera_stick() -> Vector2:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _scale_controller_camera_input(
|
||||||
|
stick: Vector2,
|
||||||
|
delta: float,
|
||||||
|
) -> Vector2:
|
||||||
|
var strength: float = stick.length()
|
||||||
|
if strength <= controller_camera_deadzone:
|
||||||
|
return Vector2.ZERO
|
||||||
|
var adjusted_strength: float = (
|
||||||
|
(strength - controller_camera_deadzone)
|
||||||
|
/ (1.0 - controller_camera_deadzone)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
stick.normalized()
|
||||||
|
* adjusted_strength
|
||||||
|
* controller_camera_speed
|
||||||
|
* delta
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _get_controller_zoom_strength() -> float:
|
func _get_controller_zoom_strength() -> float:
|
||||||
if _controller_mapping_manager != null:
|
if _controller_mapping_manager != null:
|
||||||
return _controller_mapping_manager.get_role_strength(
|
return _controller_mapping_manager.get_role_strength(
|
||||||
|
|
@ -1540,7 +1564,11 @@ func push_network_snapshot(snapshot: Dictionary) -> void:
|
||||||
_network_snapshot_ready = true
|
_network_snapshot_ready = true
|
||||||
|
|
||||||
|
|
||||||
func apply_local_prediction_correction(snapshot: Dictionary) -> void:
|
func apply_local_prediction_correction(
|
||||||
|
snapshot: Dictionary,
|
||||||
|
latest_input_sequence: int = 0,
|
||||||
|
input_interval_seconds: float = 0.0,
|
||||||
|
) -> void:
|
||||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||||
if parsed.is_empty():
|
if parsed.is_empty():
|
||||||
return
|
return
|
||||||
|
|
@ -1556,13 +1584,32 @@ func apply_local_prediction_correction(snapshot: Dictionary) -> void:
|
||||||
if not _sitting_intent_pending:
|
if not _sitting_intent_pending:
|
||||||
_set_sitting(bool(parsed["sitting"]))
|
_set_sitting(bool(parsed["sitting"]))
|
||||||
var authoritative_position: Vector3 = parsed["position"]
|
var authoritative_position: Vector3 = parsed["position"]
|
||||||
|
if (
|
||||||
|
acknowledged_input > 0
|
||||||
|
and latest_input_sequence > acknowledged_input
|
||||||
|
and input_interval_seconds > 0.0
|
||||||
|
):
|
||||||
|
# The snapshot describes the host's position when an older input was
|
||||||
|
# acknowledged. Project it through the measured input-sequence gap so
|
||||||
|
# ordinary round-trip latency is not mistaken for prediction error.
|
||||||
|
var sequence_gap: int = latest_input_sequence - acknowledged_input
|
||||||
|
var transit_seconds: float = minf(
|
||||||
|
float(sequence_gap) * input_interval_seconds,
|
||||||
|
LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
|
||||||
|
)
|
||||||
|
authoritative_position += (
|
||||||
|
(parsed["velocity"] as Vector3) * transit_seconds
|
||||||
|
)
|
||||||
var error_distance: float = global_position.distance_to(
|
var error_distance: float = global_position.distance_to(
|
||||||
authoritative_position
|
authoritative_position
|
||||||
)
|
)
|
||||||
if error_distance > 2.0:
|
if error_distance > LOCAL_PREDICTION_SNAP_DISTANCE:
|
||||||
global_position = authoritative_position
|
global_position = authoritative_position
|
||||||
elif error_distance > 0.05:
|
elif error_distance > LOCAL_PREDICTION_CORRECTION_THRESHOLD:
|
||||||
global_position = global_position.lerp(authoritative_position, 0.18)
|
global_position = global_position.lerp(
|
||||||
|
authoritative_position,
|
||||||
|
LOCAL_PREDICTION_CORRECTION_WEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func apply_network_teleport(snapshot: Dictionary) -> void:
|
func apply_network_teleport(snapshot: Dictionary) -> void:
|
||||||
|
|
@ -1760,7 +1807,10 @@ func _apply_network_casting(casting: bool) -> void:
|
||||||
func _update_network_interpolation(delta: float) -> void:
|
func _update_network_interpolation(delta: float) -> void:
|
||||||
if not _network_snapshot_ready:
|
if not _network_snapshot_ready:
|
||||||
return
|
return
|
||||||
_network_snapshot_age = minf(_network_snapshot_age + delta, 0.1)
|
_network_snapshot_age = minf(
|
||||||
|
_network_snapshot_age + delta,
|
||||||
|
NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
|
||||||
|
)
|
||||||
var predicted_position: Vector3 = (
|
var predicted_position: Vector3 = (
|
||||||
_network_target_position
|
_network_target_position
|
||||||
+ _network_target_velocity * _network_snapshot_age
|
+ _network_target_velocity * _network_snapshot_age
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,27 @@ func _run() -> void:
|
||||||
assert(QuickRadialMenu.ACTIONS.has(&"hud"))
|
assert(QuickRadialMenu.ACTIONS.has(&"hud"))
|
||||||
game_ui.call("_on_quick_action_selected", &"freecam")
|
game_ui.call("_on_quick_action_selected", &"freecam")
|
||||||
assert(player.is_free_camera_active())
|
assert(player.is_free_camera_active())
|
||||||
assert(player.get_active_gameplay_camera().name == &"FreeCamera")
|
var free_camera := player.get_active_gameplay_camera()
|
||||||
|
assert(free_camera.name == &"FreeCamera")
|
||||||
|
var free_camera_yaw_before: float = free_camera.global_rotation.y
|
||||||
|
var mapping_manager := main.get(
|
||||||
|
"_controller_mapping_manager"
|
||||||
|
) as ControllerMappingManager
|
||||||
|
var right_stick_motion := InputEventJoypadMotion.new()
|
||||||
|
right_stick_motion.device = mapping_manager.get_active_device_id()
|
||||||
|
right_stick_motion.axis = JOY_AXIS_RIGHT_X
|
||||||
|
right_stick_motion.axis_value = 0.9
|
||||||
|
Input.parse_input_event(right_stick_motion)
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
assert(
|
||||||
|
absf(angle_difference(
|
||||||
|
free_camera.global_rotation.y,
|
||||||
|
free_camera_yaw_before,
|
||||||
|
)) > 0.02
|
||||||
|
)
|
||||||
|
right_stick_motion.axis_value = 0.0
|
||||||
|
Input.parse_input_event(right_stick_motion)
|
||||||
|
await process_frame
|
||||||
game_ui.call("_on_quick_action_selected", &"freecam")
|
game_ui.call("_on_quick_action_selected", &"freecam")
|
||||||
assert(not player.is_free_camera_active())
|
assert(not player.is_free_camera_active())
|
||||||
game_ui.call("_on_quick_action_selected", &"hud")
|
game_ui.call("_on_quick_action_selected", &"hud")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const PlayerScene: PackedScene = preload("res://player/player.tscn")
|
||||||
const TEST_PORT: int = 18141
|
const TEST_PORT: int = 18141
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -9,7 +10,12 @@ func _initialize() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
|
await _validate_latency_smoothing()
|
||||||
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("unit"):
|
||||||
|
print("Movement latency smoothing validation: PASS")
|
||||||
|
quit()
|
||||||
|
return
|
||||||
if arguments.has("host"):
|
if arguments.has("host"):
|
||||||
await _run_host()
|
await _run_host()
|
||||||
return
|
return
|
||||||
|
|
@ -20,6 +26,67 @@ func _run() -> void:
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_latency_smoothing() -> void:
|
||||||
|
var avatar := PlayerScene.instantiate() as Player
|
||||||
|
root.add_child(avatar)
|
||||||
|
await process_frame
|
||||||
|
avatar.set_process(false)
|
||||||
|
avatar.set_physics_process(false)
|
||||||
|
avatar.configure_network_remote(false)
|
||||||
|
var moving_snapshot: Dictionary = _network_snapshot(
|
||||||
|
Vector3.ZERO,
|
||||||
|
Vector3(4.5, 0.0, 0.0),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
|
||||||
|
var encoded_snapshots: Array = []
|
||||||
|
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
|
||||||
|
encoded_snapshots.append(
|
||||||
|
NetworkSession._encode_movement_snapshot(moving_snapshot)
|
||||||
|
)
|
||||||
|
assert(var_to_bytes(encoded_snapshots).size() < 1200)
|
||||||
|
assert(
|
||||||
|
NetworkSession._decode_movement_snapshot(
|
||||||
|
encoded_snapshots[0]
|
||||||
|
) == moving_snapshot
|
||||||
|
)
|
||||||
|
avatar.push_network_snapshot(moving_snapshot)
|
||||||
|
for _step: int in 12:
|
||||||
|
avatar.call("_update_network_interpolation", 1.0 / 30.0)
|
||||||
|
assert(avatar.global_position.x > 1.5)
|
||||||
|
|
||||||
|
avatar.configure_network_remote(false)
|
||||||
|
avatar.global_position = Vector3(1.35, 0.0, 0.0)
|
||||||
|
avatar.apply_local_prediction_correction(
|
||||||
|
moving_snapshot,
|
||||||
|
10,
|
||||||
|
1.0 / 30.0,
|
||||||
|
)
|
||||||
|
assert(avatar.global_position.x > 1.25)
|
||||||
|
avatar.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _network_snapshot(
|
||||||
|
position: Vector3,
|
||||||
|
velocity: Vector3,
|
||||||
|
acknowledged_input: int,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"peer_id": 2,
|
||||||
|
"acknowledged_input": acknowledged_input,
|
||||||
|
"position": [position.x, position.y, position.z],
|
||||||
|
"velocity": [velocity.x, velocity.y, velocity.z],
|
||||||
|
"visual_yaw": 0.0,
|
||||||
|
"animation_state": NetworkPlayerAnimationProtocol.make_state(
|
||||||
|
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
"sitting": false,
|
||||||
|
"casting": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _run_host() -> void:
|
func _run_host() -> void:
|
||||||
var main: Node = await _create_initialized_main()
|
var main: Node = await _create_initialized_main()
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue