存档重构,游戏内容解锁机制;教程完善(未完成)
This commit is contained in:
@@ -21,6 +21,14 @@ namespace Ichni.Menu
|
||||
public Sprite avatar;
|
||||
public Switch chapterSwitch;
|
||||
|
||||
/// <summary>
|
||||
/// 章节剧情入口与选曲入口共用的内容解锁规则。
|
||||
/// 留空表示章节从新档开始即可访问;需要锁定时请使用 <see cref="UnlockRequirement"/>,
|
||||
/// 并遵守 <see cref="UnlockSaveModule.KeyNamingRule"/> 中的下划线 Key 规则。
|
||||
/// </summary>
|
||||
[LabelText("Unlock Requirement")]
|
||||
public UnlockRequirement unlockRequirement = new UnlockRequirement();
|
||||
|
||||
[Searchable]
|
||||
public List<SongItemData> songs = new List<SongItemData>();
|
||||
|
||||
@@ -54,11 +62,6 @@ namespace Ichni.Menu
|
||||
|
||||
public partial class ChapterSelectionUnit
|
||||
{
|
||||
public List<string> GetRelatedSongNamesOfUnlockKey(string key)
|
||||
{
|
||||
return (from song in songs where song.storyUnlockKey == key select song.songName).ToList();
|
||||
}
|
||||
|
||||
public (int, int, int, int, int) GetChapterSaveInfo()
|
||||
{
|
||||
int beatmapCount = 0;
|
||||
@@ -72,9 +75,19 @@ namespace Ichni.Menu
|
||||
if (GameSaveManager.instance.SongSaveModule.songStatusSaves.TryGetValue(song.songName, out var songStatus))
|
||||
{
|
||||
bool thisSongFinished = false;
|
||||
foreach (BeatmapSave beatmapSave in songStatus.beatmapSaves)
|
||||
foreach (DifficultyData difficulty in song.difficultyDataList)
|
||||
{
|
||||
if (difficulty == null || difficulty.saveDifficultyId < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
beatmapCount++;
|
||||
if (!songStatus.TryGetBeatmapSave(difficulty.saveDifficultyId, out BeatmapSave beatmapSave))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!beatmapSave.IsEmpty())
|
||||
{
|
||||
thisSongFinished = true;
|
||||
@@ -131,17 +144,24 @@ namespace Ichni.Menu
|
||||
[FoldoutGroup("$songName")]
|
||||
public List<DifficultyData> difficultyDataList;
|
||||
|
||||
/// <summary>
|
||||
/// 歌曲自身的内容解锁规则。章节是否开放由 <see cref="ChapterSelectionUnit.unlockRequirement"/> 单独判断;
|
||||
/// 真正进入歌曲时两者必须同时满足。
|
||||
/// </summary>
|
||||
[FoldoutGroup("$songName")]
|
||||
public string storyUnlockKey;
|
||||
|
||||
[FoldoutGroup("$songName")]
|
||||
public string paymentUnlockKey;
|
||||
[LabelText("Unlock Requirement")]
|
||||
public UnlockRequirement unlockRequirement = new UnlockRequirement();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class DifficultyData
|
||||
{
|
||||
public int difficultyIndex;
|
||||
/// <summary>
|
||||
/// 歌曲成绩的稳定 ID,不等同于选曲 UI 的难度列表位置。
|
||||
/// 发布后不要更改或复用既有 ID;新增难度必须使用从未分配过的非负 ID。
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("difficultyIndex")]
|
||||
public int saveDifficultyId;
|
||||
public string difficultyName;
|
||||
public string displayDifficultyName;
|
||||
public int difficultyValue;
|
||||
@@ -149,14 +169,19 @@ namespace Ichni.Menu
|
||||
public Color color;
|
||||
public bool isAvailable;
|
||||
public bool ForceFullScreenJudge = false;
|
||||
|
||||
[MinValue(1)]
|
||||
[Tooltip("仅当 Note、Timing 或判定规则变化,导致成绩不可与旧谱面比较时递增;UI、文本、封面和音量调整不递增。")]
|
||||
public int chartRevision = 1;
|
||||
|
||||
public DifficultyData()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public DifficultyData(int difficultyIndex, string difficultyName, string displayDifficultyName, int difficultyValue, string charterName, Color color, bool ForceFullScreenJudge = false)
|
||||
public DifficultyData(int saveDifficultyId, string difficultyName, string displayDifficultyName, int difficultyValue, string charterName, Color color, bool ForceFullScreenJudge = false)
|
||||
{
|
||||
this.difficultyIndex = difficultyIndex;
|
||||
this.saveDifficultyId = saveDifficultyId;
|
||||
this.difficultyName = difficultyName;
|
||||
this.displayDifficultyName = displayDifficultyName;
|
||||
this.charterName = charterName;
|
||||
@@ -170,5 +195,13 @@ namespace Ichni.Menu
|
||||
{
|
||||
return string.IsNullOrEmpty(displayDifficultyName) ? difficultyName : displayDifficultyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回合法的 Revision。配置侧始终至少使用 1;0 只会被视为未初始化值并被钳制。
|
||||
/// </summary>
|
||||
public int GetChartRevision()
|
||||
{
|
||||
return Mathf.Max(1, chartRevision);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,26 @@ using UnityEngine.Serialization;
|
||||
|
||||
namespace Ichni
|
||||
{
|
||||
/// <summary>
|
||||
/// GameScene 返回 MenuScene 后应恢复的页面。
|
||||
/// 该枚举替代互斥的布尔标记,避免普通歌曲与教程返回状态同时残留。
|
||||
/// </summary>
|
||||
public enum MenuReturnDestination
|
||||
{
|
||||
None,
|
||||
SongSelection,
|
||||
Story
|
||||
}
|
||||
|
||||
public class InformationTransistor : MonoBehaviour
|
||||
{
|
||||
public static InformationTransistor instance;
|
||||
|
||||
public bool isReturnedFromGame;
|
||||
public bool isReturnedFromTutorial;
|
||||
|
||||
/// <summary>
|
||||
/// 当前游戏运行结束后,MenuScene 应恢复的唯一目标页面。
|
||||
/// 运行期间保存在 DontDestroyOnLoad 对象中,不写入玩家存档。
|
||||
/// </summary>
|
||||
public MenuReturnDestination menuReturnDestination;
|
||||
|
||||
public ChapterSelectionUnit chapter;
|
||||
public SongItemData song;
|
||||
@@ -30,8 +44,7 @@ namespace Ichni
|
||||
{
|
||||
instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
isReturnedFromGame = false;
|
||||
isReturnedFromTutorial = false;
|
||||
menuReturnDestination = MenuReturnDestination.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -42,11 +55,48 @@ namespace Ichni
|
||||
public void SetInformation(ChapterSelectionUnit chapter,
|
||||
SongItemData song, DifficultyData difficulty)
|
||||
{
|
||||
// 保留旧入口,供现有选曲流程在播放过场前预先写入歌曲信息。
|
||||
// 真正进入 GameScene 时,MenuManager.EnterGame 会补充 SongSelection 返回目标。
|
||||
PrepareGameLaunch(chapter, song, difficulty, MenuReturnDestination.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一准备一次 GameScene 运行所需的上下文。
|
||||
/// 普通选曲和 TutorialBlock 都必须通过此入口写入,防止章节、歌曲、难度与返回目标来自不同旧运行。
|
||||
/// </summary>
|
||||
public bool PrepareGameLaunch(ChapterSelectionUnit chapter, SongItemData song,
|
||||
DifficultyData difficulty, MenuReturnDestination returnDestination)
|
||||
{
|
||||
if (chapter == null || song == null || difficulty == null)
|
||||
{
|
||||
Debug.LogWarning("[InformationTransistor] 缺少章节、歌曲或难度,无法准备 GameScene 上下文。");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.chapter = chapter;
|
||||
this.song = song;
|
||||
this.difficulty = difficulty;
|
||||
this.chapterSwitch = chapter.chapterSwitch;
|
||||
this.songSwitch = song.songSwitch;
|
||||
chapterSwitch = chapter.chapterSwitch;
|
||||
songSwitch = song.songSwitch;
|
||||
menuReturnDestination = returnDestination;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由现有选曲过场在真正激活 GameScene 前设置返回目标。
|
||||
/// </summary>
|
||||
public void SetMenuReturnDestination(MenuReturnDestination returnDestination)
|
||||
{
|
||||
menuReturnDestination = returnDestination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MenuScene 已依据返回目标完成恢复后调用。
|
||||
/// 只清除一次性路由,不清除歌曲与难度,以免当前 UI 的运行时缓存被意外破坏。
|
||||
/// </summary>
|
||||
public void ClearMenuReturnDestination()
|
||||
{
|
||||
menuReturnDestination = MenuReturnDestination.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,29 +24,70 @@ namespace Ichni.Menu
|
||||
}
|
||||
}
|
||||
|
||||
public SongSelectionRecord GetRecordOfThisChapter()
|
||||
/// <summary>
|
||||
/// 取得当前 Chapter 的选曲缓存;缓存缺失时创建“第一首有效歌曲 + 难度位置 0”的默认记录。
|
||||
/// <para>此方法只保证缓存对象可用,不保证该歌曲或难度仍能游玩。歌曲失效与难度回退由选曲 UI 在
|
||||
/// 每次进入页面时完成,避免把运行期缓存变成持久化存档。</para>
|
||||
/// </summary>
|
||||
public bool TryGetRecordOfThisChapter(out SongSelectionRecord record)
|
||||
{
|
||||
record = null;
|
||||
ChapterSelectionUnit currentChapter = ChapterSelectionManager.instance.currentChapter;
|
||||
if (songSelectionRecords.TryGetValue(currentChapter, out SongSelectionRecord record))
|
||||
if (currentChapter == null || currentChapter.songs == null)
|
||||
{
|
||||
return record;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
||||
if (songSelectionRecords.TryGetValue(currentChapter, out SongSelectionRecord cachedRecord) && cachedRecord != null)
|
||||
{
|
||||
return new SongSelectionRecord(currentChapter.songs[0], currentChapter.songs[0].difficultyDataList[0]);
|
||||
record = cachedRecord;
|
||||
return true;
|
||||
}
|
||||
|
||||
SongItemData firstValidSong = currentChapter.songs.Find(song => song != null);
|
||||
if (firstValidSong == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
record = new SongSelectionRecord(firstValidSong, 0);
|
||||
songSelectionRecords[currentChapter] = record;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将缓存修正为当前 Chapter 中实际存在的歌曲,并保留调用方提供的难度列表偏好位置。
|
||||
/// 这里只写内存缓存,玩家重启游戏后不会恢复该选择。
|
||||
/// </summary>
|
||||
public void SetRecordForChapter(ChapterSelectionUnit chapter, SongItemData song, int difficultyListIndex)
|
||||
{
|
||||
if (chapter == null || song == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
songSelectionRecords[chapter] = new SongSelectionRecord(song, difficultyListIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public class SongSelectionRecord
|
||||
{
|
||||
public SongItemData song;
|
||||
public int difficultyIndex;
|
||||
|
||||
public SongSelectionRecord(SongItemData song, DifficultyData difficulty)
|
||||
public int difficultyListIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 使用难度在歌曲列表中的位置构建缓存。
|
||||
/// 这是选曲 UI 的临时偏好,不等同于 DifficultyData.saveDifficultyId 的游戏记录稳定 ID。
|
||||
/// </summary>
|
||||
public SongSelectionRecord(SongItemData song, int difficultyListIndex)
|
||||
{
|
||||
this.song = song;
|
||||
difficultyIndex = song.difficultyDataList.IndexOf(difficulty);
|
||||
this.difficultyListIndex = difficultyListIndex;
|
||||
}
|
||||
|
||||
public SongSelectionRecord(SongItemData song, DifficultyData difficulty)
|
||||
: this(song, song?.difficultyDataList?.IndexOf(difficulty) ?? 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using AK.Wwise;
|
||||
using Ichni.Menu;
|
||||
using Ichni.Menu.UI;
|
||||
using Ichni.Story;
|
||||
using Ichni.Story.UI;
|
||||
using Ichni.UI;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.WwiseAssistance;
|
||||
using UniRx;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Serialization;
|
||||
@@ -21,6 +24,7 @@ namespace Ichni
|
||||
public LoginPage loginPage;
|
||||
public ChapterSelectionUIPage chapterSelectionUIPage;
|
||||
public StoryUIPage storyUIPage;
|
||||
public SelectionBoxUIPage selectionBoxUIPage;
|
||||
// dialogUIPage 已随旧对话系统移除;阶段 3 将接入新的 VN 对话页面引用
|
||||
public SongSelectionUIPage songSelectionUIPage;
|
||||
public TransitionUIPage transitionUIPage;
|
||||
@@ -34,6 +38,12 @@ namespace Ichni
|
||||
{
|
||||
AsyncOperation asyncOperation;
|
||||
public bool isEnteringGame;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 MenuScene 是否已经完成 GameScene 预加载且尚未处于切场中。
|
||||
/// TutorialFlowController 在写入“已处理”剧情变量前使用此状态进行最后一次启动前检查。
|
||||
/// </summary>
|
||||
public bool CanBeginGame => !isEnteringGame && asyncOperation != null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -44,24 +54,39 @@ namespace Ichni
|
||||
{
|
||||
isEnteringGame = false;
|
||||
|
||||
if (InformationTransistor.instance.isReturnedFromGame)
|
||||
InformationTransistor information = InformationTransistor.instance;
|
||||
MenuReturnDestination returnDestination = information != null
|
||||
? information.menuReturnDestination
|
||||
: MenuReturnDestination.None;
|
||||
|
||||
if (returnDestination == MenuReturnDestination.SongSelection &&
|
||||
information != null && information.chapter != null &&
|
||||
information.song != null && information.difficulty != null)
|
||||
{
|
||||
startUIPage.mainCanvasGroup.gameObject.SetActive(false);
|
||||
ChapterSelectionManager.instance.currentChapter =
|
||||
InformationTransistor.instance.chapter;
|
||||
information.chapter;
|
||||
AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch);
|
||||
MenuInformationRecorder.instance.songSelectionRecords.Add(
|
||||
ChapterSelectionManager.instance.currentChapter,
|
||||
new SongSelectionRecord(InformationTransistor.instance.song, InformationTransistor.instance.difficulty));
|
||||
MenuInformationRecorder.instance.songSelectionRecords[
|
||||
ChapterSelectionManager.instance.currentChapter] =
|
||||
new SongSelectionRecord(information.song, information.difficulty);
|
||||
songSelectionUIPage.FadeIn();
|
||||
}
|
||||
else if (InformationTransistor.instance.isReturnedFromTutorial)
|
||||
else if (returnDestination == MenuReturnDestination.Story &&
|
||||
information != null && information.chapter != null)
|
||||
{
|
||||
startUIPage.mainCanvasGroup.gameObject.SetActive(false);
|
||||
ChapterSelectionManager.instance.currentChapter = InformationTransistor.instance.chapter;
|
||||
ChapterSelectionManager.instance.currentChapter = information.chapter;
|
||||
AudioManager.SetSwitch(ChapterSelectionManager.instance.currentChapter.chapterSwitch);
|
||||
|
||||
// MenuScene 会在教程游玩后重新创建,必须显式重建章节故事树,
|
||||
// 不能依赖旧场景中残留的 Block 实例。
|
||||
StoryManager.instance?.OpenChapter(ChapterSelectionManager.instance.currentChapter.chapterIndex);
|
||||
storyUIPage.FadeIn();
|
||||
}
|
||||
|
||||
// 返回目标是一次性运行态信息。无论是否能恢复页面,都不能让它在下次重进菜单时重复生效。
|
||||
information?.ClearMenuReturnDestination();
|
||||
|
||||
Application.targetFrameRate = SettingsManager.instance.gameSettings.targetFrame;
|
||||
asyncOperation = SceneManager.LoadSceneAsync("GameScene");
|
||||
@@ -73,15 +98,59 @@ namespace Ichni
|
||||
{
|
||||
public void EnterGame()
|
||||
{
|
||||
InformationTransistor.instance.isReturnedFromTutorial = false;
|
||||
InformationTransistor.instance.isReturnedFromGame = true;
|
||||
EnterGame(MenuReturnDestination.SongSelection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活已预加载的 GameScene,并写入唯一的菜单返回目标。
|
||||
/// 普通选曲使用 <see cref="MenuReturnDestination.SongSelection"/>,教程使用
|
||||
/// <see cref="MenuReturnDestination.Story"/>。
|
||||
/// </summary>
|
||||
public void EnterGame(MenuReturnDestination returnDestination)
|
||||
{
|
||||
InformationTransistor.instance?.SetMenuReturnDestination(returnDestination);
|
||||
EnterGameScene();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 TutorialBlock 等非选曲入口准备上下文并播放通用转场。
|
||||
/// 运行成功后会返回 true;返回 false 时不会修改剧情变量或存档,调用方应保留原状。
|
||||
/// </summary>
|
||||
public bool TryBeginGame(ChapterSelectionUnit chapter, SongItemData song,
|
||||
DifficultyData difficulty, MenuReturnDestination returnDestination, UIPageBase sourcePage)
|
||||
{
|
||||
if (!CanBeginGame)
|
||||
{
|
||||
Debug.LogWarning("[MenuManager] GameScene 未完成预加载或菜单已在切换,忽略启动请求。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (InformationTransistor.instance == null ||
|
||||
!InformationTransistor.instance.PrepareGameLaunch(chapter, song, difficulty, returnDestination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
isEnteringGame = true;
|
||||
sourcePage?.FadeOut();
|
||||
transitionUIPage?.FadeIn();
|
||||
AudioManager.Post(AK.EVENTS.ENTERTOGAME);
|
||||
|
||||
Observable.Timer(TimeSpan.FromSeconds(0.6f)).Subscribe(_ => EnterGameScene());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void EnterGameScene()
|
||||
{
|
||||
MenuInputManager.instance.gameInput.Menu.Disable();
|
||||
if (asyncOperation == null)
|
||||
{
|
||||
Debug.LogError("[MenuManager] GameScene 尚未完成预加载,无法进入游戏。");
|
||||
isEnteringGame = false;
|
||||
return;
|
||||
}
|
||||
|
||||
MenuInputManager.instance?.gameInput?.Menu.Disable();
|
||||
asyncOperation.allowSceneActivation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/Scripts/Menu/MessagePage.meta
Normal file
8
Assets/Scripts/Menu/MessagePage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4029d987b6779e440b58bc5e9b457db1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Assets/Scripts/Menu/MessagePage/MessageUIPage.cs
Normal file
144
Assets/Scripts/Menu/MessagePage/MessageUIPage.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Menu.UI;
|
||||
using Ichni.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Story.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 用于在剧情系统中管理并顺序展示 MessageBox 的独立 UI 页面。
|
||||
/// 支持动态实例化给定的 MessageBox Prefab,并在所有消息展示完毕后进行大页面的 FadeOut。
|
||||
/// </summary>
|
||||
public class MessageUIPage : UIPageBase
|
||||
{
|
||||
public static MessageUIPage instance;
|
||||
|
||||
[Header("Prefabs & Containers")]
|
||||
[Tooltip("默认使用的 MessageBox 预制体(请从 Project 中拖拽,而非场景中的实例)")]
|
||||
public MessageBox defaultMessageBoxPrefab;
|
||||
|
||||
[Tooltip("生成的 MessageBox 挂载在哪?如果不填,默认挂载在自己身上")]
|
||||
public Transform messageContainer;
|
||||
|
||||
// 用于排队的内部消息数据结构
|
||||
private struct PendingMessage
|
||||
{
|
||||
public string title;
|
||||
public string content;
|
||||
public MessageBox customPrefab; // 支持未来传入不同种类的 Prefab
|
||||
}
|
||||
|
||||
private Queue<PendingMessage> _messageQueue = new Queue<PendingMessage>();
|
||||
private MessageBox _currentActiveBox;
|
||||
private bool _isPageActive = false;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[StoryMessageBoxUIPage] 场景中存在多个实例,正在销毁多余的实例。");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageContainer == null)
|
||||
{
|
||||
messageContainer = this.transform;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将歌曲解锁提示加入弹窗队列。
|
||||
/// </summary>
|
||||
public void ShowUnlockMessage(string songUnlockKey)
|
||||
{
|
||||
string title = "New Song Unlocked!";
|
||||
string content = $"You have unlocked the song: {songUnlockKey}!";
|
||||
EnqueueMessage(title, content, defaultMessageBoxPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将自定义文本提示加入弹窗队列。
|
||||
/// </summary>
|
||||
public void ShowCustomMessage(string title, string content)
|
||||
{
|
||||
EnqueueMessage(title, content, defaultMessageBoxPrefab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将指定消息入队。如果是空闲状态,则唤醒大页面并开始处理队列。
|
||||
/// </summary>
|
||||
private void EnqueueMessage(string title, string content, MessageBox prefab)
|
||||
{
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("[StoryMessageBoxUIPage] 没有可用的 MessageBox 预制体!");
|
||||
return;
|
||||
}
|
||||
|
||||
_messageQueue.Enqueue(new PendingMessage
|
||||
{
|
||||
title = title,
|
||||
content = content,
|
||||
customPrefab = prefab
|
||||
});
|
||||
|
||||
// 如果当前页面没有在显示中,则开始整体流程
|
||||
if (!_isPageActive)
|
||||
{
|
||||
_isPageActive = true;
|
||||
|
||||
// 唤醒大页面(执行 UIPageBase 的渐入并拦截底层射线)
|
||||
this.FadeIn(0.5f, false, ShowNextInQueue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从队列中取出一个并实例化显示。
|
||||
/// </summary>
|
||||
private void ShowNextInQueue()
|
||||
{
|
||||
if (_messageQueue.Count == 0)
|
||||
{
|
||||
// 队列处理完毕,大页面整体退场
|
||||
_currentActiveBox = null;
|
||||
_isPageActive = false;
|
||||
this.FadeOut();
|
||||
return;
|
||||
}
|
||||
|
||||
PendingMessage msg = _messageQueue.Dequeue();
|
||||
|
||||
// 实例化预制体
|
||||
_currentActiveBox = Instantiate(msg.customPrefab, messageContainer);
|
||||
|
||||
// 订阅“当这个消息框彻底结束关闭时”的事件
|
||||
_currentActiveBox.onAllMessagesClosed.AddListener(() =>
|
||||
{
|
||||
// 销毁旧实例
|
||||
if (_currentActiveBox != null)
|
||||
{
|
||||
Destroy(_currentActiveBox.gameObject);
|
||||
}
|
||||
|
||||
// 检查并显示下一个
|
||||
ShowNextInQueue();
|
||||
});
|
||||
|
||||
// 配置并启动 MessageBox
|
||||
_currentActiveBox.Clear();
|
||||
_currentActiveBox.AddInfo(msg.title, msg.content, null);
|
||||
|
||||
// MessageBox.SetUp() 内部会激活自己并执行自己的 FadeIn
|
||||
_currentActiveBox.gameObject.SetActive(true);
|
||||
_currentActiveBox.SetUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Menu/MessagePage/MessageUIPage.cs.meta
Normal file
2
Assets/Scripts/Menu/MessagePage/MessageUIPage.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07f2a7ffd4b513d4e90d60a6587914cc
|
||||
@@ -9,6 +9,35 @@ namespace Ichni.Menu
|
||||
[CreateAssetMenu(fileName = "TutorialCollection", menuName = "Ichni/TutorialCollection")]
|
||||
public class TutorialCollection : SerializedScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 教程 Key 到教程曲目配置的映射。
|
||||
/// Key 必须使用小写英文、数字和下划线,例如 <c>chapter0_intro</c>;
|
||||
/// 不要使用章节展示名、曲名或点号作为 Key。
|
||||
/// </summary>
|
||||
public Dictionary<string, SongItemData> songs = new Dictionary<string, SongItemData>();
|
||||
|
||||
/// <summary>
|
||||
/// 按 TutorialBlock 配置的稳定 Key 查找教程曲目。
|
||||
/// 教程运行流程只通过此函数读取集合,避免各处直接访问字典而产生空引用或不一致的错误信息。
|
||||
/// </summary>
|
||||
public bool TryGetTutorialSong(string tutorialKey, out SongItemData song)
|
||||
{
|
||||
song = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tutorialKey))
|
||||
{
|
||||
Debug.LogWarning("[TutorialCollection] Tutorial Key 为空,无法查找教程曲目。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (songs == null || !songs.TryGetValue(tutorialKey, out song) || song == null)
|
||||
{
|
||||
Debug.LogWarning($"[TutorialCollection] 未找到 Tutorial Key '{tutorialKey}' 对应的教程曲目。");
|
||||
song = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user