feat(dialog): 实现选项已探索追踪与视觉反馈

This commit is contained in:
2026-05-26 15:49:05 +08:00
parent 0794c5751d
commit 570961251c
7 changed files with 378 additions and 33 deletions
+8 -6
View File
@@ -143,15 +143,17 @@ MonoBehaviour:
text: {fileID: 6343862642438606438}
button: {fileID: 3624986506224747444}
arrow: {fileID: 2138736795402918014}
box: {fileID: 6027689100706873528}
colorBlock:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.4, g: 0.4, b: 0.4, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_NormalColor: {r: 0, g: 0, b: 0, a: 1}
m_HighlightedColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 1}
m_PressedColor: {r: 0.039215688, g: 0.039215688, b: 0.039215688, a: 1}
m_SelectedColor: {r: 0, g: 0, b: 0, a: 1}
m_DisabledColor: {r: 0.039215688, g: 0.039215688, b: 0.039215688, a: 0.49019608}
m_ColorMultiplier: 0
m_FadeDuration: 0
selectedColor: {r: 0.5471698, g: 0.5471698, b: 0.5471698, a: 1}
exploredNormalColor: {r: 0.43137255, g: 0.43137255, b: 0.43137255, a: 1}
normalColor: {r: 1, g: 1, b: 1, a: 1}
--- !u!114 &9163890225660695161
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using AibisDream.Kit;
using UnityEngine;
using UnityEngine.Events;
@@ -11,6 +12,8 @@ namespace AibisDream
{
private DialogueRunner _dialogueRunner;
public DialogueRunner DialogueRunner => _dialogueRunner;
[SerializeField] private LineAdvanceInput lineAdvanceInput;
private void Start()
@@ -132,6 +135,37 @@ namespace AibisDream
#endregion
/// <summary>
/// 返回当前正在执行的节点名与节点 tags;尚未启动 / 异常时返回 (null, 空数组)。
/// </summary>
public (string nodeName, IReadOnlyList<string> tags) GetCurrentNodeContext()
{
if (_dialogueRunner == null)
{
return (null, Array.Empty<string>());
}
var dialogue = _dialogueRunner.Dialogue;
if (dialogue == null)
{
return (null, Array.Empty<string>());
}
var nodeName = dialogue.CurrentNode;
if (string.IsNullOrEmpty(nodeName))
{
return (null, Array.Empty<string>());
}
var tagsHeader = dialogue.GetHeaderValue(nodeName, "tags");
if (string.IsNullOrWhiteSpace(tagsHeader))
{
return (nodeName, Array.Empty<string>());
}
return (nodeName, tagsHeader.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries));
}
#region
public BindProperty<AdvanceMode> advanceMode = new(new AdvanceMode());
@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Framework;
namespace AibisDream
{
/// <summary>
/// 选项已探索状态追踪(内存态,由 OptionView 私有持有)。
/// </summary>
public class OptionExploreTracker
{
private sealed class OptionRecord
{
public bool explored;
public string[] deps = Array.Empty<string>();
}
private static readonly string[] EmptyDeps = Array.Empty<string>();
private readonly Dictionary<string, OptionRecord> _records = new();
private readonly Dictionary<string, HashSet<string>> _varToOptions = new();
private string _currentExecutingLineId;
public void Bind()
{
EnumEventSystem.Global.Register<StorageEvent, VariableItem>(StorageEvent.VariableSet, OnVariableSet);
EnumEventSystem.Global.Register<DialogEventEnum, LineInfo>(DialogEventEnum.OptionSelected, OnOptionSelected);
EnumEventSystem.Global.Register(DialogEventEnum.OptionShow, OnOptionShow);
EnumEventSystem.Global.Register(GameLoopEnum.GameStart, Clear);
EnumEventSystem.Global.Register(GameLoopEnum.GameQuit, Clear);
EnumEventSystem.Global.Register(EventEnum.NextYarn, Clear);
}
public void Unbind()
{
EnumEventSystem.Global.UnRegister<StorageEvent, VariableItem>(StorageEvent.VariableSet, OnVariableSet);
EnumEventSystem.Global.UnRegister<DialogEventEnum, LineInfo>(DialogEventEnum.OptionSelected, OnOptionSelected);
EnumEventSystem.Global.UnRegister(DialogEventEnum.OptionShow, OnOptionShow);
EnumEventSystem.Global.UnRegister(GameLoopEnum.GameStart, Clear);
EnumEventSystem.Global.UnRegister(GameLoopEnum.GameQuit, Clear);
EnumEventSystem.Global.UnRegister(EventEnum.NextYarn, Clear);
}
public (bool isTrackable, bool isExplored) Evaluate(
string lineId,
IReadOnlyList<string> metadata,
string currentNodeName,
IReadOnlyList<string> nodeTags)
{
if (string.IsNullOrEmpty(lineId))
{
return (false, false);
}
ParseMetadata(metadata, out var forceTrack, out var forceNoTrack, out var deps);
var isTrackable = ResolveTrackable(forceTrack, forceNoTrack, deps, nodeTags);
if (!isTrackable)
{
return (false, false);
}
if (!_records.TryGetValue(lineId, out var rec))
{
rec = new OptionRecord { explored = false, deps = deps };
_records[lineId] = rec;
RegisterVarIndex(lineId, deps);
}
else
{
UpdateDeps(lineId, rec, deps);
}
return (true, rec.explored);
}
private void OnOptionSelected(LineInfo info)
{
if (!info.isOption || string.IsNullOrEmpty(info.lineId))
{
return;
}
if (_records.TryGetValue(info.lineId, out var rec))
{
rec.explored = true;
_currentExecutingLineId = info.lineId;
}
}
private void OnVariableSet(VariableItem item)
{
if (item.variableName == null || !_varToOptions.TryGetValue(item.variableName, out var lineIds))
{
return;
}
foreach (var lineId in lineIds)
{
if (lineId == _currentExecutingLineId)
{
continue;
}
if (_records.TryGetValue(lineId, out var rec))
{
rec.explored = false;
}
}
}
private void OnOptionShow()
{
_currentExecutingLineId = null;
}
private void Clear()
{
_records.Clear();
_varToOptions.Clear();
_currentExecutingLineId = null;
}
private static void ParseMetadata(
IReadOnlyList<string> metadata,
out bool forceTrack,
out bool forceNoTrack,
out string[] deps)
{
forceTrack = false;
forceNoTrack = false;
deps = EmptyDeps;
if (metadata == null)
{
return;
}
for (var i = 0; i < metadata.Count; i++)
{
var tag = metadata[i];
if (string.IsNullOrEmpty(tag))
{
continue;
}
if (tag == "track")
{
forceTrack = true;
}
else if (tag == "notrack")
{
forceNoTrack = true;
}
else if (tag.StartsWith("deps:", StringComparison.Ordinal))
{
deps = tag.Substring(5).Split(',')
.Select(v => v.Trim())
.Where(v => v.Length > 0)
.Select(NormalizeVariableName)
.ToArray();
}
}
}
private static string NormalizeVariableName(string name)
{
return name.StartsWith("$") ? name : $"${name}";
}
private static bool ResolveTrackable(
bool forceTrack,
bool forceNoTrack,
string[] deps,
IReadOnlyList<string> nodeTags)
{
if (forceNoTrack)
{
return false;
}
if (forceTrack || deps.Length > 0)
{
return true;
}
if (nodeTags == null)
{
return false;
}
for (var i = 0; i < nodeTags.Count; i++)
{
if (nodeTags[i] == "hub" || nodeTags[i] == "detour")
{
return true;
}
}
return false;
}
private void RegisterVarIndex(string lineId, string[] deps)
{
foreach (var dep in deps)
{
if (!_varToOptions.TryGetValue(dep, out var lineIds))
{
lineIds = new HashSet<string>();
_varToOptions[dep] = lineIds;
}
lineIds.Add(lineId);
}
}
private void UpdateDeps(string lineId, OptionRecord rec, string[] deps)
{
if (ReferenceEquals(rec.deps, deps) || (rec.deps.Length == deps.Length && rec.deps.SequenceEqual(deps)))
{
return;
}
foreach (var oldDep in rec.deps)
{
if (_varToOptions.TryGetValue(oldDep, out var oldSet))
{
oldSet.Remove(lineId);
}
}
rec.deps = deps;
RegisterVarIndex(lineId, deps);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7c3e91f4b2d48c6a1f9056d8e3b7c42
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+29 -5
View File
@@ -1,5 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AibisDream.Framework;
using UnityEngine;
@@ -14,6 +16,20 @@ namespace AibisDream
[SerializeField] private OnelineOptions onelineOptions;
[SerializeField] private DialogViewType defaultViewType = DialogViewType.StandardBubble;
private OptionExploreTracker? _tracker;
private void Awake()
{
_tracker = new OptionExploreTracker();
_tracker.Bind();
}
private void OnDestroy()
{
_tracker?.Unbind();
_tracker = null;
}
public override YarnTask OnDialogueStartedAsync()
{
return YarnTask.CompletedTask;
@@ -29,6 +45,7 @@ namespace AibisDream
{
return YarnTask.CompletedTask;
}
public void LoadBubbles(BubbleSlotGroupData data)
{
defaultViewType = data.dialogViewType;
@@ -38,10 +55,17 @@ namespace AibisDream
{
EnumEventSystem.Global.Send(DialogEventEnum.OptionShow);
var optionSelectedSource = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
var options = DialogOption.GeneOptions(dialogueOptions, optionId =>
{
optionSelectedSource.TrySetResult(optionId);
});
var (nodeName, tags) = DialogController.Instance != null
? DialogController.Instance.GetCurrentNodeContext()
: (null, (IReadOnlyList<string>)Array.Empty<string>());
var options = DialogOption.GeneOptions(
dialogueOptions,
_tracker,
nodeName,
tags,
optionId => optionSelectedSource.TrySetResult(optionId));
var viewType = GetCurrentViewType(options);
@@ -105,4 +129,4 @@ namespace AibisDream
centerOptions.HideOptions();
}
}
}
}
@@ -1,6 +1,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using AibisDream.Framework;
using AibisDream.Kit;
using AibisDream.Utility;
@@ -15,6 +15,9 @@ namespace AibisDream
private DialogueOption _option;
public bool IsAvailable => _option.IsAvailable;
public bool IsTrackable { get; private set; }
public bool IsExplored { get; private set; }
public string LineId => _option.TextID;
public string Line => _option.Line.TextWithoutCharacterName.Text;
public CharacterVo Character => _option.Line.GetCharacterVo();
@@ -45,13 +48,34 @@ namespace AibisDream
};
}
public static DialogOption[] GeneOptions(DialogueOption[] dialogueOptions, Action<int> onOptionSelected)
public static DialogOption[] GeneOptions(
DialogueOption[] dialogueOptions,
OptionExploreTracker tracker,
string nodeName,
IReadOnlyList<string> nodeTags,
Action<int> onOptionSelected)
{
return dialogueOptions
.Select(item => new DialogOption { _option = item, _onOptionSelected = onOptionSelected }).ToArray();
var tags = nodeTags ?? Array.Empty<string>();
return dialogueOptions.Select(item =>
{
var lineId = item.TextID;
var metadata = item.Line.Metadata ?? Array.Empty<string>();
var (isTrackable, isExplored) = tracker != null
? tracker.Evaluate(lineId, metadata, nodeName, tags)
: (false, false);
return new DialogOption
{
_option = item,
_onOptionSelected = onOptionSelected,
IsTrackable = isTrackable,
IsExplored = isExplored
};
}).ToArray();
}
}
[Serializable]
public struct TextParam
{
@@ -59,4 +83,4 @@ namespace AibisDream
public float lineSpace;
public int maxCharNum;
}
}
}
@@ -11,12 +11,15 @@ namespace AibisDream
[SerializeField] private TMP_Text text;
[SerializeField] private BaseButton button;
[SerializeField] private Image arrow;
[SerializeField] private Image box;
[Header("颜色配置")]
[SerializeField] private ColorBlock colorBlock;
[Header("已选择颜色")]
[SerializeField] private Color selectedColor;
[Header("文字颜色")]
[SerializeField] private Color exploredNormalColor = new(0.45f, 0.45f, 0.45f, 1f);
[SerializeField] private Color normalColor = Color.white;
private DialogOption _option;
public void Init()
@@ -30,18 +33,20 @@ namespace AibisDream
if (!option.IsAvailable)
{
// 不可用,设置为不可选
button.interactable = false;
text.SetText($"<s>{option.Line}</s>");
return;
}
button.interactable = true;
text.SetText(option.Line);
button.onClick.AddListener(() =>
{
option.Select();
hide?.Invoke();
});
OnStateTransition(SelectionType.Normal, true);
}
private void OnStateTransition(SelectionType state, bool instant)
@@ -49,37 +54,46 @@ namespace AibisDream
switch (state)
{
case SelectionType.Normal:
arrow.SetAlpha(0f);
SetColor(colorBlock.normalColor);
// arrow.SetAlpha(0f);
SetBoxColor(colorBlock.normalColor);
break;
case SelectionType.Highlighted:
arrow.SetAlpha(1f);
SetColor(colorBlock.highlightedColor);
// arrow.SetAlpha(1f);
SetBoxColor(colorBlock.highlightedColor);
break;
case SelectionType.Pressed:
arrow.SetAlpha(1f);
SetColor(colorBlock.pressedColor);
// arrow.SetAlpha(1f);
SetBoxColor(colorBlock.pressedColor);
break;
case SelectionType.Selected:
arrow.SetAlpha(1f);
SetColor(colorBlock.selectedColor);
// arrow.SetAlpha(1f);
SetBoxColor(colorBlock.selectedColor);
break;
case SelectionType.Disabled:
arrow.SetAlpha(0f);
SetColor(colorBlock.disabledColor);
// arrow.SetAlpha(0f);
SetBoxColor(colorBlock.disabledColor);
break;
}
SetTextColor(GetNormalColor());
}
private void SetBoxColor(Color color)
{
box.color = color;
}
private Color GetNormalColor()
{
return colorBlock.normalColor;
return _option.IsTrackable && _option.IsExplored
? exploredNormalColor
: normalColor;
}
private void SetColor(Color color)
private void SetTextColor(Color color)
{
arrow.color = color;
text.color = color;
}
}
}
}