This commit is contained in:
SoulliesOfficial
2026-04-01 12:23:27 -04:00
parent aff7ac0e03
commit c3b1561375
933 changed files with 114333 additions and 119360 deletions

View File

@@ -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 实现,无动画驱动器。");
}
}
}

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 63273f95cd581594d85ac454f2395265

View File

@@ -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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a4fe7995a9658f4580e2984545da6ff

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d607d72147dad7441aca976ff675e6f2

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 665a1fe9fd0c77c46b1a8567c185f7ab