430 lines
14 KiB
GDScript
430 lines
14 KiB
GDScript
extends Node
|
||
|
||
# ============================================================================
|
||
# SceneManager.gd - 场景管理器
|
||
# ============================================================================
|
||
# 全局单例管理器,负责场景切换和管理
|
||
#
|
||
# 核心职责:
|
||
# - 场景切换的统一接口
|
||
# - 场景路径映射管理
|
||
# - 场景切换过渡效果
|
||
# - 场景状态跟踪
|
||
#
|
||
# 使用方式:
|
||
# SceneManager.change_scene("main")
|
||
# SceneManager.register_scene("custom", "res://scenes/custom.tscn")
|
||
#
|
||
# 注意事项:
|
||
# - 作为自动加载单例,全局可访问
|
||
# - 场景切换是异步操作,支持过渡效果
|
||
# - 场景名称必须在 scene_paths 中注册
|
||
# ============================================================================
|
||
|
||
# ============ 信号定义 ============
|
||
|
||
# 场景切换完成信号
|
||
# 参数: scene_name - 切换到的场景名称
|
||
signal scene_changed(scene_name: String)
|
||
|
||
# 场景切换开始信号
|
||
# 参数: scene_name - 即将切换到的场景名称
|
||
signal scene_change_started(scene_name: String)
|
||
signal scene_pack_progress(scene_name: String, downloaded_bytes: int, total_bytes: int)
|
||
signal scene_pack_failed(scene_name: String, message: String)
|
||
|
||
# ============ 成员变量 ============
|
||
|
||
# 场景状态
|
||
var current_scene_name: String = "" # 当前场景名称
|
||
var is_changing_scene: bool = false # 是否正在切换场景
|
||
var _next_scene_position: Variant = null # 下一个场景的初始位置 (Vector2 or null)
|
||
var _next_spawn_name: String = "" # 下一个场景的出生点名称 (String)
|
||
var _pack_manifest: Dictionary = {}
|
||
var _loaded_scene_packs: Dictionary = {}
|
||
var _active_pack_request: HTTPRequest
|
||
var _active_pack_scene_name: String = ""
|
||
var _pack_overlay: CanvasLayer
|
||
var _pack_status_label: Label
|
||
var _pack_progress_bar: ProgressBar
|
||
|
||
const PACK_MANIFEST_PATH: String = "/packs/manifest.json"
|
||
const PACK_CACHE_DIR: String = "user://scene-packs"
|
||
const SCENE_PACK_KEYS: Dictionary = {
|
||
"main": "auth",
|
||
"auth": "auth",
|
||
"square": "square",
|
||
"work_zone": "work_zone",
|
||
"cafe_interior": "cafe_interior",
|
||
"room": "personal_space",
|
||
"personal_space": "personal_space",
|
||
}
|
||
|
||
# 场景路径映射表
|
||
# 将场景名称映射到实际的文件路径
|
||
# 便于统一管理和修改场景路径
|
||
var scene_paths: Dictionary = {
|
||
"main": "res://scenes/ui/AuthScene.tscn", # 认证入口
|
||
"auth": "res://scenes/ui/AuthScene.tscn", # 登录/注册
|
||
"square": "res://scenes/Maps/square.tscn", # 广场地图
|
||
"work_zone": "res://scenes/Maps/work_zone.tscn", # 打工区地图
|
||
"cafe_interior": "res://scenes/Maps/cafe_interior.tscn", # 咖啡馆室内
|
||
"room": "res://scenes/Maps/personal_space.tscn", # 房间地图(兼容旧入口)
|
||
"personal_space": "res://scenes/Maps/personal_space.tscn", # 个人空间
|
||
"fountain": "res://scenes/Maps/fountain.tscn", # 喷泉地图
|
||
"datawhale_home": "res://scenes/Maps/datawhale_home.tscn", # 数据鲸鱼之家
|
||
"community": "res://scenes/Maps/community.tscn" # 社区地图
|
||
}
|
||
|
||
# ============ 场景切换方法 ============
|
||
|
||
# 切换到指定场景
|
||
#
|
||
# 参数:
|
||
# scene_name: String - 要切换到的场景名称(必须在scene_paths中注册)
|
||
# use_transition: bool - 是否使用过渡效果,默认为true
|
||
#
|
||
# 返回值:
|
||
# bool - 切换是否成功
|
||
#
|
||
# 功能:
|
||
# - 检查场景切换状态和场景是否存在
|
||
# - 显示过渡效果(可选)
|
||
# - 执行场景切换
|
||
# - 更新当前场景状态
|
||
# - 发送相关信号
|
||
#
|
||
# 使用示例:
|
||
# var success = SceneManager.change_scene("main", true)
|
||
#
|
||
# 注意事项:
|
||
# - 场景切换是异步操作
|
||
# - 切换过程中会阻止新的切换请求
|
||
# - 场景名称必须预先注册
|
||
func change_scene(scene_name: String, use_transition: bool = true):
|
||
# 防止重复切换
|
||
if is_changing_scene:
|
||
push_warning("SceneManager: 场景切换中,忽略新的切换请求")
|
||
return false
|
||
|
||
# 检查场景是否存在
|
||
if not scene_paths.has(scene_name):
|
||
push_error("SceneManager: 未找到场景 %s" % scene_name)
|
||
return false
|
||
|
||
var scene_path = scene_paths[scene_name]
|
||
|
||
# 设置切换状态
|
||
is_changing_scene = true
|
||
scene_change_started.emit(scene_name)
|
||
|
||
# 显示过渡效果
|
||
if use_transition:
|
||
await show_transition()
|
||
|
||
if OS.get_name() == "Web" and SCENE_PACK_KEYS.has(scene_name):
|
||
var packLoaded := await _ensure_scene_pack(scene_name)
|
||
if not packLoaded:
|
||
is_changing_scene = false
|
||
if use_transition:
|
||
await hide_transition()
|
||
return false
|
||
|
||
# 执行场景切换
|
||
var error = get_tree().change_scene_to_file(scene_path)
|
||
if error != OK:
|
||
push_error("SceneManager: 场景切换失败 %s -> %s, 错误码: %d" % [current_scene_name, scene_name, error])
|
||
is_changing_scene = false
|
||
return false
|
||
|
||
# 更新状态
|
||
current_scene_name = scene_name
|
||
is_changing_scene = false
|
||
scene_changed.emit(scene_name)
|
||
|
||
# 隐藏过渡效果
|
||
if use_transition:
|
||
await hide_transition()
|
||
|
||
return true
|
||
|
||
func _process(_delta: float) -> void:
|
||
if not is_instance_valid(_active_pack_request):
|
||
return
|
||
var downloaded := _active_pack_request.get_downloaded_bytes()
|
||
var total := _active_pack_request.get_body_size()
|
||
if is_instance_valid(_pack_progress_bar):
|
||
_pack_progress_bar.indeterminate = total <= 0
|
||
if total > 0:
|
||
_pack_progress_bar.max_value = total
|
||
_pack_progress_bar.value = downloaded
|
||
if is_instance_valid(_pack_status_label):
|
||
_pack_status_label.text = "正在加载场景 %s" % _format_download_progress(downloaded, total)
|
||
scene_pack_progress.emit(_active_pack_scene_name, downloaded, total)
|
||
|
||
func _ensure_scene_pack(scene_name: String) -> bool:
|
||
var packKey := str(SCENE_PACK_KEYS.get(scene_name, ""))
|
||
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)):
|
||
return true
|
||
_show_pack_overlay("正在准备场景...")
|
||
if _pack_manifest.is_empty():
|
||
_pack_manifest = await _download_pack_manifest()
|
||
if _pack_manifest.is_empty():
|
||
_show_pack_error(scene_name, "场景清单加载失败,请检查网络后重试")
|
||
return false
|
||
var entryVariant: Variant = _pack_manifest.get(packKey, {})
|
||
if not (entryVariant is Dictionary):
|
||
_show_pack_error(scene_name, "服务器缺少场景资源:%s" % packKey)
|
||
return false
|
||
var entry := entryVariant as Dictionary
|
||
var fileName := str(entry.get("file", "")).get_file()
|
||
if fileName.is_empty() or not fileName.ends_with(".pck"):
|
||
_show_pack_error(scene_name, "场景资源清单格式错误")
|
||
return false
|
||
var cacheDirAbsolute := ProjectSettings.globalize_path(PACK_CACHE_DIR)
|
||
DirAccess.make_dir_recursive_absolute(cacheDirAbsolute)
|
||
var localPath := "%s/%s" % [PACK_CACHE_DIR, fileName]
|
||
if FileAccess.file_exists(localPath) and ProjectSettings.load_resource_pack(localPath, true):
|
||
_loaded_scene_packs[packKey] = true
|
||
_hide_pack_overlay()
|
||
return true
|
||
if FileAccess.file_exists(localPath):
|
||
DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath))
|
||
var packUrl := _resolve_web_url("/packs/%s" % fileName)
|
||
if packUrl.is_empty() or not await _download_pack(scene_name, packUrl, localPath):
|
||
_show_pack_error(scene_name, "场景下载失败,请检查网络后重试")
|
||
return false
|
||
if not ProjectSettings.load_resource_pack(localPath, true):
|
||
DirAccess.remove_absolute(ProjectSettings.globalize_path(localPath))
|
||
_show_pack_error(scene_name, "场景资源损坏,请重新进入")
|
||
return false
|
||
_loaded_scene_packs[packKey] = true
|
||
_hide_pack_overlay()
|
||
return true
|
||
|
||
func _download_pack_manifest() -> Dictionary:
|
||
var request := HTTPRequest.new()
|
||
request.timeout = 12.0
|
||
add_child(request)
|
||
var url := _resolve_web_url(PACK_MANIFEST_PATH)
|
||
if url.is_empty() or request.request(url, ["Cache-Control: no-cache"]) != OK:
|
||
request.queue_free()
|
||
return {}
|
||
var response: Array = await request.request_completed
|
||
request.queue_free()
|
||
if response.size() < 4 or int(response[0]) != HTTPRequest.RESULT_SUCCESS:
|
||
return {}
|
||
var responseCode := int(response[1])
|
||
if responseCode < 200 or responseCode >= 300:
|
||
return {}
|
||
var json := JSON.new()
|
||
if json.parse((response[3] as PackedByteArray).get_string_from_utf8()) != OK:
|
||
return {}
|
||
if not (json.data is Dictionary):
|
||
return {}
|
||
var root := json.data as Dictionary
|
||
var packsVariant: Variant = root.get("packs", {})
|
||
return packsVariant as Dictionary if packsVariant is Dictionary else {}
|
||
|
||
func _download_pack(scene_name: String, url: String, local_path: String) -> bool:
|
||
var request := HTTPRequest.new()
|
||
request.timeout = 180.0
|
||
request.download_file = local_path
|
||
add_child(request)
|
||
_active_pack_request = request
|
||
_active_pack_scene_name = scene_name
|
||
if request.request(url) != OK:
|
||
_active_pack_request = null
|
||
_active_pack_scene_name = ""
|
||
request.queue_free()
|
||
return false
|
||
var response: Array = await request.request_completed
|
||
_active_pack_request = null
|
||
_active_pack_scene_name = ""
|
||
request.queue_free()
|
||
if response.size() < 2 or int(response[0]) != HTTPRequest.RESULT_SUCCESS:
|
||
return false
|
||
var responseCode := int(response[1])
|
||
return responseCode >= 200 and responseCode < 300
|
||
|
||
func _resolve_web_url(path: String) -> String:
|
||
if not Engine.has_singleton("JavaScriptBridge"):
|
||
return ""
|
||
var jsBridge: Object = Engine.get_singleton("JavaScriptBridge")
|
||
if jsBridge == null:
|
||
return ""
|
||
return str(jsBridge.eval("new URL('%s', window.location.href).href" % path, true)).strip_edges()
|
||
|
||
func _show_pack_overlay(message: String) -> void:
|
||
if not is_instance_valid(_pack_overlay):
|
||
_pack_overlay = CanvasLayer.new()
|
||
_pack_overlay.layer = 1000
|
||
add_child(_pack_overlay)
|
||
var backdrop := ColorRect.new()
|
||
backdrop.color = Color(0.025, 0.09, 0.15, 0.92)
|
||
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_pack_overlay.add_child(backdrop)
|
||
var center := CenterContainer.new()
|
||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
_pack_overlay.add_child(center)
|
||
var box := VBoxContainer.new()
|
||
box.custom_minimum_size = Vector2(420, 100)
|
||
box.add_theme_constant_override("separation", 18)
|
||
center.add_child(box)
|
||
_pack_status_label = Label.new()
|
||
_pack_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
_pack_status_label.add_theme_font_size_override("font_size", 22)
|
||
box.add_child(_pack_status_label)
|
||
_pack_progress_bar = ProgressBar.new()
|
||
_pack_progress_bar.custom_minimum_size = Vector2(420, 14)
|
||
_pack_progress_bar.show_percentage = false
|
||
_pack_progress_bar.indeterminate = true
|
||
box.add_child(_pack_progress_bar)
|
||
_pack_overlay.show()
|
||
_pack_status_label.text = message
|
||
_pack_progress_bar.indeterminate = true
|
||
|
||
func _hide_pack_overlay() -> void:
|
||
if is_instance_valid(_pack_overlay):
|
||
_pack_overlay.hide()
|
||
|
||
func _show_pack_error(scene_name: String, message: String) -> void:
|
||
if not is_instance_valid(_pack_overlay):
|
||
_show_pack_overlay(message)
|
||
_pack_status_label.text = message
|
||
_pack_progress_bar.hide()
|
||
scene_pack_failed.emit(scene_name, message)
|
||
await get_tree().create_timer(3.0).timeout
|
||
_pack_progress_bar.show()
|
||
_hide_pack_overlay()
|
||
|
||
func _format_download_progress(downloaded: int, total: int) -> String:
|
||
var downloadedMb := float(downloaded) / 1048576.0
|
||
if total <= 0:
|
||
return "%.1f MB" % downloadedMb
|
||
return "%.1f / %.1f MB" % [downloadedMb, float(total) / 1048576.0]
|
||
|
||
# ============ 查询方法 ============
|
||
|
||
# 获取当前场景名称
|
||
#
|
||
# 返回值:
|
||
# String - 当前场景的名称
|
||
func get_current_scene_name() -> String:
|
||
return current_scene_name
|
||
|
||
# ============ 场景位置和出生点管理 ============
|
||
|
||
# 设置下一个场景的初始位置
|
||
#
|
||
# 参数:
|
||
# pos: Vector2 - 玩家在下一个场景的初始位置
|
||
#
|
||
# 功能:
|
||
# - 用于场景切换时传递玩家位置信息
|
||
# - 配合 DoorTeleport 等传送机制使用
|
||
#
|
||
# 使用示例:
|
||
# SceneManager.set_next_scene_position(Vector2(100, 200))
|
||
# SceneManager.change_scene("room")
|
||
func set_next_scene_position(pos: Vector2) -> void:
|
||
_next_scene_position = pos
|
||
|
||
# 获取并清除下一个场景的初始位置
|
||
#
|
||
# 返回值:
|
||
# Variant - Vector2 位置或 null(如果未设置)
|
||
#
|
||
# 功能:
|
||
# - 获取预设的场景初始位置
|
||
# - 获取后自动清除,避免影响后续场景切换
|
||
#
|
||
# 注意事项:
|
||
# - 此方法会清除存储的位置,只能获取一次
|
||
# - 如果未设置位置,返回 null
|
||
func get_next_scene_position() -> Variant:
|
||
var pos = _next_scene_position
|
||
_next_scene_position = null
|
||
return pos
|
||
|
||
# 设置下一个场景的出生点名称
|
||
#
|
||
# 参数:
|
||
# spawn_name: String - 出生点节点的名称
|
||
#
|
||
# 功能:
|
||
# - 指定玩家在下一个场景应该出现在哪个出生点
|
||
# - 配合场景中的 Marker2D 出生点使用
|
||
#
|
||
# 使用示例:
|
||
# SceneManager.set_next_spawn_name("DoorExit")
|
||
# SceneManager.change_scene("square")
|
||
func set_next_spawn_name(spawn_name: String) -> void:
|
||
_next_spawn_name = spawn_name
|
||
|
||
# 获取并清除下一个场景的出生点名称
|
||
#
|
||
# 返回值:
|
||
# String - 出生点名称(如果未设置则返回空字符串)
|
||
#
|
||
# 功能:
|
||
# - 获取预设的出生点名称
|
||
# - 获取后自动清除,避免影响后续场景切换
|
||
#
|
||
# 注意事项:
|
||
# - 此方法会清除存储的名称,只能获取一次
|
||
# - 如果未设置名称,返回空字符串
|
||
func get_next_spawn_name() -> String:
|
||
var spawn_name: String = _next_spawn_name
|
||
_next_spawn_name = ""
|
||
return spawn_name
|
||
|
||
# ============ 场景注册方法 ============
|
||
|
||
# 注册新场景
|
||
#
|
||
# 参数:
|
||
# scene_name: String - 场景名称(用于切换时引用)
|
||
# scene_path: String - 场景文件路径
|
||
#
|
||
# 功能:
|
||
# - 将场景名称和路径添加到映射表
|
||
# - 支持运行时动态注册场景
|
||
#
|
||
# 使用示例:
|
||
# SceneManager.register_scene("boss_battle", "res://scenes/boss/boss_battle.tscn")
|
||
func register_scene(scene_name: String, scene_path: String):
|
||
scene_paths[scene_name] = scene_path
|
||
|
||
# ============ 过渡效果方法 ============
|
||
|
||
# 显示场景切换过渡效果
|
||
#
|
||
# 功能:
|
||
# - 显示场景切换时的过渡动画
|
||
# - 为用户提供视觉反馈
|
||
#
|
||
# 注意事项:
|
||
# - 这是异步方法,需要await等待完成
|
||
# - 当前实现为简单的延时,可扩展为复杂动画
|
||
#
|
||
# TODO: 实现淡入淡出、滑动等过渡效果
|
||
func show_transition():
|
||
# TODO: 实现场景切换过渡效果(当前仅占位延时)
|
||
await get_tree().create_timer(0.2).timeout
|
||
|
||
# 隐藏场景切换过渡效果
|
||
#
|
||
# 功能:
|
||
# - 隐藏场景切换完成后的过渡动画
|
||
# - 恢复正常的游戏显示
|
||
#
|
||
# 注意事项:
|
||
# - 这是异步方法,需要await等待完成
|
||
# - 与show_transition()配对使用
|
||
#
|
||
# TODO: 实现与show_transition()对应的隐藏效果
|
||
func hide_transition():
|
||
# TODO: 隐藏场景切换过渡效果(当前仅占位延时)
|
||
await get_tree().create_timer(0.2).timeout
|