forked from xiangwang25/whale-town-front-v2
feat: expand multiplayer, chat, and release support
- add network NPC synchronization and dialogue interactions - add world bulletin publishing and display - improve authentication, session refresh, and appearance sync - synchronize player direction and movement animations - improve input focus and progressive Web loading - add macOS/Windows builds and deployment configuration - include required fonts, shaders, and runtime assets
This commit is contained in:
@@ -32,6 +32,7 @@ signal scene_changed(scene_name: String)
|
||||
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)
|
||||
signal scene_pack_load_finished(pack_key: String, success: bool)
|
||||
|
||||
# ============ 成员变量 ============
|
||||
|
||||
@@ -42,14 +43,18 @@ var _next_scene_position: Variant = null # 下一个场景的初始
|
||||
var _next_spawn_name: String = "" # 下一个场景的出生点名称 (String)
|
||||
var _pack_manifest: Dictionary = {}
|
||||
var _loaded_scene_packs: Dictionary = {}
|
||||
var _loading_scene_packs: Dictionary = {}
|
||||
var _scene_pack_prefetch_queue: Array[String] = []
|
||||
var _scene_pack_prefetch_running: bool = false
|
||||
var _active_pack_request: HTTPRequest
|
||||
var _active_pack_scene_name: String = ""
|
||||
var _active_pack_expected_size: int = 0
|
||||
var _pack_overlay: CanvasLayer
|
||||
var _pack_status_label: Label
|
||||
var _pack_progress_bar: ProgressBar
|
||||
var _web_pack_progress_last_emit_msec: int = 0
|
||||
|
||||
const PACK_MANIFEST_PATH: String = "/packs/manifest.json"
|
||||
const PACK_MANIFEST_PATH: String = "packs/manifest.json"
|
||||
const PACK_CACHE_DIR: String = "user://scene-packs"
|
||||
const SCENE_PACK_KEYS: Dictionary = {
|
||||
"main": "auth",
|
||||
@@ -61,6 +66,14 @@ const SCENE_PACK_KEYS: Dictionary = {
|
||||
"personal_space": "personal_space",
|
||||
}
|
||||
|
||||
# 只预取当前地图可以直接到达的区域。
|
||||
# 顺序很重要:登录进入广场后先准备个人空间,再准备打工区;
|
||||
# 咖啡馆只有在进入打工区后才加入队列。
|
||||
const SCENE_PREFETCH_ROUTES: Dictionary = {
|
||||
"square": ["personal_space", "work_zone"],
|
||||
"work_zone": ["cafe_interior"],
|
||||
}
|
||||
|
||||
# 场景路径映射表
|
||||
# 将场景名称映射到实际的文件路径
|
||||
# 便于统一管理和修改场景路径
|
||||
@@ -142,6 +155,7 @@ func change_scene(scene_name: String, use_transition: bool = true):
|
||||
current_scene_name = scene_name
|
||||
is_changing_scene = false
|
||||
scene_changed.emit(scene_name)
|
||||
_schedule_scene_prefetch(scene_name)
|
||||
|
||||
# 隐藏过渡效果
|
||||
if use_transition:
|
||||
@@ -154,13 +168,15 @@ func _process(_delta: float) -> void:
|
||||
return
|
||||
var downloaded := _active_pack_request.get_downloaded_bytes()
|
||||
var total := _active_pack_request.get_body_size()
|
||||
if total <= 0:
|
||||
total = _active_pack_expected_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)
|
||||
_pack_status_label.text = _format_scene_loading_status(downloaded, total)
|
||||
_notify_web_auth_pack_progress(downloaded, total)
|
||||
scene_pack_progress.emit(_active_pack_scene_name, downloaded, total)
|
||||
|
||||
@@ -181,44 +197,105 @@ func _ensure_scene_pack(scene_name: String) -> bool:
|
||||
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)):
|
||||
return true
|
||||
_show_pack_overlay("正在准备场景...")
|
||||
# 场景切换也进入同一个预取队列,避免用户点击入口时与后台下载并发。
|
||||
_queue_scene_pack_prefetch([scene_name])
|
||||
while not bool(_loaded_scene_packs.get(packKey, false)):
|
||||
if not bool(_loading_scene_packs.get(packKey, false)) and not _scene_pack_prefetch_queue.has(scene_name):
|
||||
break
|
||||
await scene_pack_load_finished
|
||||
if bool(_loaded_scene_packs.get(packKey, false)):
|
||||
_hide_pack_overlay()
|
||||
return true
|
||||
_show_pack_error(scene_name, "场景下载失败,请检查网络后重试")
|
||||
return false
|
||||
|
||||
func preload_scene_pack(scene_name: String) -> void:
|
||||
if OS.get_name() != "Web":
|
||||
return
|
||||
_queue_scene_pack_prefetch([scene_name])
|
||||
|
||||
func _schedule_scene_prefetch(scene_name: String) -> void:
|
||||
if OS.get_name() != "Web" or not _should_prefetch_scene_packs():
|
||||
return
|
||||
var routeVariant: Variant = SCENE_PREFETCH_ROUTES.get(scene_name, [])
|
||||
if routeVariant is Array and not (routeVariant as Array).is_empty():
|
||||
_queue_scene_pack_prefetch(routeVariant as Array)
|
||||
|
||||
func _should_prefetch_scene_packs() -> bool:
|
||||
var authManager := get_node_or_null("/root/AuthManager")
|
||||
if authManager == null or not authManager.has_method("is_authenticated"):
|
||||
return false
|
||||
if not bool(authManager.call("is_authenticated")):
|
||||
return false
|
||||
var chatManager := get_node_or_null("/root/ChatManager")
|
||||
return chatManager == null or not chatManager.has_method("is_guest_mode") or not bool(chatManager.call("is_guest_mode"))
|
||||
|
||||
func _queue_scene_pack_prefetch(scene_names: Array) -> void:
|
||||
for sceneNameVariant in scene_names:
|
||||
var sceneName := str(sceneNameVariant).strip_edges()
|
||||
var packKey := str(SCENE_PACK_KEYS.get(sceneName, ""))
|
||||
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)) or bool(_loading_scene_packs.get(packKey, false)):
|
||||
continue
|
||||
if _scene_pack_prefetch_queue.has(sceneName):
|
||||
continue
|
||||
_scene_pack_prefetch_queue.append(sceneName)
|
||||
_run_scene_pack_prefetch_queue()
|
||||
|
||||
func _run_scene_pack_prefetch_queue() -> void:
|
||||
if _scene_pack_prefetch_running:
|
||||
return
|
||||
_scene_pack_prefetch_running = true
|
||||
while not _scene_pack_prefetch_queue.is_empty():
|
||||
var sceneName: String = str(_scene_pack_prefetch_queue.pop_front())
|
||||
var packKey := str(SCENE_PACK_KEYS.get(sceneName, ""))
|
||||
if packKey.is_empty() or bool(_loaded_scene_packs.get(packKey, false)):
|
||||
continue
|
||||
if not bool(_loading_scene_packs.get(packKey, false)):
|
||||
_start_scene_pack_load(sceneName, packKey)
|
||||
while bool(_loading_scene_packs.get(packKey, false)):
|
||||
await scene_pack_load_finished
|
||||
_scene_pack_prefetch_running = false
|
||||
|
||||
func _start_scene_pack_load(scene_name: String, pack_key: String) -> void:
|
||||
_loading_scene_packs[pack_key] = true
|
||||
_load_scene_pack_task(scene_name, pack_key)
|
||||
|
||||
func _load_scene_pack_task(scene_name: String, pack_key: String) -> void:
|
||||
var success := await _load_scene_pack_file(scene_name, pack_key)
|
||||
if success:
|
||||
_loaded_scene_packs[pack_key] = true
|
||||
_loading_scene_packs.erase(pack_key)
|
||||
scene_pack_load_finished.emit(pack_key, success)
|
||||
|
||||
func _load_scene_pack_file(scene_name: String, pack_key: String) -> bool:
|
||||
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, {})
|
||||
var entryVariant: Variant = _pack_manifest.get(pack_key, {})
|
||||
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 expectedSize := int(entry.get("size", 0))
|
||||
var cacheDirAbsolute := ProjectSettings.globalize_path(PACK_CACHE_DIR)
|
||||
DirAccess.make_dir_recursive_absolute(cacheDirAbsolute)
|
||||
var localPath := "%s/%s" % [PACK_CACHE_DIR, fileName]
|
||||
if _is_scene_pack_file_valid(localPath, fileName, expectedSize) 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, "场景下载失败,请检查网络后重试")
|
||||
var packUrl := _resolve_web_url("packs/%s" % fileName)
|
||||
if packUrl.is_empty() or not await _download_pack(scene_name, packUrl, localPath, expectedSize):
|
||||
return false
|
||||
if not _is_scene_pack_file_valid(localPath, fileName, expectedSize):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(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 _is_scene_pack_file_valid(local_path: String, file_name: String, expected_size: int) -> bool:
|
||||
@@ -265,21 +342,24 @@ func _download_pack_manifest() -> 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:
|
||||
func _download_pack(scene_name: String, url: String, local_path: String, expected_size: int) -> bool:
|
||||
var request := HTTPRequest.new()
|
||||
request.timeout = 180.0
|
||||
request.timeout = 900.0
|
||||
request.download_file = local_path
|
||||
add_child(request)
|
||||
_active_pack_request = request
|
||||
_active_pack_scene_name = scene_name
|
||||
_active_pack_expected_size = expected_size
|
||||
if request.request(url) != OK:
|
||||
_active_pack_request = null
|
||||
_active_pack_scene_name = ""
|
||||
_active_pack_expected_size = 0
|
||||
request.queue_free()
|
||||
return false
|
||||
var response: Array = await request.request_completed
|
||||
_active_pack_request = null
|
||||
_active_pack_scene_name = ""
|
||||
_active_pack_expected_size = 0
|
||||
request.queue_free()
|
||||
if response.size() < 2 or int(response[0]) != HTTPRequest.RESULT_SUCCESS:
|
||||
return false
|
||||
@@ -343,6 +423,12 @@ func _format_download_progress(downloaded: int, total: int) -> String:
|
||||
return "%.1f MB" % downloadedMb
|
||||
return "%.1f / %.1f MB" % [downloadedMb, float(total) / 1048576.0]
|
||||
|
||||
func _format_scene_loading_status(downloaded: int, total: int) -> String:
|
||||
if total <= 0:
|
||||
return "正在加载场景..."
|
||||
var percent: int = mini(100, int(round(float(downloaded) / float(total) * 100.0)))
|
||||
return "正在加载场景 %d%%(%s)" % [percent, _format_download_progress(downloaded, total)]
|
||||
|
||||
# ============ 查询方法 ============
|
||||
|
||||
# 获取当前场景名称
|
||||
|
||||
Reference in New Issue
Block a user