剧情+对话完善
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前阶段使用 <see cref="Image"/> 显示静态 Helper 插画的 Presenter。
|
||||
/// <para>若 StoryHelperData 未配置 portraitSprite,则保留场景中已有的 Sprite 作为回退,
|
||||
/// 方便在视觉资源尚未完全迁移时继续工作。</para>
|
||||
/// </summary>
|
||||
public class StaticStoryHelperVisualPresenter : StoryHelperVisualPresenter
|
||||
{
|
||||
[SerializeField, Tooltip("用于显示当前 Helper 静态立绘的 Image。")]
|
||||
private Image portraitImage;
|
||||
|
||||
public override void ApplyHelper(StoryHelperData helperData)
|
||||
{
|
||||
if (portraitImage == null || helperData == null || helperData.portraitSprite == null)
|
||||
return;
|
||||
|
||||
// 不调用 SetNativeSize:Helper 的实际 UI 尺寸由场景布局统一控制。
|
||||
portraitImage.sprite = helperData.portraitSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c31f8bd5e6ca44399645c4f7cb21e0ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
386
Assets/Scripts/NewStorySystem/Helper/StoryHelperController.cs
Normal file
386
Assets/Scripts/NewStorySystem/Helper/StoryHelperController.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization.Settings;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景中常驻 Helper 的交互控制器。
|
||||
/// <para>点击后会按剧情条件筛选并加权随机抽取一条台词,实例化 HelperTalk Prefab 显示;
|
||||
/// 此过程不修改剧情变量、Block 完成状态或存档。</para>
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class StoryHelperController : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField, Tooltip("承接 Helper 点击的 Button。建议挂在固定根节点,而非会移动的视觉节点上。")]
|
||||
private Button helperButton;
|
||||
|
||||
[SerializeField, Tooltip("当前 Helper 的视觉 Presenter。未来可替换为 Spine 或 Live2D 对应实现。")]
|
||||
private StoryHelperVisualPresenter visualPresenter;
|
||||
|
||||
[SerializeField, Tooltip("点击后实例化的 HelperTalk Prefab。它独立于 MessageBox。")]
|
||||
private StoryHelperTalk helperTalkPrefab;
|
||||
|
||||
[SerializeField, Tooltip("运行时 HelperTalk 的父节点。留空时使用当前 Helper 根节点。")]
|
||||
private Transform helperTalkContainer;
|
||||
|
||||
[Header("Motion Roots")]
|
||||
[SerializeField, Tooltip("只承担循环漂浮动画的节点。")]
|
||||
private RectTransform idleRoot;
|
||||
|
||||
[SerializeField, Tooltip("只承担点击反馈的节点。它应位于 Idle Root 内部,避免与循环漂浮争夺同一属性。")]
|
||||
private RectTransform clickRoot;
|
||||
|
||||
[Header("Idle Animation")]
|
||||
[SerializeField, Min(0f), Tooltip("循环漂浮的最大纵向偏移(Canvas 单位)。")]
|
||||
private float idleFloatDistance = 8f;
|
||||
|
||||
[SerializeField, Min(0.05f), Tooltip("从基准位置移动到最高点或最低点的单程时间。")]
|
||||
private float idleHalfDuration = 1.35f;
|
||||
|
||||
[Header("Click Animation")]
|
||||
[SerializeField, Min(0.01f), Tooltip("点击缩放与位移反馈持续时间。")]
|
||||
private float clickPunchDuration = 0.2f;
|
||||
|
||||
[SerializeField, Min(0f), Tooltip("点击时的缩放强度;0.035 表示约 3.5% 的轻微放大。")]
|
||||
private float clickPunchScale = 0.035f;
|
||||
|
||||
[SerializeField, Min(0f), Tooltip("点击时的纵向位移反馈(Canvas 单位)。")]
|
||||
private float clickPunchOffset = 5f;
|
||||
|
||||
[SerializeField, Min(0f), Tooltip("连续点击的最小间隔,防止重复实例化大量气泡。")]
|
||||
private float clickCooldown = 0.3f;
|
||||
|
||||
private readonly List<StoryHelperDialogueDefinition> _eligibleDialogues = new();
|
||||
|
||||
private Sequence _idleSequence;
|
||||
private Sequence _clickSequence;
|
||||
private StoryHelperTalk _activeTalk;
|
||||
private Vector2 _idleBasePosition;
|
||||
private Vector2 _clickBasePosition;
|
||||
private Vector3 _clickBaseScale;
|
||||
private string _lastDialogueId;
|
||||
private StoryHelperData _appliedHelperData;
|
||||
private StoryManager _subscribedStoryManager;
|
||||
private float _nextClickTime;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (helperButton == null)
|
||||
helperButton = GetComponent<Button>();
|
||||
if (idleRoot == null)
|
||||
idleRoot = transform as RectTransform;
|
||||
if (clickRoot == null && visualPresenter != null)
|
||||
clickRoot = visualPresenter.transform as RectTransform;
|
||||
if (helperTalkContainer == null)
|
||||
helperTalkContainer = transform;
|
||||
|
||||
CacheBaseTransforms();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (helperButton != null)
|
||||
helperButton.onClick.AddListener(HandleHelperClicked);
|
||||
|
||||
SubscribeToStoryManager();
|
||||
|
||||
PlayIdleAnimation();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// OnEnable 的调用顺序可能早于 StoryManager.Awake;Start 时再次同步,
|
||||
// 确保首次进入 MenuScene 也能应用 Inspector 中配置的默认 Helper。
|
||||
SubscribeToStoryManager();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (helperButton != null)
|
||||
helperButton.onClick.RemoveListener(HandleHelperClicked);
|
||||
UnsubscribeFromStoryManager();
|
||||
|
||||
KillAndRestoreMotion();
|
||||
CloseActiveTalk();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_idleSequence?.Kill();
|
||||
_clickSequence?.Kill();
|
||||
}
|
||||
|
||||
/// <summary>点击 Helper 的统一入口:播放反馈后创建一条新的专属气泡。</summary>
|
||||
private void HandleHelperClicked()
|
||||
{
|
||||
if (Time.unscaledTime < _nextClickTime)
|
||||
return;
|
||||
|
||||
_nextClickTime = Time.unscaledTime + clickCooldown;
|
||||
PlayClickAnimation();
|
||||
TryCreateRandomTalk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前 Helper 对话池按条件和权重抽取文本,并实例化一个新的 HelperTalk。
|
||||
/// </summary>
|
||||
private void TryCreateRandomTalk()
|
||||
{
|
||||
StoryHelperData helperData = StoryManager.instance != null
|
||||
? StoryManager.instance.ActiveHelperData
|
||||
: null;
|
||||
if (helperData == null)
|
||||
{
|
||||
Debug.LogWarning("[StoryHelper] 未配置当前 StoryHelperData,无法创建 HelperTalk。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (helperTalkPrefab == null)
|
||||
{
|
||||
Debug.LogWarning("[StoryHelper] 未配置 HelperTalk Prefab,无法创建 Helper 对话气泡。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
StoryHelperDialogueDefinition dialogue = SelectDialogue(helperData);
|
||||
string contentKey = dialogue != null ? dialogue.dialogueKey : helperData.fallbackDialogueKey;
|
||||
if (string.IsNullOrEmpty(contentKey))
|
||||
{
|
||||
Debug.LogWarning($"[StoryHelper] Helper '{helperData.name}' 没有可用候选对话,也未配置 Fallback Dialogue Key。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dialogue != null)
|
||||
_lastDialogueId = GetDialogueIdentity(dialogue);
|
||||
|
||||
CloseActiveTalk();
|
||||
_activeTalk = Instantiate(helperTalkPrefab, helperTalkContainer);
|
||||
_activeTalk.Closed += HandleTalkClosed;
|
||||
_activeTalk.Show(
|
||||
GetLocalizedText(helperData, helperData.displayNameKey, helperData.name),
|
||||
GetLocalizedText(helperData, contentKey, contentKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建当前可用候选池;当存在其它候选时,排除与上一条相同的台词,避免连续重复。
|
||||
/// </summary>
|
||||
private StoryHelperDialogueDefinition SelectDialogue(StoryHelperData helperData)
|
||||
{
|
||||
_eligibleDialogues.Clear();
|
||||
if (helperData.dialoguePool == null)
|
||||
return null;
|
||||
|
||||
foreach (StoryHelperDialogueDefinition dialogue in helperData.dialoguePool)
|
||||
{
|
||||
if (dialogue == null || string.IsNullOrEmpty(dialogue.dialogueKey) || !IsDialogueAvailable(dialogue))
|
||||
continue;
|
||||
|
||||
_eligibleDialogues.Add(dialogue);
|
||||
}
|
||||
|
||||
if (_eligibleDialogues.Count == 0)
|
||||
return null;
|
||||
|
||||
bool hasAlternative = false;
|
||||
foreach (StoryHelperDialogueDefinition dialogue in _eligibleDialogues)
|
||||
{
|
||||
if (GetDialogueIdentity(dialogue) != _lastDialogueId)
|
||||
{
|
||||
hasAlternative = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int totalWeight = 0;
|
||||
foreach (StoryHelperDialogueDefinition dialogue in _eligibleDialogues)
|
||||
{
|
||||
if (hasAlternative && GetDialogueIdentity(dialogue) == _lastDialogueId)
|
||||
continue;
|
||||
|
||||
totalWeight += Mathf.Max(1, dialogue.weight);
|
||||
}
|
||||
|
||||
int randomWeight = UnityEngine.Random.Range(0, totalWeight);
|
||||
foreach (StoryHelperDialogueDefinition dialogue in _eligibleDialogues)
|
||||
{
|
||||
if (hasAlternative && GetDialogueIdentity(dialogue) == _lastDialogueId)
|
||||
continue;
|
||||
|
||||
randomWeight -= Mathf.Max(1, dialogue.weight);
|
||||
if (randomWeight < 0)
|
||||
return dialogue;
|
||||
}
|
||||
|
||||
return _eligibleDialogues[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复用 StoryCondition 的通用条件树。未配置条件代表始终可用;已配置条件必须在当前已构建章节中求值,
|
||||
/// 以避免全局 Helper 在没有章节上下文时误读其它章节的 Block 状态。
|
||||
/// </summary>
|
||||
private static bool IsDialogueAvailable(StoryHelperDialogueDefinition dialogue)
|
||||
{
|
||||
StoryCondition condition = dialogue.availabilityCondition;
|
||||
if (condition == null || !condition.IsConfigured)
|
||||
return true;
|
||||
|
||||
StoryTreeController treeController = StoryManager.instance != null
|
||||
? StoryManager.instance.treeController
|
||||
: null;
|
||||
if (treeController == null || treeController.ActiveStoryData == null)
|
||||
return false;
|
||||
|
||||
return condition.IsSatisfied((str) => StoryVariables.GetInt(str), treeController.IsBlockCompleted);
|
||||
}
|
||||
|
||||
private static string GetDialogueIdentity(StoryHelperDialogueDefinition dialogue)
|
||||
{
|
||||
return string.IsNullOrEmpty(dialogue.dialogueId) ? dialogue.dialogueKey : dialogue.dialogueId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建不影响点击节点的轻微循环漂浮。Idle 与 Click 使用不同节点,避免 DOTween 同时写入
|
||||
/// 同一个位置或缩放属性造成跳动。
|
||||
/// </summary>
|
||||
private void PlayIdleAnimation()
|
||||
{
|
||||
if (idleRoot == null || idleFloatDistance <= 0f)
|
||||
return;
|
||||
|
||||
_idleSequence?.Kill();
|
||||
idleRoot.anchoredPosition = _idleBasePosition;
|
||||
_idleSequence = DOTween.Sequence()
|
||||
.Pause()
|
||||
.SetUpdate(true)
|
||||
.Append(idleRoot.DOAnchorPosY(_idleBasePosition.y + idleFloatDistance, idleHalfDuration).SetEase(Ease.InOutSine))
|
||||
.Append(idleRoot.DOAnchorPosY(_idleBasePosition.y - idleFloatDistance, idleHalfDuration * 2f).SetEase(Ease.InOutSine))
|
||||
.Append(idleRoot.DOAnchorPosY(_idleBasePosition.y, idleHalfDuration).SetEase(Ease.InOutSine))
|
||||
.SetLoops(-1);
|
||||
_idleSequence.Play();
|
||||
}
|
||||
|
||||
/// <summary>播放一次克制的点击反馈,不会中断 Helper 的循环漂浮动画。</summary>
|
||||
private void PlayClickAnimation()
|
||||
{
|
||||
if (clickRoot == null)
|
||||
return;
|
||||
|
||||
_clickSequence?.Kill();
|
||||
clickRoot.anchoredPosition = _clickBasePosition;
|
||||
clickRoot.localScale = _clickBaseScale;
|
||||
_clickSequence = DOTween.Sequence()
|
||||
.Pause()
|
||||
.SetUpdate(true)
|
||||
.Join(clickRoot.DOPunchScale(Vector3.one * clickPunchScale, clickPunchDuration, 5, 0.7f))
|
||||
.Join(clickRoot.DOPunchAnchorPos(Vector2.up * clickPunchOffset, clickPunchDuration, 5, 0.7f));
|
||||
_clickSequence.Play();
|
||||
}
|
||||
|
||||
private void HandleActiveHelperChanged(StoryHelperData helperData)
|
||||
{
|
||||
_lastDialogueId = null;
|
||||
ApplyHelper(helperData);
|
||||
CloseActiveTalk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper 与 StoryManager 的 Awake 顺序不固定;OnEnable 与 Start 都调用本方法,
|
||||
/// 确保首次进入场景时既能应用默认 Helper,也能为未来的 Helper 切换保留可靠事件入口。
|
||||
/// </summary>
|
||||
private void SubscribeToStoryManager()
|
||||
{
|
||||
StoryManager manager = StoryManager.instance;
|
||||
if (manager == null || _subscribedStoryManager == manager)
|
||||
return;
|
||||
|
||||
UnsubscribeFromStoryManager();
|
||||
_subscribedStoryManager = manager;
|
||||
_subscribedStoryManager.ActiveHelperChanged += HandleActiveHelperChanged;
|
||||
ApplyHelper(_subscribedStoryManager.ActiveHelperData);
|
||||
}
|
||||
|
||||
private void UnsubscribeFromStoryManager()
|
||||
{
|
||||
if (_subscribedStoryManager == null)
|
||||
return;
|
||||
|
||||
_subscribedStoryManager.ActiveHelperChanged -= HandleActiveHelperChanged;
|
||||
_subscribedStoryManager = null;
|
||||
}
|
||||
|
||||
private void ApplyHelper(StoryHelperData helperData)
|
||||
{
|
||||
if (_appliedHelperData == helperData)
|
||||
return;
|
||||
|
||||
_appliedHelperData = helperData;
|
||||
visualPresenter?.ApplyHelper(helperData);
|
||||
}
|
||||
|
||||
/// <summary>本地化失败时显示 Key,确保测试阶段仍能定位缺失条目,而不是得到空白气泡。</summary>
|
||||
private string GetLocalizedText(StoryHelperData helperData, string key, string fallback)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return fallback;
|
||||
|
||||
try
|
||||
{
|
||||
string localized = LocalizationSettings.StringDatabase.GetLocalizedString(helperData.localizationTable, key);
|
||||
return string.IsNullOrEmpty(localized) ? fallback : localized;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.LogWarning($"[StoryHelper] 无法本地化 Key '{key}',将回退显示 '{fallback}'。{exception.Message}", this);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheBaseTransforms()
|
||||
{
|
||||
if (idleRoot != null)
|
||||
_idleBasePosition = idleRoot.anchoredPosition;
|
||||
if (clickRoot != null)
|
||||
{
|
||||
_clickBasePosition = clickRoot.anchoredPosition;
|
||||
_clickBaseScale = clickRoot.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void KillAndRestoreMotion()
|
||||
{
|
||||
_idleSequence?.Kill();
|
||||
_clickSequence?.Kill();
|
||||
_idleSequence = null;
|
||||
_clickSequence = null;
|
||||
|
||||
if (idleRoot != null)
|
||||
idleRoot.anchoredPosition = _idleBasePosition;
|
||||
if (clickRoot != null)
|
||||
{
|
||||
clickRoot.anchoredPosition = _clickBasePosition;
|
||||
clickRoot.localScale = _clickBaseScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseActiveTalk()
|
||||
{
|
||||
if (_activeTalk == null)
|
||||
return;
|
||||
|
||||
_activeTalk.Closed -= HandleTalkClosed;
|
||||
_activeTalk.CloseImmediately();
|
||||
_activeTalk = null;
|
||||
}
|
||||
|
||||
private void HandleTalkClosed(StoryHelperTalk talk)
|
||||
{
|
||||
talk.Closed -= HandleTalkClosed;
|
||||
if (_activeTalk == talk)
|
||||
_activeTalk = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e3e75d25eea4224a487c9de56d5a91c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
102
Assets/Scripts/NewStorySystem/Helper/StoryHelperTalk.cs
Normal file
102
Assets/Scripts/NewStorySystem/Helper/StoryHelperTalk.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using DG.Tweening;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// 由 <see cref="StoryHelperController"/> 按需实例化的 Helper 专属对话气泡。
|
||||
/// <para>每个实例只显示一条文本:淡入、停留、淡出后自动销毁,因此场景中不需要常驻气泡,
|
||||
/// 也不会与通用 MessageBox 队列产生耦合。</para>
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class StoryHelperTalk : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Tooltip("显示 Helper 名称的文本。")]
|
||||
private TMP_Text speakerText;
|
||||
|
||||
[SerializeField, Tooltip("显示随机 Helper 对话的文本。")]
|
||||
private TMP_Text contentText;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField, Min(0.01f), Tooltip("淡入与缩放入场时间。")]
|
||||
private float showDuration = 0.18f;
|
||||
|
||||
[SerializeField, Min(0f), Tooltip("气泡完整显示后的停留时间。")]
|
||||
private float visibleDuration = 4.5f;
|
||||
|
||||
[SerializeField, Min(0.01f), Tooltip("淡出与缩放退场时间。")]
|
||||
private float hideDuration = 0.16f;
|
||||
|
||||
[SerializeField, Range(0.5f, 1f), Tooltip("入场与退场时相对原始大小的缩放比例。")]
|
||||
private float initialScaleMultiplier = 0.92f;
|
||||
|
||||
/// <summary>气泡结束显示并准备销毁时触发,供创建者清理当前实例引用。</summary>
|
||||
public event Action<StoryHelperTalk> Closed;
|
||||
|
||||
private CanvasGroup _canvasGroup;
|
||||
private RectTransform _rectTransform;
|
||||
private Sequence _sequence;
|
||||
private Vector3 _baseScale;
|
||||
private bool _isClosed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
_rectTransform = transform as RectTransform;
|
||||
_baseScale = _rectTransform != null ? _rectTransform.localScale : Vector3.one;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入文本并播放完整的出现、停留与收回流程。
|
||||
/// 所有 DOTween Sequence 均先 Pause,再手动调用 Play,符合项目约定。
|
||||
/// </summary>
|
||||
public void Show(string speaker, string content)
|
||||
{
|
||||
if (speakerText == null || contentText == null || _rectTransform == null)
|
||||
{
|
||||
Debug.LogWarning("[StoryHelperTalk] Prefab 未配置 Speaker Text、Content Text 或 RectTransform,已取消显示。", this);
|
||||
CloseImmediately();
|
||||
return;
|
||||
}
|
||||
|
||||
speakerText.text = speaker;
|
||||
contentText.text = content;
|
||||
_canvasGroup.alpha = 0f;
|
||||
_rectTransform.localScale = _baseScale * initialScaleMultiplier;
|
||||
|
||||
_sequence?.Kill();
|
||||
_sequence = DOTween.Sequence()
|
||||
.Pause()
|
||||
.SetUpdate(true)
|
||||
.Append(_canvasGroup.DOFade(1f, showDuration))
|
||||
.Join(_rectTransform.DOScale(_baseScale, showDuration).SetEase(Ease.OutBack))
|
||||
.AppendInterval(visibleDuration)
|
||||
.Append(_canvasGroup.DOFade(0f, hideDuration))
|
||||
.Join(_rectTransform.DOScale(_baseScale * initialScaleMultiplier, hideDuration).SetEase(Ease.InQuad))
|
||||
.OnComplete(CloseImmediately);
|
||||
_sequence.Play();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即关闭并销毁该运行时实例。再次点击 Helper 时会调用它,使旧文本不会与新文本叠加。
|
||||
/// </summary>
|
||||
public void CloseImmediately()
|
||||
{
|
||||
if (_isClosed)
|
||||
return;
|
||||
|
||||
_isClosed = true;
|
||||
_sequence?.Kill();
|
||||
_sequence = null;
|
||||
Closed?.Invoke(this);
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_sequence?.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/NewStorySystem/Helper/StoryHelperTalk.cs.meta
Normal file
11
Assets/Scripts/NewStorySystem/Helper/StoryHelperTalk.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8bca20cc0f14b97944b95bdcf47d506
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Ichni.Story
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper 视觉表现的抽象接口。
|
||||
/// <para>StoryHelperController 不直接依赖 Image、Spine 或 Live2D。未来替换表现方式时,
|
||||
/// 只需实现本类并替换场景引用,点击、随机台词与气泡逻辑无需改变。</para>
|
||||
/// </summary>
|
||||
public abstract class StoryHelperVisualPresenter : MonoBehaviour
|
||||
{
|
||||
/// <summary>根据当前 Helper 数据刷新视觉资源。</summary>
|
||||
public abstract void ApplyHelper(StoryHelperData helperData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62eb3d8ec1f74ca5a981b254ecbc5187
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user