feat(dialog): 实现选项已探索追踪与视觉反馈
This commit is contained in:
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user