using UnityEngine; using UnityEngine.UI; using DG.Tweening; namespace Ichni.Story.Dialogue { /// /// 基于 的 Sprite 立绘渲染器。 /// /// 挂载在 VN 对话 UI 内的立绘槽位 GameObject 上(Left / Center / Right 等), /// 每个槽位对应一个 实例。 /// 当前阶段仅实现静态显示(Show / Hide / SetEmotion / SetPosition / SetActiveVisual), /// 后续阶段再叠加 DOTween 动画。 /// /// [RequireComponent(typeof(Image))] public class SpritePortraitRenderer : MonoBehaviour, IPortraitRenderer { // ── 设置 ──────────────────────────────────────────────────────────────── /// 说话者的不透明度(满值)。 [SerializeField] private float _alphaActive = 1.0f; /// 非说话者的压暗不透明度。 [SerializeField] private float _alphaInactive = 0.7f; // ── 组件缓存 ───────────────────────────────────────────────────────────── private Image _image; private RectTransform _rectTransform; // ── 状态 ───────────────────────────────────────────────────────────────── private CharacterData _currentCharacter; // ── IPortraitRenderer 实现 ─────────────────────────────────────────────── /// public RectTransform RectTransform => _rectTransform; // ── 生命周期 ────────────────────────────────────────────────────────────── private void Awake() { _image = GetComponent(); _rectTransform = GetComponent(); // 初始隐藏 SetGroupAlpha(0f); } // ── 接口方法 ───────────────────────────────────────────────────────────── /// 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); } /// public void Hide() { SetGroupAlpha(0f); } /// public void SetEmotion(string emotion) { if (_currentCharacter == null) { Debug.LogWarning("[SpritePortraitRenderer] SetEmotion 时尚未绑定角色数据。"); return; } ApplySprite(emotion); } /// public void SetPosition(Vector2 anchoredPos) { _rectTransform.anchoredPosition = anchoredPos; } /// public void SetActiveVisual(bool isSpeaking) { SetGroupAlpha(isSpeaking ? _alphaActive : _alphaInactive); } /// public void MoveTo(Vector2 anchoredPos, float duration) { _rectTransform.DOKill(complete: true); _rectTransform.DOAnchorPos(anchoredPos, duration).SetEase(Ease.InOutQuad).Play(); } /// public void Jump(float power, int numJumps, float duration) { // 对于跳跃,先停止并完成先前的动画,避免错乱(特别是被快进时) _rectTransform.DOKill(complete: true); // UI 的 DOJumpAnchorPos 非常适合这种表现,项目中要求手动调用 Play() _rectTransform.DOJumpAnchorPos(_rectTransform.anchoredPosition, power, numJumps, duration).Play(); } /// 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(); } // ── 内部辅助 ───────────────────────────────────────────────────────────── /// /// 从 中查找并赋值 Sprite。 /// 找不到指定情绪时回退到"normal",再找不到则使用字典中第一个条目, /// 并在 Console 输出警告。 /// 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; } } } }