176 lines
7.0 KiB
C#
176 lines
7.0 KiB
C#
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using DG.Tweening;
|
||
|
||
namespace Ichni.Story.Dialogue
|
||
{
|
||
/// <summary>
|
||
/// 基于 <see cref="UnityEngine.UI.Image"/> 的 Sprite 立绘渲染器。
|
||
/// <para>
|
||
/// 挂载在 VN 对话 UI 内的立绘槽位 GameObject 上(Left / Center / Right 等),
|
||
/// 每个槽位对应一个 <see cref="SpritePortraitRenderer"/> 实例。
|
||
/// 当前阶段仅实现静态显示(Show / Hide / SetEmotion / SetPosition / SetActiveVisual),
|
||
/// 后续阶段再叠加 DOTween 动画。
|
||
/// </para>
|
||
/// </summary>
|
||
[RequireComponent(typeof(Image))]
|
||
public class SpritePortraitRenderer : MonoBehaviour, IPortraitRenderer
|
||
{
|
||
// ── 设置 ────────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>说话者的不透明度(满值)。</summary>
|
||
[SerializeField] private float _alphaActive = 1.0f;
|
||
|
||
/// <summary>非说话者的压暗不透明度。</summary>
|
||
[SerializeField] private float _alphaInactive = 0.7f;
|
||
|
||
// ── 组件缓存 ─────────────────────────────────────────────────────────────
|
||
|
||
private Image _image;
|
||
private RectTransform _rectTransform;
|
||
|
||
// ── 状态 ─────────────────────────────────────────────────────────────────
|
||
|
||
private CharacterData _currentCharacter;
|
||
|
||
// ── IPortraitRenderer 实现 ───────────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public RectTransform RectTransform => _rectTransform;
|
||
|
||
// ── 生命周期 ──────────────────────────────────────────────────────────────
|
||
|
||
private void Awake()
|
||
{
|
||
_image = GetComponent<Image>();
|
||
_rectTransform = GetComponent<RectTransform>();
|
||
|
||
// 初始隐藏
|
||
SetGroupAlpha(0f);
|
||
}
|
||
|
||
// ── 接口方法 ─────────────────────────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public void Show(CharacterData character, string emotion)
|
||
{
|
||
if (character == null)
|
||
{
|
||
Debug.LogWarning("[SpritePortraitRenderer] Show 传入了 null CharacterData。");
|
||
return;
|
||
}
|
||
|
||
_currentCharacter = character;
|
||
ApplySprite(emotion);
|
||
SetGroupAlpha(_alphaActive);
|
||
gameObject.SetActive(true);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void Hide()
|
||
{
|
||
SetGroupAlpha(0f);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SetEmotion(string emotion)
|
||
{
|
||
if (_currentCharacter == null)
|
||
{
|
||
Debug.LogWarning("[SpritePortraitRenderer] SetEmotion 时尚未绑定角色数据。");
|
||
return;
|
||
}
|
||
|
||
ApplySprite(emotion);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SetPosition(Vector2 anchoredPos)
|
||
{
|
||
_rectTransform.anchoredPosition = anchoredPos;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SetActiveVisual(bool isSpeaking)
|
||
{
|
||
SetGroupAlpha(isSpeaking ? _alphaActive : _alphaInactive);
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void MoveTo(Vector2 anchoredPos, float duration)
|
||
{
|
||
_rectTransform.DOKill(complete: true);
|
||
_rectTransform.DOAnchorPos(anchoredPos, duration).SetEase(Ease.InOutQuad).Play();
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void Jump(float power, int numJumps, float duration)
|
||
{
|
||
// 对于跳跃,先停止并完成先前的动画,避免错乱(特别是被快进时)
|
||
_rectTransform.DOKill(complete: true);
|
||
|
||
// UI 的 DOJumpAnchorPos 非常适合这种表现,项目中要求手动调用 Play()
|
||
_rectTransform.DOJumpAnchorPos(_rectTransform.anchoredPosition, power, numJumps, duration).Play();
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void Shake(float duration, float strength, int vibrato)
|
||
{
|
||
_rectTransform.DOKill(complete: true);
|
||
|
||
// 左右震动(限制在 X 轴或以特定强度震动),项目中要求手动调用 Play()
|
||
_rectTransform.DOShakeAnchorPos(duration, new Vector2(strength, 0f), vibrato, 90, false, true).Play();
|
||
}
|
||
|
||
// ── 内部辅助 ─────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 从 <see cref="CharacterData.emotionSprites"/> 中查找并赋值 Sprite。
|
||
/// 找不到指定情绪时回退到"normal",再找不到则使用字典中第一个条目,
|
||
/// 并在 Console 输出警告。
|
||
/// </summary>
|
||
private void ApplySprite(string emotion)
|
||
{
|
||
if (_currentCharacter.emotionSprites == null || _currentCharacter.emotionSprites.Count == 0)
|
||
{
|
||
Debug.LogWarning($"[SpritePortraitRenderer] 角色 '{_currentCharacter.characterId}' 没有配置任何情绪 Sprite。");
|
||
return;
|
||
}
|
||
|
||
// 1. 精确匹配
|
||
if (!string.IsNullOrEmpty(emotion) &&
|
||
_currentCharacter.emotionSprites.TryGetValue(emotion, out Sprite target))
|
||
{
|
||
_image.sprite = target;
|
||
return;
|
||
}
|
||
|
||
// 2. 回退 normal
|
||
if (_currentCharacter.emotionSprites.TryGetValue("normal", out Sprite fallback))
|
||
{
|
||
Debug.LogWarning($"[SpritePortraitRenderer] 角色 '{_currentCharacter.characterId}' 没有情绪 '{emotion}',回退到 'normal'。");
|
||
_image.sprite = fallback;
|
||
return;
|
||
}
|
||
|
||
// 3. 使用第一个条目
|
||
foreach (Sprite first in _currentCharacter.emotionSprites.Values)
|
||
{
|
||
Debug.LogWarning($"[SpritePortraitRenderer] 角色 '{_currentCharacter.characterId}' 也没有 'normal',使用第一张 Sprite。");
|
||
_image.sprite = first;
|
||
return;
|
||
}
|
||
}
|
||
|
||
private void SetGroupAlpha(float alpha)
|
||
{
|
||
if (_image != null)
|
||
{
|
||
Color c = _image.color;
|
||
c.a = alpha;
|
||
_image.color = c;
|
||
}
|
||
}
|
||
}
|
||
}
|