更新
This commit is contained in:
@@ -14,7 +14,6 @@ namespace Continentis.MainGame.Character
|
||||
public GameObject mainView;
|
||||
public Animator animator;
|
||||
public AnimatorPlus2D animatorPlus2D;
|
||||
public SerializableDictionary<string, AnimationClip> animations;
|
||||
|
||||
public Collider selector;
|
||||
|
||||
@@ -26,7 +25,16 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public List<SpriteRenderer> spriteRenderers;
|
||||
public List<Material> materials;
|
||||
|
||||
/// <summary>
|
||||
/// 当前使用的动画驱动器,由 Initialize 时自动检测。
|
||||
/// 外部通过此属性调用 PlayAction / ReturnToIdle 等方法。
|
||||
/// </summary>
|
||||
public ICharacterAnimator CharacterAnimator { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化角色视图:收集 SpriteRenderer / Material,自动检测并初始化动画驱动器。
|
||||
/// </summary>
|
||||
public void Initialize(CharacterBase character)
|
||||
{
|
||||
this.character = character;
|
||||
@@ -39,21 +47,18 @@ namespace Continentis.MainGame.Character
|
||||
}
|
||||
SetOutline(false);
|
||||
|
||||
animations = new SerializableDictionary<string, AnimationClip>();
|
||||
|
||||
foreach (KeyValuePair<string, AnimationClip> anim in character.data.animations)
|
||||
// 自动检测动画驱动器:优先级 Spine > Frame > Static
|
||||
CharacterAnimator = GetComponent<SpineAnimator>() as ICharacterAnimator
|
||||
?? GetComponent<FrameAnimator>() as ICharacterAnimator
|
||||
?? GetComponent<StaticSpriteAnimator>() as ICharacterAnimator;
|
||||
|
||||
if (CharacterAnimator != null)
|
||||
{
|
||||
animations.Add(anim.Key, anim.Value);
|
||||
}
|
||||
|
||||
if (animations.TryGetValue("Idle", out AnimationClip idle))
|
||||
{
|
||||
animatorPlus2D.defaultIdleClip = idle;
|
||||
animatorPlus2D.Initialize();
|
||||
CharacterAnimator.InitializeAnimator(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"No Idle animation found for character {character.data.displayName}");
|
||||
Debug.LogWarning($"[CharacterView] 角色 '{character.data.displayName}' 未挂载任何 ICharacterAnimator 实现,无动画驱动器。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
Assets/Scripts/MainGame/Character/CharacterView/FrameAnimator.cs
Normal file
108
Assets/Scripts/MainGame/Character/CharacterView/FrameAnimator.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using AnimatorPlus;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 2 动画驱动器:帧动画方案,基于 AnimatorPlus2D + AnimationClip。
|
||||
/// 通过 CharacterData.animations 映射表驱动 Sprite Sheet 帧动画。
|
||||
/// 适合有完整帧动画序列的像素风角色(如 PixelFantasy 系列)。
|
||||
/// </summary>
|
||||
public class FrameAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
private AnimatorPlus2D _animatorPlus;
|
||||
private SerializableDictionary<string, AnimationClip> _animations;
|
||||
private Coroutine _completionCoroutine;
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 初始化:从 CombatCharacterViewBase 获取 AnimatorPlus2D 引用和动画映射表。
|
||||
/// </summary>
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
_animatorPlus = view.animatorPlus2D;
|
||||
|
||||
// 从 CharacterData 拷贝动画映射
|
||||
_animations = new SerializableDictionary<string, AnimationClip>();
|
||||
foreach (KeyValuePair<string, AnimationClip> pair in view.character.data.animations)
|
||||
{
|
||||
_animations.Add(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
// 设置 Idle 并初始化 Playable Graph
|
||||
if (_animations.TryGetValue("Idle", out AnimationClip idle))
|
||||
{
|
||||
_animatorPlus.defaultIdleClip = idle;
|
||||
_animatorPlus.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[FrameAnimator] 角色 '{view.character.data.displayName}' 缺少 Idle 动画,无法初始化。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放指定动作的 AnimationClip,播完自动回 Idle 并触发回调。
|
||||
/// </summary>
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
StopCompletionCoroutine();
|
||||
|
||||
if (!_animations.TryGetValue(actionName, out AnimationClip clip))
|
||||
{
|
||||
Debug.LogWarning($"[FrameAnimator] 找不到动画 '{actionName}',跳过播放。");
|
||||
onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_animatorPlus.Play(clip, speed);
|
||||
|
||||
// 动画播完后触发回调(AnimatorPlus2D 自动回 Idle,此处仅等待时长)
|
||||
if (onComplete != null)
|
||||
{
|
||||
float duration = clip.length / Mathf.Max(speed, 0.01f);
|
||||
_completionCoroutine = StartCoroutine(WaitForCompletion(duration, onComplete));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即停止当前动作,切回 Idle。
|
||||
/// </summary>
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
StopCompletionCoroutine();
|
||||
_animatorPlus.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复 PlayableGraph。
|
||||
/// </summary>
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
_animatorPlus.SetPause(isPaused);
|
||||
}
|
||||
|
||||
// ── 内部逻辑 ────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator WaitForCompletion(float duration, Action onComplete)
|
||||
{
|
||||
yield return new WaitForSeconds(duration);
|
||||
onComplete?.Invoke();
|
||||
_completionCoroutine = null;
|
||||
}
|
||||
|
||||
private void StopCompletionCoroutine()
|
||||
{
|
||||
if (_completionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_completionCoroutine);
|
||||
_completionCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63273f95cd581594d85ac454f2395265
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色动画驱动器统一接口。
|
||||
/// 由具体的 MonoBehaviour 实现,挂载在角色 Prefab 上,
|
||||
/// CombatCharacterViewBase 在初始化时自动检测并持有引用。
|
||||
/// </summary>
|
||||
public interface ICharacterAnimator
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化动画驱动器,由 CombatCharacterViewBase.Initialize() 调用。
|
||||
/// </summary>
|
||||
void InitializeAnimator(CombatCharacterViewBase view);
|
||||
|
||||
/// <summary>
|
||||
/// 播放指定名称的动作(如 "Attack"、"Hit"、"Skill")。
|
||||
/// 播放完毕后自动回到 Idle,并触发 onComplete 回调。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称,需与 CharacterData.animations 的 Key 一致。</param>
|
||||
/// <param name="speed">播放速度倍率,默认 1.0。</param>
|
||||
/// <param name="onComplete">动作播放完毕后的回调,可为 null。</param>
|
||||
void PlayAction(string actionName, float speed = 1f, Action onComplete = null);
|
||||
|
||||
/// <summary>
|
||||
/// 立即切回 Idle 状态。
|
||||
/// </summary>
|
||||
void ReturnToIdle();
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复动画播放。
|
||||
/// </summary>
|
||||
void SetPause(bool isPaused);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a4fe7995a9658f4580e2984545da6ff
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 3 动画驱动器:Spine 骨骼动画(预留接口)。
|
||||
/// 美术资源到位后,接入 Spine-Unity Runtime 的 SkeletonAnimation API。
|
||||
///
|
||||
/// 预期实现思路:
|
||||
/// - InitializeAnimator:获取 SkeletonAnimation 引用,设置 Idle 动画
|
||||
/// - PlayAction:调用 SkeletonAnimation.AnimationState.SetAnimation()
|
||||
/// - 利用 Spine 事件系统触发攻击命中帧、特效帧等
|
||||
/// - 支持动画混合(如上半身攻击 + 下半身移动)
|
||||
/// </summary>
|
||||
public class SpineAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
// TODO: Spine Runtime 引用
|
||||
// private SkeletonAnimation _skeleton;
|
||||
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
Debug.LogWarning("[SpineAnimator] Spine 动画驱动器尚未实装,当前为占位。");
|
||||
// TODO: 获取 SkeletonAnimation 组件
|
||||
// _skeleton = view.mainView.GetComponent<SkeletonAnimation>();
|
||||
// _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
}
|
||||
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
Debug.LogWarning($"[SpineAnimator] PlayAction('{actionName}') 未实装。");
|
||||
// TODO:
|
||||
// var entry = _skeleton.AnimationState.SetAnimation(0, actionName, false);
|
||||
// entry.TimeScale = speed;
|
||||
// entry.Complete += _ => {
|
||||
// _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
// onComplete?.Invoke();
|
||||
// };
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
// TODO: _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
}
|
||||
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
// TODO: _skeleton.timeScale = isPaused ? 0f : 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d607d72147dad7441aca976ff675e6f2
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 1 动画驱动器:静态图片切换 + DOTween 位移。
|
||||
/// 不使用 AnimationClip,通过切换 SpriteRenderer.sprite 表现不同动作。
|
||||
/// 适合尚无帧动画资源的角色,或仅有立绘/静态图片的简易角色。
|
||||
/// </summary>
|
||||
public class StaticSpriteAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
[Header("Sprite 映射")]
|
||||
[Tooltip("动作名称 → Sprite 映射表,Key 需与 CharacterData.animations 的 Key 对齐(如 Idle、Attack、Hit)")]
|
||||
public SerializableDictionary<string, Sprite> actionSprites;
|
||||
|
||||
[Header("DOTween 参数")]
|
||||
[Tooltip("攻击时前冲距离")]
|
||||
[SerializeField] private float attackLungeDistance = 0.5f;
|
||||
|
||||
[Tooltip("攻击前冲时长")]
|
||||
[SerializeField] private float attackLungeDuration = 0.15f;
|
||||
|
||||
[Tooltip("受击抖动强度")]
|
||||
[SerializeField] private float hitShakeStrength = 0.3f;
|
||||
|
||||
[Tooltip("受击抖动时长")]
|
||||
[SerializeField] private float hitShakeDuration = 0.2f;
|
||||
|
||||
private SpriteRenderer _mainSpriteRenderer;
|
||||
private Sprite _idleSprite;
|
||||
private Vector3 _originalLocalPosition;
|
||||
private Tween _currentTween;
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 初始化:缓存主 SpriteRenderer 和 Idle Sprite。
|
||||
/// </summary>
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
_mainSpriteRenderer = view.mainView.GetComponent<SpriteRenderer>();
|
||||
if (_mainSpriteRenderer == null)
|
||||
_mainSpriteRenderer = view.mainView.GetComponentInChildren<SpriteRenderer>();
|
||||
|
||||
_originalLocalPosition = view.mainView.transform.localPosition;
|
||||
|
||||
// 尝试从映射表获取 Idle sprite,回退为当前显示的 sprite
|
||||
_idleSprite = actionSprites != null && actionSprites.TryGetValue("Idle", out Sprite idle)
|
||||
? idle
|
||||
: _mainSpriteRenderer != null ? _mainSpriteRenderer.sprite : null;
|
||||
|
||||
// 应用 Idle Sprite
|
||||
if (_mainSpriteRenderer != null && _idleSprite != null)
|
||||
_mainSpriteRenderer.sprite = _idleSprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放动作:切换 Sprite + DOTween 动效,完毕后自动回 Idle。
|
||||
/// </summary>
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
KillCurrentTween();
|
||||
|
||||
// 切换 Sprite
|
||||
if (_mainSpriteRenderer != null && actionSprites != null &&
|
||||
actionSprites.TryGetValue(actionName, out Sprite sprite))
|
||||
{
|
||||
_mainSpriteRenderer.sprite = sprite;
|
||||
}
|
||||
|
||||
// 根据动作类型执行 DOTween 动效
|
||||
float adjustedDuration = 1f / Mathf.Max(speed, 0.01f);
|
||||
Transform mainTransform = _mainSpriteRenderer != null
|
||||
? _mainSpriteRenderer.transform
|
||||
: transform;
|
||||
|
||||
switch (actionName)
|
||||
{
|
||||
case "Attack":
|
||||
case "Skill":
|
||||
PlayLungeAnimation(mainTransform, adjustedDuration, onComplete);
|
||||
break;
|
||||
|
||||
case "Hit":
|
||||
PlayShakeAnimation(mainTransform, adjustedDuration, onComplete);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 无特殊动效,短暂延迟后回 Idle
|
||||
float holdDuration = DefaultActionHoldDuration * adjustedDuration;
|
||||
_currentTween = DOVirtual.DelayedCall(holdDuration, () =>
|
||||
{
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即切回 Idle Sprite 并复位位置。
|
||||
/// </summary>
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
KillCurrentTween();
|
||||
|
||||
if (_mainSpriteRenderer != null && _idleSprite != null)
|
||||
_mainSpriteRenderer.sprite = _idleSprite;
|
||||
|
||||
if (_mainSpriteRenderer != null)
|
||||
_mainSpriteRenderer.transform.localPosition = _originalLocalPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复当前 Tween。
|
||||
/// </summary>
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
if (_currentTween != null && _currentTween.IsActive())
|
||||
{
|
||||
if (isPaused) _currentTween.Pause();
|
||||
else _currentTween.Play();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部动效 ────────────────────────────────────────────────────────
|
||||
|
||||
private const float DefaultActionHoldDuration = 0.3f;
|
||||
|
||||
private void PlayLungeAnimation(Transform target, float durationScale, Action onComplete)
|
||||
{
|
||||
float duration = attackLungeDuration * durationScale;
|
||||
Vector3 lungeOffset = target.right * attackLungeDistance;
|
||||
|
||||
_currentTween = DOTween.Sequence()
|
||||
.Append(target.DOLocalMove(_originalLocalPosition + lungeOffset, duration).SetEase(Ease.OutQuad))
|
||||
.Append(target.DOLocalMove(_originalLocalPosition, duration).SetEase(Ease.InQuad))
|
||||
.OnComplete(() =>
|
||||
{
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void PlayShakeAnimation(Transform target, float durationScale, Action onComplete)
|
||||
{
|
||||
float duration = hitShakeDuration * durationScale;
|
||||
|
||||
_currentTween = target.DOShakePosition(duration, hitShakeStrength, vibrato: 10, randomness: 90, fadeOut: true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
target.localPosition = _originalLocalPosition;
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void KillCurrentTween()
|
||||
{
|
||||
if (_currentTween != null && _currentTween.IsActive())
|
||||
{
|
||||
_currentTween.Kill();
|
||||
_currentTween = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
KillCurrentTween();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 665a1fe9fd0c77c46b1a8567c185f7ab
|
||||
Reference in New Issue
Block a user