多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,31 @@
using Animancer;
namespace BaseGames.Animation
{
/// <summary>
/// 静态工具类:将 AnimationEventConfigSO 中声明的事件注入 Animancer ClipTransition。
/// 使用 Animancer Pro APIClipTransition.Events.SetCallback(normalizedTime, Action)。
/// 调用时机MonoBehaviour.Awake在 Animancer 播放前完成绑定)。
/// </summary>
public static class AnimationEventBinder
{
/// <summary>
/// 将 config 中的所有事件回调注入到 clip 的 Animancer 事件系统。
/// </summary>
/// <param name="clip">目标 Animancer ClipTransition。</param>
/// <param name="config">事件配置资产null 时静默跳过)。</param>
/// <param name="receiver">事件接收者(通常是同一 MonoBehaviour。</param>
public static void Bind(ClipTransition clip, AnimationEventConfigSO config, IAnimationEventHandler receiver)
{
if (clip == null || config == null || receiver == null) return;
foreach (var entry in config.SortedEvents)
{
// 捕获循环变量,避免闭包陷阱
var captured = entry;
clip.Events.Add(captured.normalizedTime, () =>
receiver.HandleEvent(captured.eventType, captured.data));
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 14d8a3e3d7371e54eb25a5d4dded4645
guid: e9c16d7d7a6978a4c98a745261fbbf4f
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace BaseGames.Animation
{
/// <summary>
/// 动画事件配置资产(架构 §AnimationModule
/// 为指定 AnimationClip 声明一组事件时机,由 AnimationEventBinder 在运行时注入 Animancer 回调。
///
/// 使用方式:
/// 1. 在 Inspector 中创建此资产菜单Animation/EventConfig
/// 2. 配置 TargetClip 和 Events 列表。
/// 3. 在 MonoBehaviour.Awake 中AnimationEventBinder.Bind(clip, config, this)。
/// </summary>
[CreateAssetMenu(menuName = "Animation/EventConfig")]
public class AnimationEventConfigSO : ScriptableObject
{
[Serializable]
public struct EventEntry
{
public AnimationEventType eventType;
[Range(0f, 1f)]
[Tooltip("事件在动画中的归一化时间0 = 开始1 = 结束)。")]
public float normalizedTime;
[Tooltip("附加字符串数据(可空)。用于 payload 参数。")]
public string data;
}
[Header("绑定的动画片段")]
public AnimationClip targetClip;
[Header("事件时机列表(归一化时间)")]
public EventEntry[] events;
/// <summary>
/// 用于编辑器工具验证:记录创建 Config 时 Clip 的实际帧数,
/// 若 Clip 被替换(长度漂移 &gt;5 帧)则 EventConfigEditor 显示警告。
/// </summary>
[HideInInspector] public float ExpectedClipLength = -1f;
// ── 运行时查询 ───────────────────────────────────────────────────
/// <summary>按归一化时间升序返回所有事件(保证 SetCallback 顺序稳定)。</summary>
public IEnumerable<EventEntry> SortedEvents =>
events != null
? events.OrderBy(e => e.normalizedTime)
: Enumerable.Empty<EventEntry>();
/// <summary>
/// 返回第一个匹配类型的归一化时间;未找到时返回 -1。
/// </summary>
public float GetNormalizedTime(AnimationEventType eventType)
{
if (events == null) return -1f;
foreach (var e in events)
if (e.eventType == eventType) return e.normalizedTime;
return -1f;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3dc3625236c5f8747ae7352e78c01137
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
namespace BaseGames.Animation
{
/// <summary>
/// 动画事件类型枚举(架构 §AnimationModule
/// 用作 AnimationEventConfigSO 中事件时机的分类标签,
/// 与 AnimationEventBinder 配合将 Animancer ClipTransition 回调路由到 IAnimationEventHandler。
/// </summary>
public enum AnimationEventType
{
// ── 命中判定 ──────────────────────────────────────────────────────
EnableHitBox,
DisableHitBox,
AttackImpact, // 攻击命中瞬间反馈(不依赖特定 HitBox
// ── 招架窗口 ──────────────────────────────────────────────────────
EnableParryWindow,
DisableParryWindow,
// ── 无敌帧 ────────────────────────────────────────────────────────
EnableIFrame,
DisableIFrame,
// ── 移动反馈 ──────────────────────────────────────────────────────
Footstep,
LandImpact,
JumpLaunch,
// ── 交互 ──────────────────────────────────────────────────────────
EnableInteract,
// ── 通用反馈 ──────────────────────────────────────────────────────
TriggerFeedback, // payloadFeedbackPreset 名称
PlaySFX, // payloadSFX Id
// ── 敌方专用 ──────────────────────────────────────────────────────
SpawnProjectile, // payload弹幕配置 Id
RoarStart,
RoarEnd,
PhaseTwoStart,
// ── 玩家取消窗口 ──────────────────────────────────────────────────
CancelWindowOpen,
CancelWindowClose,
// ── 状态机钩子 ────────────────────────────────────────────────────
StateTransition, // payload目标状态 Id
AnimationComplete, // payload上下文字符串可空
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97412b5f50276484fa226d97b8c3769b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -9,7 +9,12 @@
"rootNamespace": "BaseGames.Animation",
"references": [
"BaseGames.Core.Events",
"Kybernetik.Animancer"
"Kybernetik.Animancer",
"BaseGames.Combat",
"BaseGames.Parry",
"BaseGames.Feedback",
"BaseGames.Player",
"BaseGames.Enemies"
],
"autoReferenced": true,
"overrideReferences": false,

View File

@@ -0,0 +1,111 @@
using System;
using Animancer;
using BaseGames.Combat;
using BaseGames.Enemies;
using UnityEngine;
namespace BaseGames.Animation
{
/// <summary>
/// 敌人动画事件接收器(架构 §AnimationModule
/// 挂载于敌人 Prefab 根节点。负责将 Animancer 动画时间点回调路由到
/// HitBox 激活、弹幕生成、嘶吼状态、二阶段切换等系统。
///
/// 使用方式:
/// 1. 在 Inspector 中填充 _hitBoxes、_enemy。
/// 2. 将每条 ClipTransition + AnimationEventConfigSO 配对添加到 _bindings。
/// 3. Awake 中 AnimationEventBinder.Bind 自动完成注入。
/// </summary>
public class EnemyAnimationEvents : MonoBehaviour, IAnimationEventHandler
{
[Serializable]
public struct EventBinding
{
[Tooltip("Animancer ClipTransition需与动画组件中同一引用绑定。")]
public ClipTransition clip;
[Tooltip("对应的 AnimationEventConfigSO 资产。")]
public AnimationEventConfigSO config;
}
[Header("子系统引用")]
[SerializeField] private HitBox[] _hitBoxes;
[SerializeField] private EnemyBase _enemy;
[Header("事件绑定(每个 Clip 对应一个配置资产)")]
[SerializeField] private EventBinding[] _bindings;
private void Awake()
{
if (_enemy == null)
_enemy = GetComponentInParent<EnemyBase>();
foreach (var b in _bindings)
AnimationEventBinder.Bind(b.clip, b.config, this);
}
// ── IAnimationEventHandler ─────────────────────────────────────────
public void HandleEvent(AnimationEventType type, string payload)
{
switch (type)
{
// ── 命中判定 ──────────────────────────────────────────────
case AnimationEventType.EnableHitBox:
SetHitBoxActive(payload, true);
break;
case AnimationEventType.DisableHitBox:
SetHitBoxActive(payload, false);
break;
// ── 弹幕 / 技能 ───────────────────────────────────────────
case AnimationEventType.SpawnProjectile:
_enemy?.SpawnProjectile(payload);
break;
// ── 嘶吼 ──────────────────────────────────────────────────
case AnimationEventType.RoarStart:
_enemy?.SetRoaring(true);
break;
case AnimationEventType.RoarEnd:
_enemy?.SetRoaring(false);
break;
// ── 二阶段切换 ────────────────────────────────────────────
case AnimationEventType.PhaseTwoStart:
_enemy?.TriggerPhaseTwo();
break;
// ── 状态机钩子 ────────────────────────────────────────────
case AnimationEventType.AnimationComplete:
_enemy?.OnAnimationComplete(payload);
break;
}
}
// ── 私有辅助 ───────────────────────────────────────────────────────
/// <summary>
/// 按 Id 激活或停用 HitBox。
/// payload 为空时操作所有 HitBox非空时仅操作 Id 匹配的 HitBox。
/// </summary>
private void SetHitBoxActive(string id, bool active)
{
if (_hitBoxes == null) return;
bool matchAll = string.IsNullOrEmpty(id);
foreach (var hb in _hitBoxes)
{
if (hb == null) continue;
if (matchAll || hb.Id == id)
{
if (active) hb.Activate();
else hb.Deactivate();
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 13003b40ed7bd164d8d90eb58f0548ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
namespace BaseGames.Animation
{
/// <summary>
/// 动画事件接收接口(架构 §AnimationModule
/// 由 PlayerAnimationEvents / EnemyAnimationEvents 实现。
/// AnimationEventBinder 将 Animancer 回调路由至此接口,避免硬耦合。
/// </summary>
public interface IAnimationEventHandler
{
/// <summary>
/// 处理由 Animancer ClipTransition 触发的动画事件。
/// </summary>
/// <param name="type">事件类型。</param>
/// <param name="payload">附加字符串数据(可为 null 或空)。</param>
void HandleEvent(AnimationEventType type, string payload);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2c57ff98093620448d1f7c8ce6d0511
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,149 @@
using System;
using Animancer;
using BaseGames.Combat;
using BaseGames.Feedback;
using BaseGames.Parry;
using BaseGames.Player;
using UnityEngine;
namespace BaseGames.Animation
{
/// <summary>
/// 玩家动画事件接收器(架构 §AnimationModule
/// 挂载于玩家 Prefab 根节点。负责将 Animancer 动画时间点回调路由到
/// HitBox 激活、招架窗口、无敌帧、移动取消窗口、反馈等系统。
///
/// 使用方式:
/// 1. 在 Inspector 中填充 _hitBoxes、_hurtBox、_mover、_parrySystem。
/// 2. 将每条 ClipTransition + AnimationEventConfigSO 配对添加到 _bindings。
/// 3. Awake 中 AnimationEventBinder.Bind 自动完成注入。
/// </summary>
public class PlayerAnimationEvents : MonoBehaviour, IAnimationEventHandler
{
[Serializable]
public struct EventBinding
{
[Tooltip("Animancer ClipTransition需与动画组件中同一引用绑定。")]
public ClipTransition clip;
[Tooltip("对应的 AnimationEventConfigSO 资产。")]
public AnimationEventConfigSO config;
}
[Header("子系统引用")]
[SerializeField] private HitBox[] _hitBoxes;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private PlayerMovement _mover;
[SerializeField] private ParrySystem _parrySystem;
[Header("事件绑定(每个 Clip 对应一个配置资产)")]
[SerializeField] private EventBinding[] _bindings;
private IFeedbackPlayer _feedback;
private void Awake()
{
// IFeedbackPlayer 由父层或同层实现PlayerFeedback / NullFeedbackPlayer
_feedback = GetComponentInParent<IFeedbackPlayer>()
?? NullFeedbackPlayer.Instance;
foreach (var b in _bindings)
AnimationEventBinder.Bind(b.clip, b.config, this);
}
// ── IAnimationEventHandler ─────────────────────────────────────────
public void HandleEvent(AnimationEventType type, string payload)
{
switch (type)
{
// ── 命中判定 ──────────────────────────────────────────────
case AnimationEventType.EnableHitBox:
SetHitBoxActive(payload, true);
break;
case AnimationEventType.DisableHitBox:
SetHitBoxActive(payload, false);
break;
case AnimationEventType.AttackImpact:
_feedback.PlayAttackWhoosh();
break;
// ── 招架窗口 ──────────────────────────────────────────────
case AnimationEventType.EnableParryWindow:
_parrySystem?.OpenParryWindow();
break;
case AnimationEventType.DisableParryWindow:
_parrySystem?.CloseParryWindow();
break;
// ── 无敌帧 ────────────────────────────────────────────────
case AnimationEventType.EnableIFrame:
_hurtBox?.SetInvincible(true);
break;
case AnimationEventType.DisableIFrame:
_hurtBox?.SetInvincible(false);
break;
// ── 移动反馈 ──────────────────────────────────────────────
case AnimationEventType.LandImpact:
_feedback.PlayLandImpact();
break;
case AnimationEventType.JumpLaunch:
_feedback.PlayJumpLaunch();
break;
case AnimationEventType.Footstep:
_feedback.PlayFootstep();
break;
// ── 取消窗口 ──────────────────────────────────────────────
case AnimationEventType.CancelWindowOpen:
_mover?.SetCancelWindowOpen(true);
break;
case AnimationEventType.CancelWindowClose:
_mover?.SetCancelWindowOpen(false);
break;
// ── 通用反馈 ──────────────────────────────────────────────
case AnimationEventType.TriggerFeedback:
if (!string.IsNullOrEmpty(payload))
_feedback.TriggerPreset(payload);
break;
case AnimationEventType.PlaySFX:
if (!string.IsNullOrEmpty(payload))
_feedback.PlaySFXById(payload);
break;
}
}
// ── 私有辅助 ───────────────────────────────────────────────────────
/// <summary>
/// 按 Id 激活或停用 HitBox。
/// payload 为空时操作所有 HitBox非空时仅操作 Id 匹配的 HitBox。
/// </summary>
private void SetHitBoxActive(string id, bool active)
{
if (_hitBoxes == null) return;
bool matchAll = string.IsNullOrEmpty(id);
foreach (var hb in _hitBoxes)
{
if (hb == null) continue;
if (matchAll || hb.Id == id)
{
if (active) hb.Activate();
else hb.Deactivate();
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4e3a90eca47d12a49a07be7f38a376e8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,3 +0,0 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Animation { }