xiandie/scene/ground/ground_loader.gd

310 lines
10 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name GroundLoader extends Node2D
# Constants
const DEFAULT_TRANSITION_TIME := 1.4
const MIN_TRANSITION_TIME := 0.6
const EASE_DURATION := 0.3
# Scene name to path mapping
const GROUND_SCENE_PATH_DICT: Dictionary[String, Dictionary] = {
"c01_s05": {"path": "uid://dlx5xxbg53rb8", "name": "院长房间"},
"c01_s06": {"path": "uid://bx16c8nn32f40", "name": "孤儿院长廊"},
"c01_s07": {"path": "uid://ds2iyfndwamiy", "name": "书店外"},
"c01_s08": {"path": "uid://cwu4dhayra8pg", "name": "书店"},
"c01_s09": {"path": "uid://c777lv8mjojcw", "name": "公寓楼外"},
"c01_s10": {"path": "uid://be57l2o3vxxtm", "name": "公寓楼道"},
"c01_s11": {"path": "uid://coiumaaenimbc", "name": "黄包车"},
"c01_s12": {"path": "uid://bol5hl68pbpgq", "name": "诡异书店外"},
"c02_s01": {"path": "uid://bbs7yy5aofw1v", "name": "公寓门口"},
"c02_s02": {"path": "uid://brck77w81fhvc", "name": "公寓楼道"},
"c02_s03": {"path": "uid://djc2uaefhmu7", "name": "一楼院子"},
"c02_s04": {"path": "uid://bivc5cdap370p", "name": "一楼保卫科"},
"c02_s05": {"path": "uid://cp8d3ag5nbjq0", "name": "一楼内侧楼道"},
"c02_s06": {"path": "uid://cootarwb44vvh", "name": "二楼楼道"},
"c02_s07": {"path": "uid://t4xjt774ngwh", "name": "二楼内侧楼道"},
"c02_s08": {"path": "uid://ce2vyyg2reg52", "name": "瞎子卧室"},
"c02_s09": {"path": "uid://ryups1dnwdto", "name": "裂缝空间"},
"c02_s10": {"path": "uid://dny21yhtuteap", "name": "空房间"},
"c02_s11": {"path": "uid://dq41rvwl5hyrk", "name": "一楼火灾"},
"c02_s12": {"path": "uid://da4cuf2i3nwpj", "name": "盒子猫安全屋"},
"c02_s13": {"path": "uid://bvjutch6jex0v", "name": "盒子猫二楼"},
"c02_s14": {"path": "uid://d0p4x5st2r315", "name": "盒子猫二楼内侧"},
"c02_s15": {"path": "uid://b21p53g42j2nt", "name": "盒子猫一楼内侧"},
"c02_s16": {"path": "uid://22hc3oe8t0id", "name": "盒子猫三楼内侧"},
"c02_s17": {"path": "uid://cbr6gbgrl2wb1", "name": "盒子猫三楼"},
"c02_s18": {"path": "uid://d27gv3pbkn4b8", "name": "盒子猫一楼"},
"c03_s01": {"path": "uid://dlrbhfvnd3cs0", "name": "三楼楼道"},
"c03_s02": {"path": "uid://rkro7u5wd3t1", "name": "三楼内侧"},
"c03_s03": {"path": "uid://bsqt2c061fmin", "name": "瞎子理发店"}
}
# Exports
@export var ignore_archive := false
@export var current_scene := "c02_s01"
@export var entrance_portal := "left"
@export var debug_reload := false:
set(new_val):
debug_reload = false
if is_node_ready() and current_scene and entrance_portal:
transition_to_scene(current_scene, entrance_portal, 0.0)
# 强制覆盖 archive 记录
@export var force_archive_scene := ""
@export var force_archive_portal := ""
# Nodes
@onready var mask_layer := %MaskLayer as CanvasLayer
@onready var mask := %Mask as ColorRect
# State
var has_entered := false
var ground: Ground2D
var display_start_sec := 0.0
var _frozen_start_time_ms: int
var _allow_ground_start := false:
set(val):
_allow_ground_start = val
if ground and val:
if ground.process_mode != Node.PROCESS_MODE_INHERIT:
SceneManager.ground_start.emit()
ground.process_mode = Node.PROCESS_MODE_INHERIT
print(
"GroundLoader _allow_ground_start: unfrozen. frozen duration(ms):",
Time.get_ticks_msec() - _frozen_start_time_ms
)
# Debug
var update_watcher: Timer
var last_modify_time := 0
func _ready() -> void:
_setup_mask_layer()
if not ignore_archive:
_load_save()
if current_scene and entrance_portal:
# 首次进入渐隐效果
transition_to_scene(current_scene, entrance_portal)
func _setup_mask_layer() -> void:
mask.visible = true
mask.color.a = 0.0
mask_layer.layer = GlobalConfig.CANVAS_LAYER_GROUND_MASK
func _load_save() -> void:
# 强制覆盖 archive 记录
if force_archive_scene or force_archive_portal:
current_scene = force_archive_scene
entrance_portal = force_archive_portal
return
if Engine.is_editor_hint():
return
var archive = ArchiveManager.archive
if archive:
if archive.current_scene:
current_scene = archive.current_scene
if archive.entrance_portal:
entrance_portal = archive.entrance_portal
static func get_ground_scene_uid(scene_name: String) -> String:
if GROUND_SCENE_PATH_DICT.has(scene_name):
return GROUND_SCENE_PATH_DICT[scene_name]["path"]
printerr("GroundLoader get_ground_scene_uid: scene not found:", scene_name)
return ""
static func get_ground_scene_readable_name(scene_name: String) -> String:
if GROUND_SCENE_PATH_DICT.has(scene_name):
return GROUND_SCENE_PATH_DICT[scene_name]["name"]
printerr("GroundLoader get_ground_scene_readable_name: scene not found:", scene_name)
return scene_name
func toggle_mask(
display: bool, wait_time: float, ease_min_duration := EASE_DURATION, mask_color := Color.BLACK
) -> Tween:
var tween = get_tree().create_tween()
mask_color.a = mask.color.a
mask.color = mask_color
var duration = min(ease_min_duration, wait_time * 0.5)
if display:
display_start_sec = Time.get_ticks_msec() * 0.001
tween.tween_property(mask, "color:a", 1.0, duration).set_trans(Tween.TRANS_CUBIC)
else:
# 转场至少 0.6s, 除去 0.3s 最后的淡出,需要 0.3s 的等待时间(包含 mask 的淡入)
if wait_time > 0.0:
var time = Time.get_ticks_msec() * 0.001
wait_time = max(wait_time + display_start_sec - time - EASE_DURATION, 0.0)
if wait_time > 0.0:
tween.tween_interval(wait_time)
tween.tween_property(mask, "color:a", 0.0, duration).set_trans(Tween.TRANS_CUBIC)
return tween
func transition_to_scene(
scene_name: String, portal: String, wait_time := DEFAULT_TRANSITION_TIME
) -> void:
if not GROUND_SCENE_PATH_DICT.has(scene_name):
print("Scene not found: " + scene_name)
return
_pause_current_ground()
current_scene = scene_name
entrance_portal = portal
# 优先更新 archive使 ground 可以访问自己的 current_scene 键值
if not Engine.is_editor_hint():
_update_archive()
if wait_time > 0.0:
_transition_with_effect(scene_name, wait_time)
else:
_allow_ground_start = true
_do_transition.call_deferred(scene_name)
func _pause_current_ground() -> void:
if not ground:
return
print("GroundLoader transition_to_scene: pause prev ground.")
# 先发送,再暂停,允许 sfx 等节点执行 ease out
SceneManager.ground_transition_pre_paused.emit()
ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED)
# print reenter lock status
print("GroundLoader transition_to_scene: reenter lock status: ", ground.reenter_lock)
func _transition_with_effect(scene_name: String, wait_time: float) -> void:
# 转场效果,在 _load_ground_node 之前播放
var tween = toggle_mask(true, wait_time)
tween.tween_callback(_do_transition.call_deferred.bind(scene_name))
_allow_ground_start = false
# 等到 toggle_mask 结束,再重置 freeze 状态
toggle_mask(false, wait_time).tween_callback(func(): _allow_ground_start = true)
func _update_archive() -> void:
var archive = ArchiveManager.archive
if archive:
archive.current_scene = current_scene
archive.entrance_portal = entrance_portal
func _do_transition(scene_name: String) -> void:
print("GroundLoader Transition to scene:", scene_name, "portal:", entrance_portal)
_remove_current_ground()
_load_and_setup_ground(scene_name)
_add_ground()
if _allow_ground_start:
SceneManager.ground_start.emit()
_post_transition()
func _remove_current_ground() -> void:
ground = get_node_or_null("Ground") as Ground2D
if ground:
# 防止命名冲突
remove_child(ground)
ground.queue_free()
func _load_and_setup_ground(scene_name: String) -> void:
# 先设置 ground再添加到场景中
# 因为 ground 在 enter_tree 时会用到 SceneManager 的方法
# 其中间接用到了 GroundLoader 的 ground
ground = _load_ground_node(scene_name)
if not _allow_ground_start:
ground.set_deferred("process_mode", Node.PROCESS_MODE_DISABLED)
print("GroundLoader not _allow_ground_start: frozen (delayed)")
_frozen_start_time_ms = Time.get_ticks_msec()
func _add_ground() -> void:
ground.ready.connect(SceneManager.ground_ready.emit.bind(ground))
ground.name = "Ground"
# 在 add child 之前,调整 ground 内部元素属性,在 on ground ready 前设置完成
if not Engine.is_editor_hint():
_setup_player_position()
add_child(ground)
print(
"GroundLoader add_ground finished:",
ground.scene_name,
" player.pos=",
ground.get_player().global_position
)
# ready 后,再整体重置 camera 位置
if not Engine.is_editor_hint():
ground.get_camera().reset_position_immediately()
has_entered = true
func _setup_player_position() -> void:
if not has_entered:
_update_player_position_from_archive()
else:
ground.move_player_to_portal(entrance_portal)
func _update_player_position_from_archive() -> void:
if ignore_archive or Engine.is_editor_hint():
return
var archive = ArchiveManager.archive
if not archive:
return
var player = ground.get_player() as MainPlayer
if player:
player.global_position.x = archive.player_global_position_x
player.set_facing_direction(archive.player_direction)
ground.reset_player_y()
func _load_ground_node(scene_name: String) -> Ground2D:
if not GROUND_SCENE_PATH_DICT.has(scene_name):
return null
var path = get_ground_scene_uid(scene_name)
var scene: PackedScene = _load_scene_resource(path)
if not scene:
return null
var instance = scene.instantiate() as Node2D
var ground_node = instance.get_child(0)
instance.remove_child(ground_node)
ground_node.owner = null
instance.queue_free()
return ground_node
func _load_scene_resource(path: String) -> PackedScene:
if ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_LOADED:
return ResourceLoader.load_threaded_get(path) as PackedScene
else:
return ResourceLoader.load(path) as PackedScene
func _post_transition() -> void:
if not ground:
return
_preload_neighbor_scenes()
GlobalConfigManager.print_global_info()
func _preload_neighbor_scenes() -> void:
var scene_names: Array[String] = []
var deploy_layer = ground.get_node_or_null("DeployLayer")
if not deploy_layer:
return
for node in deploy_layer.get_children():
var portal = node as Portal2D
if portal and portal.target_scene and GROUND_SCENE_PATH_DICT.has(portal.target_scene):
scene_names.append(portal.target_scene)
if GlobalConfig.DEBUG:
print("preload neighbor scenes:", scene_names)
for scene_name in scene_names:
if GROUND_SCENE_PATH_DICT.has(scene_name):
ResourceLoader.load_threaded_request(get_ground_scene_uid(scene_name))