当前位置: 首页 > news >正文

Arpg第五节——方法

延迟激活碰撞体ColliderEnable

这段代码实现了一个可控制激活时间的碰撞体组件,主要功能是:

  1. 延迟激活 - 物体启用后,等待指定延迟时间才激活碰撞体

  2. 限时存活 - 碰撞体激活后只持续指定时间,然后自动关闭

  3. 状态管理 - 使用状态机管理碰撞体的生命周期

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ColliderEnable : MonoBehaviour
{[Header("延迟多长时间激活")]public float delay; // 碰撞体激活前的延迟时间(秒)Collider _collider; // 引用挂载的碰撞体组件float enable_time; // 记录组件被启用时的时间戳[Header("存活时长")]public float duration; // 碰撞体激活后持续的时长(秒)byte state = 0; // 状态机:0-等待激活 1-等待关闭 2-已关闭private void Awake(){// 初始化时获取碰撞体组件引用_collider = this.GetComponent<Collider>();}private void OnEnable(){// 记录组件启用时的时间戳enable_time = GameTime.time;// 初始禁用碰撞体_collider.enabled = false;// 重置状态为等待激活state = 0;}void Update(){if (state == 0) // 等待激活状态{// 检查是否达到延迟激活时间if (GameTime.time - enable_time >= delay){// 激活碰撞体_collider.enabled = true;// 进入等待关闭状态state = 1;}}else if (state == 1) // 激活后的持续状态{// 检查是否达到总持续时间(延迟时间+持续时长)if (GameTime.time - enable_time >= delay + duration){// 禁用碰撞体_collider.enabled = false;// 进入已关闭状态state = 2;}}// state == 2 时不再进行任何操作}
}

技能编辑器SkillEditor

using System;           // 基础.NET命名空间,包含核心类型(如String、Int32)和基础类
using System.Collections;       // 包含非泛型集合(如ArrayList、Hashtable)
using System.Collections.Generic; // 包含泛型集合(如List<T>、Dictionary<TKey, TValue>)using UnityEditor;      // Unity编辑器API命名空间,用于扩展编辑器功能(仅Editor模式下可用)using UnityEngine;               // Unity引擎核心功能(如GameObject、Component、MonoBehaviour)
using UnityEngine.Playables;     // Timeline播放系统相关功能
using UnityEngine.Timeline;      // Timeline时间轴系统相关功能
using System.Linq;               // LINQ(语言集成查询)功能,用于数据查询操作

1. 类定义和菜单项

public class SkillEditor : Editor
{[MenuItem("GameObject/CreateStateConfig")]public static void CreateStateConfig(){// 方法实现...}
}
  • 继承自 Editor 类,表明这是一个 Unity 编辑器扩展

  • 添加了菜单项 GameObject/CreateStateConfig,可以在 Unity 编辑器的 GameObject 菜单中调用

2. 获取选中的对象和 PlayableDirector

  • 获取当前选中的 GameObject

  • 检查该对象是否有 PlayableDirector 组件(用于控制 Timeline 播放)

       //选择的物体 不是空var go = Selection.activeGameObject;if (go == null){return;}var director = go.GetComponent<PlayableDirector>();if (director == null){return;}
var go = Selection.activeGameObject;

获取当前在 Unity 编辑器中“被选中”的那个 GameObject(游戏对象)

  • Selection 是 Unity Editor 命名空间下的一个静态类UnityEditor.Selection),它代表用户在编辑器中当前的选择状态
  • activeGameObject 是 Selection 类的一个属性,返回当前在 Hierarchy 或 Scene 视图中被选中的 GameObject
    • 如果没有选中任何 GameObject,它返回 null
    • 如果选中了多个 GameObject,它返回最后被点击/激活的那个(即“主选对象”)。

3. 分析 Timeline 轨道并判断轨道类型

  • 初始化轨道字典,用于存储不同类型的轨道

  • 遍历 Timeline 的所有输出轨道

  • 识别并分类不同类型的轨道

/ 初始化动画总时长为 0
double anm_length = 0;// 创建一个字典,用于存储不同类型的轨道(TrackAsset)
// Key: 轨道类型标识(0=动作轨道,1=特效控制轨道,2=特效配置参数轨道)
// Value: 对应的 TrackAsset 对象
Dictionary<int, TrackAsset> track_dct = new Dictionary<int, TrackAsset>();//0动作 1特效  2特效配置参数
foreach (PlayableBinding item in director.playableAsset.outputs)
{// 分析不同类型的轨道
}

语法解析——遍历TimeLine输出结果

foreach (PlayableBinding item in director.playableAsset.outputs)
{var track = item.sourceObject as TrackAsset;
}

1、director 是什么?

👉 它是一个 PlayableDirector 类型的组件(Component),通常挂载在某个 GameObject 上。

  • 作用:控制 Timeline(时间轴)的播放、暂停、绑定对象等。

2、director.playableAsset是什么?

👉 它是 director 所关联的 PlayableAsset —— 通常是 TimelineAsset(你创建的那个 .playable 文件)。

  • 作用:存储 Timeline 的结构,包括轨道(Track)、剪辑(Clip)、行为等。
  • 类型PlayableAsset 是基类,实际运行时通常是 TimelineAsset

3、director.playableAsset.outputs

👉 这是一个 IList<PlayableBinding> 类型的集合 —— 即“输出绑定列表”。

  • 作用:描述 Timeline 中每个轨道(Track)如何与场景中的 GameObject 或组件进行绑定。
  • 数量:通常与 Timeline 中的轨道数量一致(但不绝对,某些轨道可能没有输出绑定)。
  • 内容:每个 PlayableBinding 对应一个轨道的“输出配置”。

4、PlayableBinding item

👉 每次循环中的 item 是一个 PlayableBinding 结构体(struct),它包含以下重要字段:

字段名类型说明
sourceObjectUnityEngine.Object通常指向 Timeline 中的 TrackAsset(轨道)
streamNamestring输出流的名称(通常和轨道名一致)
sourceOutputTypeType输出数据的类型,如 typeof(AnimationPlayableOutput)
outputTargetObject绑定的目标对象(如 Animator、GameObject 等,可能为 null)

        总结:PlayableBinding 是 item 的类型,director.playableAsset.outputs即是该TimeLine中的输出结果——这段语法就是遍历该输出结果。

   

例如:

执行代码如下:

if (director?.playableAsset?.outputs != null)
{foreach (PlayableBinding binding in director.playableAsset.outputs){// 获取轨道if (binding.sourceObject is TrackAsset track){Debug.Log($"轨道类型: {track.GetType().Name}, 名称: {track.name}");// 获取绑定目标var target = binding.outputTarget;if (target != null){Debug.Log($"绑定到: {target.name}");// 根据轨道类型进行不同处理if (track is AnimationTrack animationTrack){Debug.Log("这是一个动画轨道");var animator = target as Animator;if (animator != null){Debug.Log($"绑定到: {animator.gameObject.name} 的 Animator");}}else if (track is ControlTrack controlTrack){Debug.Log("这是一个控制轨道");}else if (track is ActivationTrack activationTrack){Debug.Log("这是一个激活轨道");var gameObject = target as GameObject;if (gameObject != null){Debug.Log($"绑定到: {gameObject.name}");}}}}}
}

输出结果:

轨道类型: AnimationTrack, 名称: Animation Track
绑定到: Player
这是一个动画轨道
绑定到: Player 的 Animator轨道类型: ControlTrack, 名称: Control Track
绑定到: null
这是一个控制轨道轨道类型: ActivationTrack, 名称: Activation Track
绑定到: Enemy
这是一个激活轨道
绑定到: Enemy

语法解析——安全类型转换

TrackAsset track = item.sourceObject as TrackAsset;
  • item.sourceObject as TrackAsset:表达式,执行“安全类型转换”

流程作用:

  1. 获取 item.sourceObject → 得到一个 UnityEngine.Object 类型的对象。
  2. 尝试将其转换为 TrackAsset
    • ✅ 如果它确实是 TrackAsset 或其子类(如 AnimationTrack)→ 返回该对象。
    • ❌ 如果不是(比如是 null 或其他类型)→ 返回 null
  3. 将结果赋值给变量 track
  4. track 的类型是 TrackAsset(或 null)。

为什么要转换类型:这里可能是TimeLine输出结果的容器只能是sourceObject,不能区分出具体的Track类型,所以才要转换成TrackAsset用于分类

UnityEngine.Object└── ScriptableObject└── TrackAsset├── AnimationTrack├── ActivationTrack├── ControlTrack         

代码解析——判断轨道类型并存储

// 遍历 Timeline Director 所关联的 PlayableAsset 的所有输出绑定(即所有轨道)
foreach (PlayableBinding item in director.playableAsset.outputs)
{// 尝试将当前绑定的 sourceObject 转换为 TrackAsset(Timeline 中的轨道基类)var track = item.sourceObject as TrackAsset;// 如果转换成功(即确实是轨道对象)if (track != null){// 情况1:如果是动画轨道(AnimationTrack)if (track is AnimationTrack _AnimationTrack){// 确保只记录第一个动画轨道(避免重复赋值)if (track_dct.ContainsKey(0) == false){// 打印动画轨道名称(调试用)Debug.Log(_AnimationTrack.name + "  " + track.name);// 将该动画轨道存入字典,键为 0track_dct[0] = track;// 获取该轨道上的所有 TimelineClip(通常是动画片段)TimelineClip[] clip = track.GetClips() as TimelineClip[];// 计算第一个 Clip 的持续时间(结束时间 - 开始时间),作为整个动作的参考时长              anm_length = clip[0].end - clip[0].start;// 打印计算出的动作时长(调试用)Debug.Log("动作时长:" + anm_length);}}// 情况2:如果是控制轨道(ControlTrack,通常用于激活/控制 GameObject,比如特效)else if (track is ControlTrack _ControlTrack){// 确保只记录第一个控制轨道if (track_dct.ContainsKey(1) == false){// 打印控制轨道名称(调试用)Debug.Log(_ControlTrack.name + "  " + track.name);// 将该控制轨道存入字典,键为 1track_dct[1] = track;}}// 情况3:如果是特效数据配置轨道,存入字典,键为2else if (track is EffectTrackAsset _EffectTrackAsset){if (track_dct.ContainsKey(2) == false){track_dct[2] = track;}}// 情况4:如果是位移配置轨道,存入字典,键为3else if (track is MoveTrackAsset _MoveTrackAsset){if (track_dct.ContainsKey(3) == false){track_dct[3] = track;}}}
}

// 获取该轨道上的所有 TimelineClip(通常是动画片段)
TimelineClip[] clip = track.GetClips() as TimelineClip[];
// 计算第一个 Clip 的持续时间(结束时间 - 开始时间),作为整个动作的参考时长              
nm_length = clip[0].end - clip[0].start;
  • track.GetClips()
    调用 track 对象的 GetClips() 方法,该方法通常返回一个 IEnumerable<TimelineClip>(可枚举的动画片段集合)。

  • as TimelineClip[]
    使用 as 操作符尝试将返回值强制转换TimelineClip[](即 TimelineClip 数组)

  • 两种变量的参数都是关于动画片段

    • TimelineClip[]:适用于需要快速随机访问元素的情况,但需要预先知道元素数量并占用更多内存。
    • IEnumerable<TimelineClip>:适用于需要遍历大量元素的情况,更加灵活和高效,但不支持直接通过索引访问元素

TimelineClip和TrackAsset的区别

Q1:TimelineClip[ ]和TrackAsset都包含了Skin (Animator) 轨道,这两者变量区别是什么

A1:

TrackAsset —— 代表“轨道”本身

  • TrackAsset 是 Unity Timeline 系统中的一个类,表示 Timeline 时间轴上的一个轨道(Track)
    • 例如你图中的:

      • “Skin (Animator)” 轨道
      • “Control Track” 轨道
      • “4002Atk1” 轨道

      每一个都是一个 TrackAsset 实例(或其子类,如 AnimationTrackActivationTrack 等)。

    • 它能做什么?

      • 存储轨道的设置(如轨道名称、绑定对象、混合模式等)

TimelineClip[] —— 代表“轨道上的动画片段数组”

  • TimelineClip[] 是一个数组,存储某个轨道上的所有动画片段(Clips)对象

            例如你图中 “Skin (Animator)” 轨道上有一个片段叫 attack_01 —— 它就是一个 TimelineClip

      TimelineClip[] 就是这些片段的集合(数组形式)。

  • 它能做什么?

    • 访问每个片段的属性:.start.end.duration.displayName.asset(关联的动画资源)等

图文对比

项目TrackAssetTimelineClip[]
代表什么一条轨道(如 Skin (Animator))该轨道上的多个动画片段组成的数组
层级关系父级容器子级内容(片段集合)
功能管理轨道设置、获取/操作片段集合操作每个片段的时间、资源、属性等
获取方式timelineAsset.GetTrack(index) 或通过路径查找track.GetClips().ToArray()
是否包含轨道?✅ 是轨道本身❌ 不包含轨道,只是轨道上的片段集合
TrackAsset 是轨道本身,TimelineClip[] 是这条轨道上所有动画片段的集合 —— 前者是“容器”,后者是“内容”。

4. 提取特效信息

  • 从特效轨道提取所有剪辑

  • 计算每个特效相对于动画时长的触发时间点

  • 创建特效配置对象

  • 将特效对象保存为预制体

// 声明一个字典,用于存储技能特效配置项,键为资源路径,值为特效配置对象
Dictionary<string, SkillEditor_EffectItemConfig> skill_dct = new Dictionary<string, SkillEditor_EffectItemConfig>();// 【特效轨道处理】尝试从轨道字典中获取索引为1的轨道(通常代表特效轨道)
if (track_dct.TryGetValue(1, out var effect))
{// 获取该轨道上所有的动画片段(clips为TimelineClip数组)var clips = effect.GetClips() as TimelineClip[];// 遍历每一个剪辑foreach (var item in clips){// 计算该剪辑的触发时间比例(触发点就是百分比类型数据)var n = item.start / anm_length;// 创建一个新的技能特效配置对象SkillEditor_EffectItemConfig c = new SkillEditor_EffectItemConfig();// 将剪辑关联的资产转换为ControlPlayableAsset类型(用于控制GameObject的播放资产)ControlPlayableAsset x = item.asset as ControlPlayableAsset;// 通过PlayableDirector的解析器,解析出该资产关联的实际场景中的GameObjectvar o = x.sourceGameObject.Resolve(director.playableGraph.GetResolver());// 设置配置对象的obj字段:// 如果该对象没有父物体,则使用自身;否则使用其父物体(通常是为了获取完整的特效根节点)c.obj = o.transform.parent == null ? o : o.transform.parent.gameObject;// 设置资源路径,格式为 "Effect/对象名"c.res_path = $"Effect/{c.obj.name}";// 设置触发时间比例(0~1之间的值,表示在动画中的相对触发时刻)c.trigger = n;// 将该配置项添加到字典中,以资源路径为键skill_dct[c.res_path] = c;// 在控制台输出日志,显示特效生成点的时间比例和对象名称Debug.Log($"特效生成点:{n}  {c.obj.name}");// 标记是否需要创建Prefab,默认为truebool create_prefab = true;// 检查当前GameObject是否已经是Prefab的实例(避免重复创建)if (PrefabUtility.GetPrefabInstanceStatus(c.obj) != PrefabInstanceStatus.NotAPrefab){// 如果是Prefab实例,则跳过创建create_prefab = false;}// 定义Prefab保存路径(位于Assets/Resources/Effect/目录下)string prefabPath = "Assets/Resources/Effect/" + c.obj.name + ".prefab";// 如果需要创建Prefab,且该路径下尚不存在同名Prefab资源if (create_prefab && AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) == null){// 将当前GameObject保存为Prefab资源,并自动建立连接(保持场景中对象与Prefab的关联)GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(c.obj, prefabPath, InteractionMode.AutomatedAction);// 注意:返回的prefab对象可以用于后续处理(如记录、验证等),但当前未使用}}
}

语法解析——ControlPlayableAsset类型

// 将剪辑关联的资产转换为ControlPlayableAsset类型(用于控制GameObject的播放资产)
ControlPlayableAsset x = item.asset as ControlPlayableAsset;

ControlPlayPlayableAsset 

        是 Unity Timeline 系统中的一个核心组件,它允许你在时间轴的一条轨道上直接控制另一个游戏对象(GameObject)的激活状态、播放其动画以及触发事件

Control Track 和 ControlPlayableAsset 的关系

  1. Control Track:

    • 是 Timeline 中的一种轨道类型。
    • 用于放置和管理 ControlPlayableAsset 类型的剪辑(Clips)。
    • 可以用来控制场景中的 GameObject 在特定时间点的行为变化。
  2. ControlPlayableAsset:

    • 是一种特殊的 Playable Asset,专门用于控制 GameObject 的行为。
    • 它包含一个 ExposedReference<GameObject> 字段,允许你在 Inspector 中指定要控制的 GameObject。
    • 提供了多种控制模式,如克隆(Clone)、激活(Activate)、禁用(Deactivate)等。

TimelineClip和TrackAsset的区别 的性质相似——Track是容器,Asset是内容。

另外,在这里ControlPlayableAsset是用于控制特效/碰撞器生成

语法解析——解析关联资产

这行代码的核心目的是:将一个在编辑时设置的、间接的引用(ExposedReference<GameObject>)转换(解析)为运行时实际的、可用的 GameObject 对象(GameObject)。

ControlPlayableAsset x = item.asset as ControlPlayableAsset;// 通过PlayableDirector的解析器,解析出该资产关联的实际场景中的GameObject
var o = x.sourceGameObject.Resolve(director.playableGraph.GetResolver());

1、x.sourceGameObject 的类型

        它不是我们常见的 public GameObject类型,而是ExposedReference<GameObject>类型

,间接引用物体的变量类型

public struct ExposedReference<T> where T : Object
{public T Resolve(IExposedPropertyTable resolver);// 内部其实存的是一个 GUID 或路径字符串
}

        这里涉及到资源资产(Asset)和场景中资产(Scene)的引用关系:即使在同一个场景中,如果你删除了被引用的 GameObject,然后创建一个新的、名字相同的 GameObject,引用也会丢失——这就是间接引用的来源

2、director.playableGraph.GetResolver()

        这个函数一个“解析器”,知道怎么根据“引用描述”找到真实 GameObject

  • director: 你的 PlayableDirector 组件引用。

  • .playableGraph: 每个 PlayableDirector 在播放时都会管理一个 PlayableGraph。这是 Unity Playables API 的核心,它负责组织所有播放节点(Playable)及其关系。

  • .GetResolver(): 这是关键!PlayableGraph 继承并持有了那个作为“柜台”的 IExposedPropertyTable 解析器。

    • 这个解析器就是在 Unity 编辑器中你设置那个 PlayableDirector 时,由 Unity 自动创建和维护的映射表

3、.Resolve(...)

        这就是执行具体的解析动作了。


🟢 第1层xControlPlayableAssetTimeline 剪辑关联的资产对象(控制型资产)
🟡 第2层x.sourceGameObjectExposedReference<GameObject>一个“可暴露的引用”,不是 GameObject 本身,而是“引用描述”
🔵 第3层director.playableGraph.GetResolver()IExposedPropertyTable一个“解析器”,知道怎么根据“引用描述”找到真实 GameObject
🟣 第4层.Resolve(...)方法调用执行“解析”动作,返回真实的 GameObject
🟥 最终var o = ...GameObject解析出来的实际游戏对象,可以拿来用(比如 Instantiate、GetParent、改名等

代码解析——物体信息存储到结构体类中

// 设置配置对象的obj字段(储存在SkillEditor_EffectItemConfig实例中):// 如果该对象没有父物体,则使用自身;否则使用其父物体(通常是为了获取完整的特效根节点)c.obj = o.transform.parent == null ? o : o.transform.parent.gameObject;// 设置资源路径,格式为 "Effect/对象名"c.res_path = $"Effect/{c.obj.name}";// 设置触发时间比例(0~1之间的值,表示在动画中的相对触发时刻)c.trigger = n;// 将该配置项添加到字典中,以资源路径为键skill_dct[c.res_path] = c;// 在控制台输出日志,显示特效生成点的时间比例和对象名称Debug.Log($"特效生成点:{n}  {c.obj.name}");

额外注意一下,这里只是存储资源路径——下段内容中的prefabPath才是用来将该对象物体存入Resource/Effect的文件夹中,别搞错了。

语法解析——选中物体创建对应预制体放入Resource文件夹

// 声明一个布尔变量,用于控制是否创建Prefab,默认为true(即默认要创建)
bool create_prefab = true;// 检查当前GameObject是否已经是某个Prefab的实例
if (PrefabUtility.GetPrefabInstanceStatus(c.obj) != PrefabInstanceStatus.NotAPrefab)
{// 如果不是 "NotAPrefab"(即它是Prefab实例、断开连接的实例等),则不创建新的Prefabcreate_prefab = false;
}// 定义要保存Prefab的路径(在Assets/Resources/Effect/目录下,以对象名称命名.prefab文件)
string prefabPath = "Assets/Resources/Effect/" + c.obj.name + ".prefab";// 只有在满足两个条件时才创建Prefab:
// 1. create_prefab 为 true(即当前对象不是Prefab实例)
// 2. 该路径下尚不存在同名Prefab资源(避免重复创建或覆盖)
if (create_prefab && AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) == null)
{// 创建Prefab资产并自动将原GameObject与新Prefab建立连接(即变成Prefab实例)GameObject prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(c.obj, prefabPath, InteractionMode.AutomatedAction);
}
if (PrefabUtility.GetPrefabInstanceStatus(c.obj) != PrefabInstanceStatus.NotAPrefab)

1. PrefabUtility.GetPrefabInstanceStatus(c.obj)

  • 作用:调用 Unity 编辑器 API,获取 GameObject c.obj 的 Prefab 实例状态。
  • 返回值类型PrefabInstanceStatus(枚举类型)
  • 常见值
    • NotAPrefab — 不是 Prefab 实例
    • Connected — 是已连接的 Prefab 实例
    • Disconnected — 是断开连接的 Prefab 实例
    • MissingAsset — Prefab 资源丢失

这个判断条件就是当前对象如果不为实例

AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) == null

AssetDatabase.LoadAssetAtPath(...)

这是 Unity Editor API 中的一个静态方法调用

  • 所属类AssetDatabase(UnityEditor 命名空间)
  • 作用:从项目 Assets 文件夹中的指定路径加载资源(Asset)

typeof(GameObject)

  • 作用:C# 中的运算符,用于在编译时获取某个类型的 System.Type 对象
  • 返回值Type 类型,表示 GameObject 类的元数据
  • 用途:告诉 LoadAssetAtPath 方法:“我要加载的是 GameObject 类型的资源”

可以理解为从prefabPath加载GameObject类型的资源

PrefabUtility.SaveAsPrefabAssetAndConnect(c.obj, prefabPath, InteractionMode.AutomatedAction);

将场景中的 GameObject c.obj 保存为一个 新的 Prefab 资产(保存到 prefabPath 路径),并自动将原 GameObject 替换为该 Prefab 的实例

public static GameObject SaveAsPrefabAssetAndConnect(GameObject instanceGameObject,   // 要保存为Prefab的源GameObject(通常是场景中的对象)string assetPath,                // Prefab 保存的路径(必须是 Assets/ 开头的完整路径)InteractionMode interactionMode  // 交互模式:决定是否弹窗、是否撤销支持等
);

5. 处理特效配置轨道

将EffectPlayableAsset中的配置参数导入到对应状态面板(StateScriableObject)Effectconfig中

  //收集特效Effect配置轨道 ==> 转化为需求参数 (插入到stateconfig_effectconfig)if (track_dct.TryGetValue(2, out var effect_config_track)){//获取当前状态TimeLine全部Clip片段var clips = effect_config_track.GetClips() as TimelineClip[];for (int i = 0; i < clips.Length; i++){var item = clips[i];// 尝试将 Clip 关联的资源(asset)转换为 EffectPlayableAsset 类型(自定义的可播放特效资产)var effectPlayableAsset = item.asset as EffectPlayableAsset;if (effectPlayableAsset != null)//如果转换成功{//从技能配置字典 skill_dct 中,按索引 i 获取对应的技能配置(但似乎并不推荐这种方式)//将技能配置中的资源路径赋值给特效资产的配置//将配置好的特效参数反向赋值回技能配置对象,供后续使用var skill_config = skill_dct.ElementAt(i).Value;effectPlayableAsset.Config.res_path = skill_config.res_path;effectPlayableAsset.Config.trigger = (float)Math.Round(skill_config.trigger, 3);skill_config.effectConfig = effectPlayableAsset.Config;                  }}}

6. 处理位移轨道

if (track_dct.TryGetValue(3, out var move))
{var clips = move.GetClips() as TimelineClip[];foreach (var item in clips){// 计算位移触发时间和持续时间var n = item.start / anm_length;var end = (float)Math.Round((item.end / anm_length), 3);// 创建位移配置move_config.Add(x.Config);}
}

7. 加载和更新状态配置

string unit_id = director.transform.parent.gameObject.name;
int state_id = int.Parse(director.transform.gameObject.name.Split('@')[0]);

红色部分unit_id,紫色部分state_id

1、遍历到对应的状态id表(如1005)

// 从资源中加载指定单位的状态配置文件(ScriptableObject),路径为 "StateConfig/{unit_id}"
var anmConfig = Resources.Load<StateScriptableObject>($"StateConfig/{unit_id}");// 声明一个列表,用于存储当前要处理的目标状态所关联的特效配置(EffectConfig)
List<EffectConfig> target = null; // 初始化为空,后续会指向匹配状态的 effectConfigs// 遍历该单位所有状态配置,查找与当前 state_id 匹配的状态
for (int i = 0; i < anmConfig.states.Count; i++)
{// 如果找到匹配的状态ID,则将该状态下的特效配置列表赋值给 targetif (state_id == anmConfig.states[i].id){target = anmConfig.states[i].effectConfigs;break; // 找到后可提前退出循环(可选优化,非必需)}
}

2、找到表后清理无效的表

target是当前状态1005的effectConfigs数量

Dictionary<string, SkillEditor_EffectItemConfig> skill_dct 是全局自定义类字典,用于收集并关联来自不同轨道的数据

键(Key): string 类型,存储的是特效的资源路径(如:"Effect/FireExplosion")
值(Value): SkillEditor_EffectItemConfig 类型,是一个自定义类,存储了某个特效的所有配置信息
// 来自特效轨道(ControlTrack)的处理
skill_dct[c.res_path] = c; // 存入资源路径和基础信息// 来自特效配置轨道的处理
var skill_config = skill_dct.ElementAt(i).Value; // 通过索引或路径找到对应的条目
skill_config.effectConfig = effectPlayableAsset.Config; // 补充详细配置
// 如果找到了对应的状态配置(target 不为空),则开始清理无效或过时的配置项
if (target != null)
{// 从后往前遍历 target 列表(避免在遍历时因删除元素导致索引错乱)for (int i = target.Count - 1; i >= 0; i--){// 如果当前配置的资源路径为空,或者不在当前配置字典 skill_dct 中(即已失效或不再需要)if (string.IsNullOrEmpty(target[i].res_path) || skill_dct.ContainsKey(target[i].res_path) == false){target.Remove(target[i]); // 从列表中移除该无效配置}}
}

3、添加临时列表,确认需要是更新还是新增列表(特效配置表)

// 准备一个临时列表,用于存放需要新增的特效配置
List<EffectConfig> wait_add = new List<EffectConfig>();// 遍历当前技能字典 skill_dct(Key: 资源路径, Value: 包含 EffectConfig 的结构)
foreach (var item in skill_dct)
{bool add = true; // 标记是否需要新增该配置,默认为 true// 遍历当前 target 列表,检查是否已存在相同资源路径的配置for (int i = 0; i < target.Count; i++){// 如果已存在相同 res_path 的配置,则更新其内容if (target[i].res_path == item.Key){target[i] = item.Value.effectConfig; // 用新配置覆盖旧配置add = false; // 标记为“无需新增”break; // 找到匹配项后可跳出内层循环——因为将特效表配置到配置面板上需要跳出循环}}// 如果未找到匹配项(add 仍为 true),则将该配置加入待添加列表if (add){wait_add.Add(item.Value.effectConfig);}
}// 如果有待添加的新配置,则一次性添加到目标列表中
if (wait_add.Count > 0)
{target.AddRange(wait_add.ToArray()); // 使用 ToArray() 是为了兼容某些旧版 List 实现(非必需,可直接 AddRange(wait_add))
}

这里设计wait_add的目的:避免在遍历 target 列表的同时修改它

  • 但 逻辑上是危险的 —— 因为下一次循环中,target.Count 会变大,如果你在内层 for 循环中依赖 target.Count,就可能出错(虽然你这里没有)。
foreach (var item in skill_dct)
{bool add = true;for (int i = 0; i < target.Count; i++){………………}if (add){// ❌ 如果这里直接写:target.Add(item.Value.effectConfig);// 会导致什么问题?}
}

为什么需要在这里 break

假设 target 有 1000 个元素,而你在第 3 个就找到了匹配项:

  • 如果 不加 break → 你还会继续从第 4 个遍历到第 1000 个,做 997 次无意义的比较。
  • 如果 加了 break → 立即跳出,节省 997 次判断。

4、将外部配置好的列表传递给该状态

在状态配置中找到指定 state_id 对应的状态(如1005),然后用新的 target(特效配置)和 move_config(物理运动配置)更新它。

// 遍历当前单位的所有状态配置(anmConfig.states),寻找与指定 state_id 匹配的状态
for (int i = 0; i < anmConfig.states.Count; i++)
{// 如果当前状态的 ID 与目标 state_id 匹配if (state_id == anmConfig.states[i].id){// 将外部处理好的特效配置列表(target)赋值给该状态的 effectConfigs,完成更新anmConfig.states[i].effectConfigs = target;// 清空该状态原有的物理运动配置(如位移、速度、转向等)anmConfig.states[i].physicsConfig.Clear();// 如果有新的运动配置(move_config)需要应用,则将其全部添加到物理配置列表中if (move_config.Count > 0){anmConfig.states[i].physicsConfig.AddRange(move_config);}// 可选:找到匹配项后可 break,避免不必要的后续遍历(性能优化)// break;}
}

SkillEditor_EffectItemConfig

class SkillEditor_EffectItemConfig
{public GameObject obj;//表示这个效果项关联的 游戏对象(GameObject)。public double trigger;//表示触发该效果的 时机或条件public string res_path;//表示该效果资源的 资源路径public EffectConfig effectConfig;//表示该效果的 详细配置数据,是一个自定义结构或类 EffectConfig}

EffectPlayableAsset

EffectPlayableAsset 是一个 Timeline Clip 的“数据模板”,它:

  • 存储了播放“特效”所需的配置数据(如持续时间、特效预制体、参数等)。
  • 在 Timeline 播放时,被用来创建一个实际的“可播放行为”(EffectBehaviour),该行为控制特效如何播放。
  • 支持在 Timeline 编辑器中像普通 Clip 一样拖拽、剪辑、排列。

PlayableAsset:表示这是一个 Timeline 可播放资源的“资产模板”。

ITimelineClipAsset:实现这个接口后,该 Asset 就能被 Timeline 当作 Clip 使用(可拖入轨道)。

  • EffectBehaviour 应该是一个继承自 PlayableBehaviour 的类,用于定义播放时每帧执行的逻辑(如播放特效、修改参数等)。
    • _EffectBehaviour 是作为“模板”存在的,真正的播放行为由每次 CreatePlayable 时创建的克隆体执行,避免多个 Clip 共用同一个实例导致数据污染。
  • EffectConfig 是你自定义的数据结构,用于配置该特效播放的各种参数(如持续时间、粒子系统、音效等)。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;    // 用于创建可播放内容(Playable)
using UnityEngine.Timeline;     // 用于与 Timeline 系统集成// 标记该类可序列化,使其能在 Inspector 中显示并保存数据
[System.Serializable]
// 继承 PlayableAsset:表示这是一个 Timeline 中可播放的资源
// 实现 ITimelineClipAsset 接口:使该 Asset 能作为 Timeline Clip 使用
public class EffectPlayableAsset : PlayableAsset, ITimelineClipAsset
{// 私有字段,用于存储默认的 EffectBehaviour 实例(模板)// 注意:这个实例不会直接用于播放,而是作为“原型”在 CreatePlayable 时被克隆private readonly EffectBehaviour _EffectBehaviour = new EffectBehaviour();// 用于存储该 Clip 的配置数据,由用户在 Timeline 编辑器中设置public EffectConfig Config;// 重写 CreatePlayable 方法:Timeline 在播放时会调用此方法创建实际的 Playable 实例public override Playable CreatePlayable(PlayableGraph graph, GameObject owner){// 创建一个 ScriptPlayable<EffectBehaviour> 实例// 它包装了 _EffectBehaviour 的一个“副本”(实际上是深拷贝行为数据)var b = ScriptPlayable<EffectBehaviour>.Create(graph, _EffectBehaviour);// 获取该 Playable 实例内部的行为对象(即 EffectBehaviour 的克隆体)var clone = b.GetBehaviour();// 将配置数据赋值给克隆体,确保播放时使用的是当前 Clip 的配置clone.Config = Config;// 注释掉的代码:原本可能用于绑定某个 Transform(如结束位置)// clone.EndTransform = endTrans.Resolve(graph.GetResolver());// 返回创建好的 Playable,Timeline 系统将用它来播放return b;}// 实现 ITimelineClipAsset.clipCaps 属性// ClipCaps.None 表示该 Clip 不支持混合、循环等高级功能(保持默认行为)// 可根据需要改为 ClipCaps.Blending、ClipCaps.Looping 等public ClipCaps clipCaps => ClipCaps.None;
}

1、私有模板行为

private readonly EffectBehaviour _EffectBehaviour = new EffectBehaviour();
  • 这是一个“原型”或“模板”行为实例。
  • 不会直接用于播放!仅用于在 CreatePlayable 时被“克隆”。
  • Unity 的 ScriptPlayable<T>.Create(graph, template) 会复制 template 的字段值(浅拷贝),创建一个新的行为实例供播放使用。
  • 使用 readonly 是为了防止意外修改这个模板。

2、创建实际播放实例

public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)

这是 Timeline 播放系统在每次需要播放这个 Clip 时调用的方法。

 ➤ 创建 Playable 行为实例:

var b = ScriptPlayable<EffectBehaviour>.Create(graph, _EffectBehaviour);
ScriptPlayable<T>.Create(graph, template)1.ScriptPlayable<T> 是 Unity 提供的一个泛型结构体,用于创建和包装一个自定义的 PlayableBehaviour(即你的 EffectBehaviour)
————表示这个 ScriptPlayable 将包装并执行 EffectBehaviour 类型的逻辑2.参数 (graph, _EffectBehaviour)
graph (类型: PlayableGraph)
这是你希望将新创建的 Playable 节点添加到的 播放图。
所有 Timeline 轨道和 Clip 的 Playable 节点最终都会被添加到同一个 PlayableGraph 中,由它来统一调度和执行。_EffectBehaviour (类型: EffectBehaviour)
这是你的 行为模板 (template) 或 原型 (prototype)。
Create 方法会以这个 _EffectBehaviour 实例为蓝本,拷贝其字段的值(浅拷贝),来初始化新创建的 EffectBehaviour 实例。可以把它理解为:告诉工厂,“用这个模板(_EffectBehaviour)生产一个节点,并把它放到这个舞台(graph)上”
  • 基于 _EffectBehaviour 模板,创建一个新的 ScriptPlayable<EffectBehaviour> 实例。
  • 这个实例是 Timeline 播放图(PlayableGraph)中的一个节点,负责实际播放逻辑。
    • Timeline 本身不直接执行逻辑,它只是一个“调度器”。
    • Timeline 通过 PlayableGraph 来管理所有 Clip 的播放顺序、混合、同步等,而每个 Clip 的“实际行为”必须由一个 Playable 节点 来实现

Timeline相关参数:

【Unity Blog】第二十五 · Timeline 结构及其源码浅析_unity timeline-CSDN博客


Q1:为什么不能直接用 _EffectBehaviour 模板?

  • Timeline 是多实例系统
    同一个 EffectPlayableAsset 可能被用在 Timeline 的多个地方,甚至多个 Timeline 同时播放。
    → 如果所有播放都共享同一个 _EffectBehaviour 实例,会导致状态冲突(比如一个播放完了把另一个的状态也清空了)。

  • PlayableGraph 要求每个节点是独立的
    Unity 的播放系统需要每个 Clip 在播放图中拥有自己独立的状态(播放时间、暂停状态、混合权重等),所以必须为每个 Clip 创建独立的 Playable 实例。

➤ 获取行为对象并注入配置:

var clone = b.GetBehaviour();
clone.Config = Config;
  • GetBehaviour() 获取 Playable 内部封装的 EffectBehaviour 实例(即“克隆体”)。
    • 所以你需要通过 b.GetBehaviour() 来获取这个“真正的行为对象”。
  • 把当前 Clip 的配置 Config 赋值给它 → 确保播放时使用的是这个 Clip 的专属设置

举例说明——b.GetBehaviour();

假设你的 EffectBehaviour 是这样:

public class EffectBehaviour : PlayableBehaviour
{public string EffectName; // 比如 “爆炸”、“闪光”public float Duration;
}

而你的模板是:

private readonly EffectBehaviour _EffectBehaviour = new EffectBehaviour()
{EffectName = "flash",Duration = 1.0f
};

当你调用:

var b = ScriptPlayable<EffectBehaviour>.Create(graph, _EffectBehaviour);
var clone = b.GetBehaviour();
clone.EffectName = "Big Explosion!";//给克隆出来的模板重新赋值
clone.Duration = 3.0f;

        这里的机制是因为特效轨道可以复用——所以使用模板进行克隆并进行赋值

➤ 返回 Playable:

return b;
  • 把创建好的 Playable 返回给 Timeline 系统,Timeline 会负责调度它在正确的时间播放

3、固定播放能力

public ClipCaps clipCaps => ClipCaps.None;

ClipCaps 是 Unity Timeline 系统中的一个 枚举(Enum),用于描述一个 Timeline Clip 支持哪些“高级播放能力”。

[Flags]
public enum ClipCaps
{None     = 0,       // 不支持任何特殊能力Blending = 1 << 0,  // 支持混合(淡入淡出、权重混合)Looping  = 1 << 1,  // 支持循环播放// 可能还有其他...
}

public ClipCaps clipCaps
{get { return ClipCaps.None; }
}

这意味着:我这个 Clip 不支持任何高级功能 —— 不能混合、不能循环,就老老实实从头播到尾

EffectTrackAsset

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;[TrackColor(0.855f, 0.8623f, 0.87f)]
[TrackClipType(typeof(EffectPlayableAsset))]
public class EffectTrackAsset : TrackAsset
{}

[TrackColor(...)]

  • 设置该轨道在 Timeline 编辑器中的显示颜色
  • 参数是 RGB 颜色值(范围 0~1),这里是浅灰色调:(0.855f, 0.8623f, 0.87f)

[TrackClipType(typeof(EffectPlayableAsset))]

  • 指定该轨道只能添加某种类型的 Clip
  • EffectPlayableAsset 是你(或团队)自定义的 Playable Asset 类型,用于控制某种特效(如粒子、屏幕特效、音效等)。

 public class EffectTrackAsset : TrackAsset

  • 继承自 TrackAsset,这是 Unity Timeline 中所有轨道的基础类。
  • 表示这是一个可以在 Timeline 上创建和使用的轨道资产。

EffectBehaviour

public class EffectBehaviour : PlayableBehaviour
{public EffectConfig Config;public override void OnBehaviourPause(Playable playable, FrameData info){base.OnBehaviourPause(playable, info);}public override void OnBehaviourPlay(Playable playable, FrameData info){base.OnBehaviourPlay(playable, info);}public override void OnGraphStart(Playable playable){base.OnGraphStart(playable);}public override void OnGraphStop(Playable playable){base.OnGraphStop(playable);}public override void OnPlayableCreate(Playable playable){base.OnPlayableCreate(playable);}public override void OnPlayableDestroy(Playable playable){base.OnPlayableDestroy(playable);}public override void PrepareData(Playable playable, FrameData info){base.PrepareData(playable, info);}public override void PrepareFrame(Playable playable, FrameData info){base.PrepareFrame(playable, info);}public override void ProcessFrame(Playable playable, FrameData info, object playerData){base.ProcessFrame(playable, info, playerData);//每一帧 处理//按移动的类型 让对应的特效 进行移动//检测它是否命中目标//Debug.LogError($"帧变动..:{playable.GetTime()}");//if (Config != null && Config.res_path != null) {//    var go = GameObject.Find(Config.res_path);//}}
}

该脚本类的核心方法集合如下:

通过继承 PlayableBehaviour,你可以重写以下关键方法,精确控制特效在 Timeline 中的行为:OnGraphStart     // 整个 Graph 开始时调用(全局)
OnGraphStop      // 整个 Graph 停止时调用
OnPlayableCreate // Playable 节点被创建时
OnPlayableDestroy// Playable 节点被销毁时
OnBehaviourPlay  // 该 Clip 开始播放时
OnBehaviourPause // 该 Clip 被暂停时
PrepareFrame     // 每帧准备阶段
ProcessFrame     // 每帧处理阶段(最常用)
方法名调用者调用时机说明
OnPlayableCreatePlayableGraph当 Graph 创建该 Playable 节点时调用(通常在 Timeline 开始播放前)
OnGraphStartPlayableGraph整个 PlayableGraph 开始播放时调用(全局一次,所有节点都会收到)
OnBehaviourPlayPlayable 系统当前 Clip(对应这个 Behaviour)开始播放时调用(比如 Timeline 播放到这一轨)
PrepareFramePlayable 系统(每帧)在每一帧的“准备阶段”调用,适合做数据准备、状态同步
ProcessFramePlayable 系统(每帧)在每一帧的“处理阶段”调用,适合执行主要逻辑(如移动、检测、播放特效等)
OnBehaviourPausePlayable 系统当前 Clip 被暂停或跳过时调用(例如 Timeline 暂停、跳转、Clip 结束)
OnGraphStopPlayableGraph整个 Graph 停止播放时调用(全局一次)
OnPlayableDestroyPlayableGraphPlayable 节点被销毁时调用(资源清理的好地方

EffectBehaviour与EffectPlayableAsset共同作用

你的设计中:

  • EffectBehaviour 负责运行时行为逻辑(每帧做什么);
  • EffectPlayableAsset 负责编辑器配置和实例化(把配置数据传给 Behaviour);

这是一种经典的“数据驱动行为”模式:

var clone = b.GetBehaviour();
clone.Config = Config; // 将配置数据注入运行时行为

为什么继承 PlayableBehaviour 是“正确做法”

继承 PlayableBehaviour 不是“有什么优势”,而是“必须这么做” —— 它是接入 Unity Timeline/Playable 系统的唯一标准方式。
优势点说明
符合系统架构Unity Playable 系统强制要求,否则无法使用 ScriptPlayable
生命周期管理自动获得播放、暂停、帧更新等回调,精准控制行为
数据行为分离Asset 配置 + Behaviour 执行,结构清晰,易于维护
编辑器集成配置可序列化,在 Timeline 中直观编辑
易于扩展新功能只需扩展现有类,不影响整体结构
性能与同步与 Timeline 时间轴精确同步,避免自己写 Update 带来的不同步或性能问题

SkillHitController(更新后)

目前似乎是敌人的攻击调用这个方法——检查玩家状态进行代码操作

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SkillHitController : MonoBehaviour
{EffectService effectService;EffectConfig effectConfig;PlayerState state;SkillEffect skillEffect;int hit_unit_max;//击中最多次数(New)//设定某一特定角色,能命中的次数(New)Dictionary<int, int> hit_objs = new Dictionary<int, int>();public void Init(EffectService effectService, EffectConfig effectConfig, PlayerState state, SkillEffect skillEffect){this.effectService = effectService;this.effectConfig = effectConfig;this.state = state;this.skillEffect = skillEffect;//击中次数设定——如果特效配置表中写入了数值则用设定的数值,没有则设定击中次数为1hit_unit_max = effectConfig.unit_hit_count == 0 ? 1 : effectConfig.unit_hit_count;hit_objs.Clear();}public void OnTriggerEnter(Collider other){var fsm = other.GetComponent<FSM>();if (fsm != null){检查 hit_objs 字典中是否已经记录过这个目标,如果没有,初始化命中次数为 0if (hit_objs.ContainsKey(fsm.instance_id) == false){hit_objs[fsm.instance_id] = 0;}if (fsm.IsBlockState()){//判断下前后方向if (fsm._transform.ForwardOrBack(effectService.player._transform.position) > 0){var d = Vector3.Distance(fsm._transform.position, effectService.player._transform.position);if (d <= state.skill.block_distance){fsm.OnBlockSucces(effectService.player);effectService.player.BeBlock(fsm);//1.生成格挡时特效var blockEffect = ResourcesManager.Instance.Create_Skill(CombatConfig.Instance.Config().block_effect);if (blockEffect != null){/*blockEffect.transform.position = fsm.GetBlockEffectPoint();*/blockEffect.transform.forward = fsm.transform.forward;}//镜头模糊控制 /*GameEvent.DORadialBlur?.Invoke(CombatConfig.Instance.Config().block_radialBlur);*///顿帧GameEvent.DOHitlag?.Invoke(CombatConfig.Instance.Config().block_hitlag.frame,CombatConfig.Instance.Config().block_hitlag.lerp);//放格挡成功的音效AudioController.Instance.Play(CombatConfig.Instance.Config().block_audio, blockEffect.transform.position);skillEffect.DODestroy();return;}}}if (hit_objs[fsm.instance_id] < hit_unit_max){hit_objs[fsm.instance_id] += 1;if (other.gameObject.layer != effectService.player._gameObject.layer){if (effectConfig.hit_effect_count == 0 || skillEffect.hit_effect_count < effectConfig.hit_effect_count){//生成命中的特效var hit_effect = ResourcesManager.Instance.Create_Skill(CombatConfig.Instance.GetHitEffectPath(effectConfig.hit_effect));hit_effect.transform.position = fsm.hit_target.transform.position;hit_effect.transform.forward = this.transform.position - fsm.hit_target.transform.position;skillEffect.hit_effect_count += 1;}//敌方扣血var damage = AttHelper.Instance.Damage(effectService.player, state, fsm);fsm.UpdateHP_OnHit(damage);var fb = fsm._transform.ForwardOrBack(this.transform.position) > 0 ? 0 : 1;if (fsm.att_crn.hp > 0){if (state.skill.add_fly != null){//击飞的流程fsm.OnBash(fb, effectService.player, state.skill.add_fly, this.transform.position);}else{fsm.OnHit(fb, effectService.player);}}else{fsm.OnDeath(fb);}//命中时的顿帧this.effectService.player.Attack_Hitlag(state);//6.命中的音效AudioController.Instance.Play(effectConfig.hit_audio, this.transform.position);if (hit_objs.Count >= effectConfig.destroy_hit_count){skillEffect.DODestroy();}}}}}}

格挡相关逻辑

        这里需要了解这段代码的前提——攻击检测的碰撞体与特效生成机制结合在一起了,所以对敌人的攻击需要进行额外的格挡机制

这段代码的格挡机制是玩家格挡敌人的攻击

代码逻辑如下:

1、先给敌人的攻击添加该脚本

2、如果敌人的攻击碰撞器与玩家角色碰撞器重合则判断玩家是否处于格挡状态

if (fsm.IsBlockState())

3、判断玩家与发动该攻击的角色前后位置关系与身位距离——如果达到攻击范围且格挡成功,敌人则进入被格挡状态,执行被格挡状态接口的操作

函数fsm.GetBlockEffectPoint();——获取格挡特效挂点位置

就是对于这种新情况的特定脚本,出现特定方法和特定参数(因为原有格挡机制的位置参数hitInfo.point;在这里不便使用)

 public void OnTriggerEnter(Collider other){var fsm= other.GetComponent<FSM>();if (fsm!=null){…………if (fsm.IsBlockState())//判断玩家是否处于格挡状态{//判断下前后方向if (fsm._transform.ForwardOrBack(effectService.player._transform.position)>0){var d = Vector3.Distance(fsm._transform.position, effectService.player._transform.position);if (d<= state.skill.block_distance){fsm.OnBlockSucces(effectService.player);effectService.player.BeBlock(fsm);//1.生成格挡时特效var blockEffect = ResourcesManager.Instance.Create_Skill(CombatConfig.Instance.Config().block_effect);if (blockEffect != null){blockEffect.transform.position = fsm.GetBlockEffectPoint();blockEffect.transform.forward = fsm.transform.forward;}//顿帧                        GameEvent.DOHitlag?.Invoke(CombatConfig.Instance.Config().block_hitlag.frame,CombatConfig.Instance.Config().block_hitlag.lerp);//放格挡成功的音效                        AudioController.Instance.Play(CombatConfig.Instance.Config().block_audio, blockEffect.transform.position);skillEffect.DODestroy();return;}}}

击中次数相关逻辑

hit_objs[fsm.instance_id]这个字典是用来存储当前目标的击中次数:比如玩家1001

其特有的instance_id作为Key,击中次数为Value(hit_objs[fsm.instance_id])

int hit_unit_max;public void OnTriggerEnter(Collider other){var fsm = other.GetComponent<FSM>();if (fsm != null){检查 hit_objs 字典中是否已经记录过这个目标,如果没有,初始化命中次数为 0if (hit_objs.ContainsKey(fsm.instance_id) == false){hit_objs[fsm.instance_id] = 0;}………………//格挡相关//判断当前目标是否还能被命中。if (hit_objs[fsm.instance_id] < hit_unit_max){hit_objs[fsm.instance_id] += 1;//记录当前目标击中次数(多击中一次,新增一次)if (other.gameObject.layer != effectService.player._gameObject.layer)//防止同阵营互相攻击{  //控制特效数量(配置表中特效数量无限制或者已播放特效的次数满足(针对数量有限的情况))if (effectConfig.hit_effect_count == 0 || skillEffect.hit_effect_count < effectConfig.hit_effect_count){//生成命中的特效var hit_effect = ResourcesManager.Instance.Create_Skill(CombatConfig.Instance.GetHitEffectPath(effectConfig.hit_effect));hit_effect.transform.position = fsm.hit_target.transform.position;hit_effect.transform.forward = this.transform.position - fsm.hit_target.transform.position;skillEffect.hit_effect_count += 1;}
 if (effectConfig.hit_effect_count == 0 || skillEffect.hit_effect_count < effectConfig.hit_effect_count)

前者意味着命中特效为数量无限制的情况下

[Header("命中时特效数量:0无限制 1只创建一次")]
public int hit_effect_count;

后者是数量有限制,且未到达播放次数

skillEffect.hit_effect_count < effectConfig.hit_effect_count)

锁定玩家相关代码

我们先确立atk_fsm是什么:

atk_fsm(以及相关的 atk_target既可以指玩家,也可以指敌人,具体取决于当前 FSM 实例是谁

  • 当 敌人 被玩家攻击时,敌人的 atk_fsm 会指向玩家(攻击者)。
  • 当 玩家 被敌人攻击时,玩家的 atk_fsm 会指向那个敌人(攻击者)。

这段代码功能场景如下:

多个敌人角色技能攻击玩家,出现atk_fsm._transform 报空的情况——可能有多个因素,比如共享一个atk_fsm引用,玩家fsm还未初始化

它在ForceLockPlayer()中就避免了atk_fsm == null为空的情况,并且一定会返回玩家的fsm

 public void ForceLockPlayer(){if (atk_fsm == null){atk_fsm = UnitManager.Instance.player;}}
public FSM GetAtkTarget(){ForceLockPlayer();return atk_fsm;}

召唤机制相关代码

SummonService 

  • OnBegin:重置所有召唤执行标记,准备开始召唤流程。
  • OnUpdate:根据动画归一化时间,按配置在指定时机召唤 NPC,并设置其位置、朝向、目标和状态。
  • OnEnd:状态正常结束时的回调(当前无额外逻辑)。
  • OnDisable:状态被中断或禁用时的回调(当前无额外逻辑)。
  • OnAnimationEnd:动画播放结束时的回调(当前无额外逻辑)。
  • ReStart:状态重新开始时的回调(当前无额外逻辑)。
  • ReLoop:状态循环时的回调(当前无额外逻辑)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SummonService : FSMServiceBase
{public override void OnAnimationEnd(PlayerState state){base.OnAnimationEnd(state);}public override void OnBegin(PlayerState state){base.OnBegin(state);ReSetAllExcuted();}public override void OnDisable(PlayerState state){base.OnDisable(state);}public override void OnEnd(PlayerState state){base.OnEnd(state);}public override void OnUpdate(float normalizedTime, PlayerState state){base.OnUpdate(normalizedTime, state);var config = state.stateEntity.summonConfigs;if (config != null && config.Count > 0){for (int i = 0; i < config.Count; i++){if (GetExcuted(i) == true){continue;}var item = config[i];if (normalizedTime >= item.trigger){for (int j = 0; j < item.count; j++){if (UnitManager.Instance.CanCreate(item.npc_id, item.max_count) == false){SetExcuted(i);break;}var fsm = UnitManager.Instance.CreateNPC(item.npc_id);if (fsm != null){//设置位置 角度fsm._transform.position = this.player._transform.GetOffsetPoint(UnityEngine.Random.Range(item.distance_min,item.distance_max), UnityEngine.Random.Range(item.angle_min, item.angle_max));fsm._transform.forward = this.player._transform.forward;fsm.SetAtkTarget(this.player.GetAtkTarget());if (item.state != 0){fsm.ToNext(item.state);}}}SetExcuted(i);}}}}public override void ReLoop(PlayerState state){base.ReLoop(state);}public override void ReStart(PlayerState state){base.ReStart(state);}
}

OnUpdate

public override void OnUpdate(float normalizedTime, PlayerState state)
{    base.OnUpdate(normalizedTime, state);// 获取当前状态绑定的召唤配置列表var config = state.stateEntity.summonConfigs;// 如果配置存在且不为空,则尝试执行召唤逻辑if (config != null && config.Count > 0){// 遍历每一个召唤项(每个项代表一个召唤时机和规则)for (int i = 0; i < config.Count; i++){// 如果该项已经执行过,则跳过,避免重复召唤if (GetExcuted(i) == true){continue;}var item = config[i];// 检查当前动画播放进度是否达到该召唤项的触发时间点if (normalizedTime >= item.trigger){// 根据配置,尝试召唤指定数量(item.count)的 NPCfor (int j = 0; j < item.count; j++){// 检查是否还能创建该类型的 NPC(受全局最大数量限制)if (UnitManager.Instance.CanCreate(item.npc_id, item.max_count) == false){// 若已达上限,标记该项为已执行并跳出循环,不再继续创建SetExcuted(i);break;}// 创建一个 NPC 实例var fsm = UnitManager.Instance.CreateNPC(item.npc_id);if (fsm != null){// 设置 NPC 出生位置:在玩家前方随机距离和角度的偏移点fsm._transform.position = this.player._transform.GetOffsetPoint(UnityEngine.Random.Range(item.distance_min, item.distance_max),   // 随机距离UnityEngine.Random.Range(item.angle_min, item.angle_max)          // 随机角度(相对于玩家朝向));// 使 NPC 朝向与玩家一致fsm._transform.forward = this.player._transform.forward;// 设置 NPC 的攻击目标为玩家当前的攻击目标fsm.SetAtkTarget(this.player.GetAtkTarget());// 如果配置指定了初始状态(非0),则让 NPC 立即切换到该状态if (item.state != 0){fsm.ToNext(item.state);}}}// 无论是否成功创建所有 NPC,只要触发时间到达,就标记该项为已执行SetExcuted(i);}}}
}

UnitManager

public class UnitManager
{static UnitManager instance = new UnitManager();public static UnitManager Instance => instance;public FSM player;public GameObject CreatePlayer(){…………//Main初运行时创建角色,不参与召唤机制}//npc参数管理召唤物的FSM组件(即FSM的相关内容)Dictionary<int, List<FSM>> npc = new Dictionary<int, List<FSM>>();public bool CanCreate(int id, int max){if (npc.TryGetValue(id, out var v)){return v.Count < max;}return true;}public void RemoveNPC(FSM fsm){if (npc.TryGetValue(fsm.id, out var v)){v.Remove(fsm);}}public FSM CreateNPC(int id){var config = Game.Config.UnitData.Get(id);if (config != null){var go = ResourcesManager.Instance.Instantiate<GameObject>(config.prefab_path);var fsm = go.GetComponent<FSM>();fsm.AI = true;if (npc.ContainsKey(id) == false){npc[id] = new List<FSM>();}npc[id].Add(fsm);return fsm;}return null;}}

CreateNPC

1、获取对应单位配置面板数据,并且根据路径创建实例

2、每创建一个单位就获取该单位的FSM组件,并存入全局字典npc中

3、记录了fsm后,将该组件返回到调用方

public FSM CreateNPC(int id)
{// 1. 从全局配置表中查找该ID对应的配置面板数据var config = Game.Config.UnitData.Get(id);// 2. 如果配置不存在,说明该ID无效,直接返回nullif (config != null){// 3. 从资源管理器加载并实例化对应的Prefab(GameObject)var go = ResourcesManager.Instance.Instantiate<GameObject>(config.prefab_path);// 4. 获取该GameObject上的FSM组件(有限状态机,控制AI行为)var fsm = go.GetComponent<FSM>();// 5. 标记该单位为AI控制(区别于玩家角色)fsm.AI = true;// 6. 如果该ID类型的NPC列表尚未创建,则初始化一个空列表if (npc.ContainsKey(id) == false){npc[id] = new List<FSM>();}// 7. 将新创建的FSM实例加入对应ID的列表中,用于后续数量统计和管理npc[id].Add(fsm);// 8. 返回FSM组件,供调用方(如SummonService)设置位置、目标、状态等return fsm;}// 配置不存在,创建失败return null;
}

Q1:为什么用 List<FSM> 而不是 List<GameObject> 或直接计数?

A1:

1. FSM 是行为控制的核心

  • 在这个架构中,每个单位(无论是玩家还是NPC)的行为逻辑都由 FSM(有限状态机)组件驱动。
  • 后续需要对单位进行操作时(如:
    • 设置攻击目标(fsm.SetAtkTarget(...)),
    • 切换状态(fsm.ToNext(...)),
    • 销毁时通知管理器(RemoveNPC(fsm))), 直接持有 FSM 引用比持有 GameObject 更高效、更语义清晰

2. 便于精确移除和生命周期管理

  • 当 NPC 死亡或被销毁时,通常会在 FSM 的某个状态(如“死亡”)中调用:
    UnitManager.Instance.RemoveNPC(this); // this 就是 FSM 自身
  • 如果列表存的是 GameObject,就需要额外通过 GetComponent<FSM>() 才能匹配,或者在销毁时难以准确找到对应条目。
  • 而 FSM 组件的引用是唯一的、稳定的,且与单位生命周期一致。

3. 避免重复或无效引用

  • GameObject 可能被销毁(Destroy(go)),但如果你只存了 GameObject 引用,之后访问会报错或为空。
  • 而在设计良好的系统中,FSM 组件会在单位销毁前主动调用 RemoveNPC,确保列表中不会残留无效引用。
  • 即便如此,存 FSM 也比存 GameObject 更贴近“逻辑实体”而非“渲染实体”。

4. 数量统计的本质是“活跃行为体”的数量

  • max_count 限制的不是“有多少个模型”,而是“有多少个正在运行AI逻辑的单位”。
  • FSM 正是这个“活跃行为体”的代表。一个没有 FSM 的 GameObject 只是个空壳。

5. 性能与一致性

  • 后续如 CanCreateRemoveNPC 等方法都围绕 FSM 设计,保持数据结构一致,减少类型转换和查找开销。

CanCreate

根据SummonService传递来的面板——最多召唤人数max。

判断是否还能继续生成角色

 //npc参数管理召唤物的FSM组件(即FSM的相关内容)Dictionary<int, List<FSM>> npc = new Dictionary<int, List<FSM>>();public bool CanCreate(int id, int max){if (npc.TryGetValue(id, out var v)){return v.Count < max;}return true;}

前后摇机制——CheckConfig(FSM)更新

public bool overwrite_atk;     // 是否使用 Unity 编辑器中设置的前后摇参数(true),还是用外部传入的 config(false)
public float atk_before;       // 攻击前摇时长(归一化时间,0~1)
public float atk_after;        // 攻击后摇开始时间(归一化时间,0~1)

情况一:overwrite_atk == true(使用 Unity 编辑器中的参数)

if ((animationService.normalizedTime >= 0 && animationService.normalizedTime <= atk_before)|| animationService.normalizedTime >= atk_after)
{return true;
}

情况二:overwrite_atk == false(使用外部 config 参数)

if ((animationService.normalizedTime >= 0 && animationService.normalizedTime <= config[0])|| animationService.normalizedTime >= config[1])
{return true;
}
http://www.dtcms.com/a/423618.html

相关文章:

  • 太原网站搭建推广服装设计网站模板下载
  • 人工智能-机器学习day3
  • 第四部分:VTK常用类详解(第113章 vtkTensorGlyph张量符号化类)
  • 中国平安官方网站心态建设课件做网站的学校
  • 翻译插件分享
  • 网页设计广州网站购物型网站用dw做
  • 水平扩展与垂直扩展
  • React基础到进阶
  • cvat使用
  • 东莞小学网站建设空间设计说明怎么写
  • 万网网站后台管理系统网站策划招聘
  • 网站首页静态化代码网站建设架构选型
  • Stable Diffusion DALL-E Imagen背后共同套路
  • 网上商城html模板无锡seo关键词排名
  • 天津 网站策划湛江专门做网站
  • 【Linux】进程的概念和状态
  • 【完整源码+数据集+部署教程】无人机场景城市环境图像分割系统: yolov8-seg-timm
  • 鸿蒙NEXT WLAN服务开发指南:从STA模式到多设备联网实战
  • 网站建设开票项目是什么意思昭通做网站
  • 岳阳网站建设设计如何做网站家具导购
  • 做网站推广有什么升职空间怎么做才能设计出好的网站
  • ZStack Cloud v5.4.0 LTS让运维自动驾驶,让合规开箱即用
  • 10-RAG(Retrieval Augmented Generation)
  • S7-200 SMART 开放式用户通信(OUC)深度指南:TCP/ISO-on-TCP(上)
  • 03_交易的核心:我如何驾驭趋势与反趋势
  • 比较网站建设长春建设网站制作
  • 丢件预警!快递批量查询工具,未更新物流自动提醒,避免损失
  • 申请建设部门网站的报告用discuz做的门户网站
  • 厦门网站建设多少钱网站设计登录界面怎么做
  • FastAPI参数类型与请求格式详解:Query、Form、Body、File与Content-Type的对应关系