4 Commits

9 changed files with 991 additions and 10 deletions

View File

@@ -0,0 +1,548 @@
# 背景
文件名2025-12-01_1_performance_optimization_aggressive.md
创建于2025-12-01
创建者User
主分支main
任务分支task/performance_optimization_aggressive_2025-12-01_1
Yolo模式Off
# 任务描述
对Unity音游编辑器项目进行激进的、彻底的性能优化使用最先进的技术栈。项目限定在Windows 10+平台运行。
## 核心问题
基于Tracy Profiler分析结果
1. **SplineNode.ScriptUpdate**: 5.66ms (19.37%), 3,240次调用 - 函数调用开销过大
2. **递归Update模式**: 深度树形遍历导致缓存不友好
3. **剪枝不彻底**: 大量对象在非活跃时间范围内仍在Update
4. **Timeline UI更新**: 存在严重性能开销(已知问题)
## 性能目标
- 将SplineNode.ScriptUpdate从5.66ms优化到<0.5ms**10倍以上提升**
- 帧率从77fps提升到120fps+
- 消除不必要的Update调用减少70%+调用次数
# 项目概览
- **项目类型**: Unity音游编辑器
- **核心框架**: Convention框架 + RScript脚本系统基于FLEE
- **主要系统**:
- ScriptableObject树形更新系统
- Updatement<T>插值系统
- IInteraction多级判定系统
- SplineCore样条线渲染
- TimelineScriptObject时间轴对象
## 技术栈
- Unity 6.2 (6000.2.51)
- C# (支持最新特性)
- Tracy Profiler 0.11.1
- Windows 10+ 专用
⚠️ 警告:永远不要修改此部分 ⚠️
# RIPER-5核心规则摘要
- 必须在响应开头声明当前模式:[MODE: MODE_NAME]
- RESEARCH模式只读取和理解禁止建议和实施
- INNOVATE模式讨论多种方案禁止具体实施
- PLAN模式创建详尽技术规范必须转换为编号清单
- EXECUTE模式只实施已批准计划禁止偏离
- REVIEW模式验证实施与计划的完全一致性
关键原则:
- 未经明确许可不得在模式间转换
- EXECUTE模式必须100%忠实遵循计划
- 标记任何偏差,无论多小
- 使用中文响应(除模式声明和代码)
⚠️ 警告:永远不要修改此部分 ⚠️
# 分析
## 当前架构分析
### 1. Update调用链存在问题
```
GameController.Update()
└─ RootObject.ScriptUpdate()
└─ foreach (child in UpdateChilds)
└─ child.ScriptUpdate() // 递归3,240次
└─ child.UpdateTicks() // 虚函数调用
└─ 具体逻辑
```
**性能问题**
- 递归调用开销累积
- 虚函数调用开销
- 缓存不友好(对象内存分散)
- 无法并行化
- 大量对象在非活跃时间范围内仍被调用
### 2. 现有优化机制(不足)
代码中存在一些优化:
- `UpdatePerFrame`: 降低Update频率
- `IsEnableUpdate`: 静态剪枝(只能剪掉永远不更新的对象)
- Update树剪枝简化单子节点调用链
**不足之处**
- 无法剪枝"暂时不活跃"的对象
- 无法利用多核CPU
- 内存布局对缓存不友好
### 3. 技术约束
- Unity主线程限制Transform等API必须在主线程调用
- MonoBehaviour限制继承自MonoBehaviour的对象管理复杂
## 可用的先进技术
### Windows 10+ 专属优势
1. **SIMD指令集**: AVX2, AVX-512如果CPU支持
2. **现代CPU特性**: 更多核心,更好的缓存
3. **.NET 最新特性**: Span<T>, Memory<T>, stackalloc
4. **Unity DOTS**: ECS + Job System + Burst Compiler
### Unity DOTS技术栈
1. **ECS (Entity Component System)**
- 数据驱动架构
- 缓存友好的内存布局
- 易于并行化
2. **C# Job System**
- 托管的多线程系统
- 自动线程池管理
- 安全检查避免竞态条件
3. **Burst Compiler**
- 将C#编译为高度优化的机器码
- SIMD自动向量化
- 接近C/C++的性能
4. **Collections Package**
- NativeArray, NativeList等非托管集合
- 可在Job中安全使用
- 避免GC压力
### 性能提升潜力
| 技术 | 预期提升 | 适用场景 |
|------|---------|---------|
| 时间范围剪枝 | 3-5倍 | 所有TimelineObject |
| 扁平化调度 | 2-3倍 | Update调用链 |
| Burst编译 | 5-10倍 | 数学密集计算 |
| Job并行 | 2-4倍 | 批量数据处理 |
| 缓存优化 | 2-3倍 | 数据访问模式 |
| **综合** | **30-100倍** | 整体系统 |
# 提议的解决方案
## 方案概览
采用**混合架构**保留MonoBehaviour作为外壳内部使用ECS数据处理。
### 架构设计
```
传统层MonoBehaviour
↓ 数据同步
ECS数据层Burst Job处理
↓ 结果同步
传统层应用到Transform
```
## 核心方案要素
### 1. 时间分片调度系统
**目标**: 只更新活跃时间范围内的对象
**实现**:
- 使用SortedSet维护时间排序
- 激活/停用事件驱动
- 扁平化对象列表(无递归)
**预期**: 减少70%无效Update调用
### 2. ECS化核心系统
#### 2.1 Updatement系统最适合ECS
- 纯数据Entry列表、插值参数
- 纯计算Lerp、曲线求值
- 可完全Burst编译和并行化
#### 2.2 Interaction判定系统
- 输入处理可并行化
- 判定计算可Burst优化
- 结果批量应用
#### 2.3 SplineNode计算
- 样条线计算适合SIMD
- 批量计算所有节点位置
- Burst编译获得巨大提升
### 3. Burst编译的Job
```csharp
[BurstCompile]
public struct UpdatementCalculationJob : IJobParallelFor
{
[ReadOnly] public float CurrentTime;
[ReadOnly] public NativeArray<UpdatementEntry> Entries;
[ReadOnly] public NativeArray<int> ContentIndices;
[WriteOnly] public NativeArray<float3> Results;
public void Execute(int index)
{
// 纯数学计算Burst自动SIMD优化
int content = ContentIndices[index];
float percent = CalculatePercent(CurrentTime, Entries, content);
Results[index] = math.lerp(
Entries[content].Position,
Entries[content + 1].Position,
EvaluateCurve(percent, Entries[content].CurveType)
);
}
}
```
### 4. 内存布局优化
**SoA (Structure of Arrays) 代替 AoS (Array of Structures)**:
```csharp
// 坏AoS缓存不友好
struct UpdatementData {
float timePoint;
Vector3 position;
EaseCurveType curve;
}
UpdatementData[] data; // 数据交错
// 好SoA缓存友好
struct UpdatementDataSoA {
NativeArray<float> timePoints; // 连续
NativeArray<float3> positions; // 连续
NativeArray<byte> curveTypes; // 连续
}
```
### 5. 对象池系统
- 避免GC压力
- 重用NativeArray
- 预分配Job句柄
## 实施分层
### Phase 1: 基础设施1周
1. 时间分片调度器
2. UpdateScheduler基类
3. 基础性能测试框架
### Phase 2: ECS核心2周
1. Updatement系统ECS化
2. Burst Job实现
3. 数据同步机制
### Phase 3: 扩展优化2周
1. SplineNode优化
2. Interaction判定优化
3. 对象池和内存管理
### Phase 4: 深度优化1周
1. SIMD手动优化关键路径
2. 内存布局调优
3. 性能剖析和微调
## 技术细节
### Job依赖管理
```csharp
// 计算Job
JobHandle calcHandle = calculationJob.Schedule(count, 64);
// 应用Job依赖计算Job
JobHandle applyHandle = applyJob.Schedule(calcHandle);
// 等待完成
applyHandle.Complete();
```
### 安全检查
Unity Job System提供编译时和运行时安全检查
- 检测数据竞争
- 验证NativeArray生命周期
- 防止悬空指针
### Profiler集成
保持Tracy Profiler集成
```csharp
using (Profiler.BeginZone("BurstJob.Schedule"))
{
handle = job.Schedule(count, 64);
}
```
## 风险与挑战
### 技术风险
1. **学习曲线**: ECS和Burst需要学习
2. **调试难度**: Burst代码调试较困难
3. **兼容性**: 现有脚本系统集成
### 缓解措施
1. 渐进式迁移,保持向后兼容
2. 完善的性能测试
3. 详细的文档和注释
### 回滚计划
每个Phase完成后打tag必要时可回滚
# 当前执行步骤:
"阶段4.8 - 阶段4整体测试"
# 任务进度
## 阶段1基础设施搭建
### 1.1 创建目录结构
[2025-12-01 17:16:16]
- 已修改:创建目录 Assets/Scripts/Framework/UpdateScheduler/
- 更改新建UpdateScheduler模块目录
- 原因:为扁平化调度器准备代码组织结构
- 阻碍因素:无
- 状态:未确认
### 1.2 创建IUpdateable接口
[2025-12-01 17:17:30]
- 已修改:创建文件 IUpdateable.cs (48行)
- 更改定义IUpdateable接口3个方法和UpdateMode枚举3个值
- 原因为ScriptableObject提供统一的Update接口支持扁平化调度
- 阻碍因素:无
- 状态:未确认
### 1.3 创建UpdateScheduler核心类
[2025-12-01 17:18:45]
- 已修改:创建文件 UpdateScheduler.cs (201行)
- 更改实现完整的扁平化调度器Register、DoUpdate、时间分片、Reset/Clear等
- 原因提供时间分片和扁平化Update管理的核心实现
- 阻碍因素:无
- 状态:未确认
### 1.4 RootObject添加Scheduler字段
[2025-12-01 17:20:15]
- 已修改RootObject.cs (添加1行Line 19)
- 更改添加Scheduler字段并初始化
- 原因为RootObject提供调度器实例
- 阻碍因素:无
- 状态:未确认
### 1.5 RootObject添加清理逻辑
[2025-12-01 17:21:30]
- 已修改RootObject.cs UnloadScript方法 (添加2行Line 38-39)
- 更改在卸载时调用Scheduler.Clear()
- 原因:防止内存泄漏,正确释放注册对象
- 阻碍因素:无
- 状态:未确认
### 1.6 阶段1整体测试
[2025-12-01 17:22:45]
- 已修改阶段1所有代码完成等待测试
- 更改基础设施搭建完成3个新文件RootObject修改2处
- 原因验证阶段1无破坏性影响
- 测试项目:
1. 编译项目 - ✅ 已通过无linter错误
2. 运行编辑器 - ✅ 用户确认成功
3. 打开项目 - ✅ 用户确认成功
4. 检查RootObject.Scheduler字段 - ✅ 用户确认成功
5. 关闭项目 - ✅ 用户确认成功
6. 检查控制台无错误 - ✅ 用户确认成功
- 阻碍因素:无
- 状态:✅ 成功
- Commit: 用户已提交
## 阶段2接口适配
### 2.1 ScriptableObject添加IUpdateable接口声明
[2025-12-01 17:30:00]
- 已修改ScriptableObject.cs 类声明 (Line 582添加IUpdateable接口)
- 更改ScriptableObject实现IUpdateable接口
- 原因使所有ScriptableObject可以被调度器管理
- 阻碍因素:无
- 状态:未确认
### 2.2 添加Update模式字段
[2025-12-01 17:32:15]
- 已修改ScriptableObject.cs (添加3个字段Line 111-116)
- 更改添加_updateMode、_activeStartTime、_activeEndTime字段
- 原因存储对象的Update模式和时间范围
- 阻碍因素:无
- 状态:未确认
### 2.3 添加虚方法供子类重写
[2025-12-01 17:34:30]
- 已修改ScriptableObject.cs (添加2个虚方法Line 175-183)
- 更改添加GetUpdateMode和GetActiveTimeRange虚方法
- 原因允许子类自定义Update模式和时间范围
- 阻碍因素:无
- 状态:未确认
### 2.4 实现IUpdateable接口方法
[2025-12-01 17:36:45]
- 已修改ScriptableObject.cs (添加新的partial class约35行Line 916-950)
- 更改实现IUpdateable的3个接口方法ExecuteUpdate、GetUpdateName、IsUpdateReady
- 原因提供扁平化Update入口复用原有UpdateTicks逻辑
- 阻碍因素:无
- 状态:未确认
### 2.4.1 重命名方法为FlatOptimizationUpdate更具描述性
[2025-12-01 17:40:00]
- 已修改IUpdateable.cs、UpdateScheduler.cs、ScriptableObject.cs
- 更改将方法重命名为FlatOptimizationUpdate明确表达"扁平化优化Update"的含义
- 原因:提高代码可读性,方法名清晰表达优化目的
- 阻碍因素:无
- 状态:未确认
### 2.5 阶段2整体测试
[2025-12-01 17:38:00]
- 已修改阶段2所有代码完成等待测试
- 更改ScriptableObject完整实现IUpdateable接口添加3个字段、2个虚方法、3个接口方法
- 原因:验证接口实现无破坏性影响
- 测试项目:
1. 编译项目 - ✅ 已通过无linter错误
2. 运行编辑器 - ✅ 用户确认成功
3. 打开项目 - ✅ 用户确认成功
4. 检查Inspector中新增字段是否可见 - ✅ 用户确认成功
5. 验证游戏功能不受影响 - ✅ 用户确认成功
- 阻碍因素:无
- 状态:✅ 成功
- Commit: 用户已提交
## 阶段3可选注册机制
### 3.1 添加注册控制字段
[2025-12-01 17:45:00]
- 已修改ScriptableObject.cs (添加1个字段Line 120)
- 更改添加_useUpdateScheduler控制字段默认false
- 原因:提供可选的调度器注册开关,默认不影响现有逻辑
- 阻碍因素:无
- 状态:未确认
### 3.2 添加脚本方法启用调度器
[2025-12-01 17:47:15]
- 已修改ScriptableObject.cs (添加EnableUpdateScheduler方法Line 178-183)
- 更改添加RScript可调用的启用方法
- 原因:允许脚本中手动启用调度器
- 阻碍因素:无
- 状态:未确认
### 3.3 修改ApplyScript添加注册逻辑
[2025-12-01 17:49:30]
- 已修改ScriptableObject.cs ApplyScript方法 (添加5行Line 678-682)
- 更改在ApplyScript中添加可选注册调用
- 原因:对象应用时自动注册到调度器(如果启用)
- 阻碍因素:无
- 状态:未确认
### 3.4 实现RegisterToScheduler方法
[2025-12-01 17:51:45]
- 已修改ScriptableObject.cs (添加RegisterToScheduler私有方法约25行Line 684-708)
- 更改:实现调度器注册逻辑,包含错误处理和日志
- 原因封装注册流程自动获取Update模式和时间范围
- 阻碍因素:无
- 状态:未确认
### 3.5 阶段3整体测试
[2025-12-01 17:53:00]
- 已修改阶段3所有代码完成等待测试
- 更改实现可选注册机制添加1个字段、1个公开方法、1个私有方法、修改ApplyScript
- 原因:验证注册机制正确工作
- 测试项目:
1. 编译项目 - ✅ 已通过无linter错误
2. 运行编辑器 - 待测试
3. 在测试脚本中调用EnableUpdateScheduler() - 待测试
4. 检查控制台日志,确认对象被注册 - 待测试
5. 验证默认行为不变_useUpdateScheduler=false - 待测试
- 阻碍因素:无
- 状态:未确认 → 等待用户确认:成功/不成功?
## 阶段4双轨运行和实际调用
### 4.1 GameController添加控制字段
[2025-12-01 18:00:00]
- 已修改GameController.cs (添加1个字段Line 22)
- 更改添加_enableSchedulerUpdate控制字段默认false
- 原因控制GameController使用新旧Update方式
- 阻碍因素:无
- 状态:未确认
### 4.2 修改GameController主Update循环
[2025-12-01 18:02:15]
- 已修改GameController.cs Update方法主循环 (修改8行Line 318-329)
- 更改添加新旧Update方式分支支持调用FlatOptimizationUpdate或ScriptUpdate
- 原因允许运行时切换Update方式进行对比
- 阻碍因素:无
- 状态:未确认
### 4.3 修改GameController滚动时间轴Update调用
[2025-12-01 18:04:30]
- 已修改GameController.cs Update方法滚动逻辑 (修改4行Line 373-376)
- 更改:滚动时间轴也支持新旧方式切换
- 原因确保所有Update调用点一致
- 阻碍因素:无
- 状态:未确认
### 4.4 修改GameController滚动Reset逻辑
[2025-12-01 18:05:45]
- 已修改GameController.cs Update方法滚动Reset (修改10行Line 378-388)
- 更改:滚动重置也支持新旧方式切换
- 原因确保Reset调用点一致
- 阻碍因素:无
- 状态:未确认
### 4.5 修改ForceScriptUpdate方法
[2025-12-01 18:07:00]
- 已修改GameController.cs ForceScriptUpdate方法 (修改5行Line 294-299)
- 更改:强制更新也支持新旧方式切换
- 原因确保所有Update入口一致
- 阻碍因素:无
- 状态:未确认
### 4.6 修改Stop/Pause/Play方法
[2025-12-01 18:08:15]
- 已修改GameController.cs Stop/Pause/Play方法 (修改15行Line 263-292)
- 更改:播放控制方法也支持新旧方式切换
- 原因确保播放控制的Update调用一致
- 阻碍因素:无
- 状态:未确认
### 4.7 RootObject.UpdateTicks调用调度器关键
[2025-12-01 18:10:30]
- 已修改RootObject.cs UpdateTicks方法 (添加10行Line 111-120)
- 更改RootObject调用Scheduler.DoUpdate()进行扁平化Update移除base.UpdateTicks()调用
- 原因这是关键步骤让Scheduler实际被调用建立完整调用链
- 阻碍因素:无
- 状态:未确认
### 4.7.1 修复崩溃问题 - 条件调用Scheduler
[2025-12-01 18:15:00]
- 已修改RootObject.cs UpdateTicks方法 (修改逻辑Line 112-124)
- 更改添加条件判断仅在Scheduler有注册对象时调用Scheduler否则调用base.UpdateTicks()保持旧行为
- 原因:修复旧方式下的崩溃问题,避免双重更新和冲突
- 阻碍因素:无
- 状态:未确认
### 4.8 阶段4整体测试
[2025-12-01 18:12:00]
- 已修改阶段4所有代码完成等待测试
- 更改实现双轨Update支持新旧方式切换GameController 6处修改RootObject 1处关键修改
- 原因验证新方式功能正确且性能提升Scheduler实际被调用
- 测试项目:
1. 编译项目 - ✅ 已通过无linter错误
2. 旧方式测试_enableSchedulerUpdate=false - 待测试
3. 新方式测试_enableSchedulerUpdate=true部分对象启用调度器 - 待测试
4. 功能验证(播放/暂停/停止/滚动时间轴) - 待测试
5. 性能对比Tracy Profiler数据 - 待测试
- 阻碍因素:无
- 状态:未确认 → 等待用户确认:成功/不成功?
- 调用链验证:
* 旧方式GameController → MainObject.ScriptUpdate() → 递归
* 新方式GameController → MainObject.FlatOptimizationUpdate() → RootObject.UpdateTicks() → Scheduler.DoUpdate() → 扁平化
# 最终审查
[待完成]

View File

@@ -17,6 +17,9 @@ namespace Demo.Game
[Resources, SerializeField] public GlobalConfig MainConfig;
[Content] private RootObject MainObject;
[Content, SerializeField]
private bool _enableSchedulerUpdate = false; // 控制是否使用新的UpdateScheduler
public string RootSourcePath { get; private set; }
public Action<float, float> SetupSongDuration { get; private set; } = (_, _) => { };
@@ -264,7 +267,10 @@ namespace Demo.Game
{
SetSongCurrentTime(SongOffset);
}
MainObject.ScriptUpdate(SongOffset, Time.deltaTime, ScriptableObject.TickType.Reset);
if (_enableSchedulerUpdate)
MainObject.FlatOptimizationUpdate(SongOffset, Time.deltaTime, ScriptableObject.TickType.Reset);
else
MainObject.ScriptUpdate(SongOffset, Time.deltaTime, ScriptableObject.TickType.Reset);
}
public void Pause()
@@ -274,7 +280,10 @@ namespace Demo.Game
{
SetSongCurrentTime(CurrentTime);
}
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Pause);
if (_enableSchedulerUpdate)
MainObject.FlatOptimizationUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Pause);
else
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Pause);
}
public void Play()
@@ -285,12 +294,18 @@ namespace Demo.Game
SetupSongDuration(SongOffset, MainAudio.CurrentClip.length + SongOffset);
}
SetSongCurrentTime(CurrentTime);
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Start);
if (_enableSchedulerUpdate)
MainObject.FlatOptimizationUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Start);
else
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, ScriptableObject.TickType.Start);
}
public void ForceScriptUpdate(ScriptableObject.TickType type = ScriptableObject.TickType.Update)
{
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, type);
if (_enableSchedulerUpdate)
MainObject.FlatOptimizationUpdate(CurrentTime, Time.deltaTime, type);
else
MainObject.ScriptUpdate(CurrentTime, Time.deltaTime, type);
}
private bool IsScrollTimeline = false;
@@ -314,7 +329,16 @@ namespace Demo.Game
using (Profiler.BeginZone("GameController.ScriptUpdate"))
{
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
if (_enableSchedulerUpdate)
{
// 新方式调用FlatOptimizationUpdate会触发Scheduler
MainObject.FlatOptimizationUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
}
else
{
// 旧方式:递归调用
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
}
}
}
if (IsMain == false)
@@ -358,13 +382,24 @@ namespace Demo.Game
{
SetSongCurrentTime(CurrentTime);
}
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
if (_enableSchedulerUpdate)
MainObject.FlatOptimizationUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
else
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
}
else if (IsScrollTimeline == true)
{
IsScrollTimeline = false;
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Reset);
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
if (_enableSchedulerUpdate)
{
MainObject.FlatOptimizationUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Reset);
MainObject.FlatOptimizationUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
}
else
{
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Reset);
MainObject.ScriptUpdate(CurrentTime, deltaTime, ScriptableObject.TickType.Update);
}
}
}
}

View File

@@ -15,6 +15,8 @@ namespace Demo.Game
[Content] public GameController RootGameController;
public string SourcePath;
[Content] public UpdateScheduler Scheduler = new UpdateScheduler();
protected override IEnumerator DoSomethingDuringApplyScript()
{
@@ -32,6 +34,10 @@ namespace Demo.Game
{
Keyboard.current.onTextInput -= InputCatchChar;
}
// 清理调度器
Scheduler?.Clear();
yield return base.UnloadScript();
}
@@ -102,7 +108,20 @@ namespace Demo.Game
Foo();
}
base.UpdateTicks(currentTime, deltaTime, tickType);
// 调用调度器进行扁平化Update仅当有对象注册时
if (Scheduler != null && Scheduler.TotalRegistered > 0)
{
using (Profiler.BeginZone("RootObject.SchedulerUpdate"))
{
Scheduler.DoUpdate(currentTime, deltaTime, tickType);
}
}
else
{
// 旧方式调用base.UpdateTicks()(保持向后兼容)
base.UpdateTicks(currentTime, deltaTime, tickType);
}
}
}
}

View File

@@ -108,6 +108,18 @@ namespace Demo
[Content, SerializeField] private bool IsSetObjectDisable = false;
[Content] public int UpdatePerFrame = 1;
[Content, SerializeField, Header("UpdateMode")]
protected UpdateMode _updateMode = UpdateMode.Permanent;
[Content, SerializeField]
protected float _activeStartTime = 0f;
[Content, SerializeField]
protected float _activeEndTime = float.MaxValue;
[Content, SerializeField]
private bool _useUpdateScheduler = false; // 控制是否使用新的UpdateScheduler
/// <summary>
/// 设置坐标
/// </summary>
@@ -163,6 +175,26 @@ namespace Demo
UpdatePerFrame = Mathf.Max(1, frame);
}
/// <summary>
/// 启用新的UpdateScheduler用于逐步迁移
/// </summary>
[Convention.RScript.Variable.Attr.Method]
public void EnableUpdateScheduler()
{
_useUpdateScheduler = true;
}
/// <summary>
/// 子类可重写定义自己的Update模式
/// </summary>
protected virtual UpdateMode GetUpdateMode() => UpdateMode.Permanent;
/// <summary>
/// 子类可重写,定义自己的活跃时间范围
/// </summary>
protected virtual (float start, float end) GetActiveTimeRange()
=> (0f, float.MaxValue);
private void ScriptableObjectDoReset()
{
transform.localPosition = EnterGameLocalPosition;
@@ -533,7 +565,7 @@ namespace Demo
if (gameObject.activeInHierarchy == false)
return;
using (Profiler.BeginZone($"{GetType().Name}.ScriptUpdate"))
using (Profiler.BeginZone($"{m_GetTypeName}.ScriptUpdate"))
{
if (tickType == TickType.Reset)
{
@@ -640,9 +672,42 @@ namespace Demo
IsEnableUpdate = false;
}
}
// 可选的调度器注册
if (_useUpdateScheduler && (IsSelfEnableUpdate || IsEnableUpdate))
{
RegisterToScheduler();
}
IsScriptApply = true;
}
/// <summary>
/// 注册到UpdateScheduler
/// </summary>
private void RegisterToScheduler()
{
// 获取Update模式
_updateMode = GetUpdateMode();
// 获取时间范围
if (_updateMode == UpdateMode.TimeBound)
{
(_activeStartTime, _activeEndTime) = GetActiveTimeRange();
}
// 注册到调度器
try
{
GetRoot()?.Scheduler?.Register(this, _updateMode, _activeStartTime, _activeEndTime);
Debug.Log($"[ScriptableObject] 已注册到调度器: {GetUpdateName()}, Mode={_updateMode}");
}
catch (System.Exception ex)
{
Debug.LogError($"[ScriptableObject] 注册到调度器失败: {ex.Message}", this);
}
}
public virtual IEnumerator UnloadScript()
{
if (EnsureEnableScript())
@@ -696,6 +761,63 @@ namespace Demo
}
}
/// <summary>
/// IUpdateable接口实现
/// </summary>
public partial class ScriptableObject : IUpdateable
{
/// <summary>
/// IUpdateable.FlatOptimizationUpdate实现 - 扁平化优化Update入口直接更新对象自身无递归遍历子对象
/// </summary>
public void FlatOptimizationUpdate(float currentTime, float deltaTime, TickType tickType)
{
if (IsScriptApply == false)
return;
if (gameObject.activeInHierarchy == false)
return;
// 只更新自己,不递归子节点
if (this.IsSelfEnableUpdate && UpdatePerFrame > 0)
{
if (ScriptUpdateCounter % UpdatePerFrame == 0)
{
using (Profiler.BeginZone($"{this.ScriptName}.UpdateTicks"))
{
UpdateTicks(currentTime, deltaTime, tickType);
}
}
ScriptUpdateCounter += tickType == TickType.Update ? 1 : 0;
}
}
private string c_GetTypeName;
private string m_GetTypeName
{
get
{
if (string.IsNullOrEmpty(c_GetTypeName))
{
c_GetTypeName = GetType().Name;
}
return c_GetTypeName;
}
}
/// <summary>
/// IUpdateable.GetUpdateName实现
/// </summary>
public string GetUpdateName()
{
return $"{ScriptName}<{m_GetTypeName}>";
}
/// <summary>
/// IUpdateable.IsUpdateReady实现
/// </summary>
public bool IsUpdateReady => IsScriptApply;
}
#endregion
public interface IAssetBundleLoader : IScriptableObject

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 88165dc714eb2714a9488d29f279ec51
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using UnityEngine;
namespace Demo.Game
{
/// <summary>
/// 统一Update接口用于扁平化调度
/// </summary>
public interface IUpdateable
{
/// <summary>
/// 扁平化优化Update调用直接更新对象自身无递归遍历子对象
/// </summary>
void FlatOptimizationUpdate(float currentTime, float deltaTime, ScriptableObject.TickType tickType);
/// <summary>
/// 对象名称,用于调试
/// </summary>
string GetUpdateName();
/// <summary>
/// 是否已应用脚本
/// </summary>
bool IsUpdateReady { get; }
}
/// <summary>
/// Update模式
/// </summary>
public enum UpdateMode
{
/// <summary>
/// 永久活跃(整个关卡周期)
/// </summary>
Permanent,
/// <summary>
/// 有时间范围限制
/// </summary>
TimeBound,
/// <summary>
/// 手动控制激活/停用
/// </summary>
Manual
}
}

View File

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

View File

@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Convention;
using UnityEngine;
namespace Demo.Game
{
/// <summary>
/// 全局扁平化Update调度器
/// </summary>
public class UpdateScheduler
{
// 永久活跃对象
private readonly List<IUpdateable> permanentObjects = new();
// 时间范围对象(排序队列)
private readonly SortedDictionary<float, List<IUpdateable>> activationQueue = new();
private readonly SortedDictionary<float, List<IUpdateable>> deactivationQueue = new();
// 当前活跃对象
private readonly List<IUpdateable> activeObjects = new();
// 统计信息
public int TotalRegistered => permanentObjects.Count + activationQueue.Values.Sum(l => l.Count);
public int CurrentActive => permanentObjects.Count + activeObjects.Count;
/// <summary>
/// 注册对象到调度器
/// </summary>
public void Register(IUpdateable obj, UpdateMode mode, float startTime = 0f, float endTime = float.MaxValue)
{
if (obj == null)
{
Debug.LogError("[UpdateScheduler] 尝试注册null对象");
return;
}
switch (mode)
{
case UpdateMode.Permanent:
if (!permanentObjects.Contains(obj))
{
permanentObjects.Add(obj);
Debug.Log($"[UpdateScheduler] 注册永久对象: {obj.GetUpdateName()}");
}
break;
case UpdateMode.TimeBound:
// 注册激活时间
if (!activationQueue.ContainsKey(startTime))
activationQueue[startTime] = new List<IUpdateable>();
if (!activationQueue[startTime].Contains(obj))
{
activationQueue[startTime].Add(obj);
Debug.Log($"[UpdateScheduler] 注册时间范围对象: {obj.GetUpdateName()} ({startTime:F2}s - {endTime:F2}s)");
}
// 注册停用时间
if (!deactivationQueue.ContainsKey(endTime))
deactivationQueue[endTime] = new List<IUpdateable>();
if (!deactivationQueue[endTime].Contains(obj))
{
deactivationQueue[endTime].Add(obj);
}
break;
case UpdateMode.Manual:
// 手动模式不自动注册
Debug.Log($"[UpdateScheduler] 手动模式对象: {obj.GetUpdateName()}");
break;
}
}
/// <summary>
/// 手动激活对象Manual模式
/// </summary>
public void Activate(IUpdateable obj)
{
if (!activeObjects.Contains(obj))
{
activeObjects.Add(obj);
}
}
/// <summary>
/// 手动停用对象Manual模式
/// </summary>
public void Deactivate(IUpdateable obj)
{
activeObjects.Remove(obj);
}
/// <summary>
/// 主Update循环 - 扁平化遍历
/// </summary>
public void DoUpdate(float currentTime, float deltaTime, ScriptableObject.TickType tickType)
{
// 时间分片管理
using (Profiler.BeginZone("UpdateScheduler.TimeSlicing"))
{
ProcessActivations(currentTime);
ProcessDeactivations(currentTime);
}
// 扁平更新所有活跃对象
using (Profiler.BeginZone($"UpdateScheduler.FlatUpdate (Permanent:{permanentObjects.Count} Active:{activeObjects.Count})"))
{
// 永久活跃对象
for (int i = 0; i < permanentObjects.Count; i++)
{
if (permanentObjects[i]?.IsUpdateReady == true)
{
permanentObjects[i].FlatOptimizationUpdate(currentTime, deltaTime, tickType);
}
}
// 当前活跃对象
for (int i = activeObjects.Count - 1; i >= 0; i--)
{
if (activeObjects[i] == null || !activeObjects[i].IsUpdateReady)
{
activeObjects.RemoveAt(i);
continue;
}
activeObjects[i].FlatOptimizationUpdate(currentTime, deltaTime, tickType);
}
}
}
private void ProcessActivations(float currentTime)
{
while (activationQueue.Count > 0)
{
var firstKey = activationQueue.Keys.First();
if (firstKey > currentTime) break;
var objects = activationQueue[firstKey];
foreach (var obj in objects)
{
if (obj != null && !activeObjects.Contains(obj))
{
activeObjects.Add(obj);
Debug.Log($"[UpdateScheduler] 激活: {obj.GetUpdateName()} @ {currentTime:F2}s");
}
}
activationQueue.Remove(firstKey);
}
}
private void ProcessDeactivations(float currentTime)
{
while (deactivationQueue.Count > 0)
{
var firstKey = deactivationQueue.Keys.First();
if (firstKey > currentTime) break;
var objects = deactivationQueue[firstKey];
foreach (var obj in objects)
{
activeObjects.Remove(obj);
if (obj != null)
{
Debug.Log($"[UpdateScheduler] 停用: {obj.GetUpdateName()} @ {currentTime:F2}s");
}
}
deactivationQueue.Remove(firstKey);
}
}
/// <summary>
/// 重置调度器用于Reset/Restart
/// </summary>
public void Reset()
{
activeObjects.Clear();
Debug.Log("[UpdateScheduler] 重置完成");
}
/// <summary>
/// 清空所有注册
/// </summary>
public void Clear()
{
permanentObjects.Clear();
activationQueue.Clear();
deactivationQueue.Clear();
activeObjects.Clear();
Debug.Log("[UpdateScheduler] 清空所有注册");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4188132fc03e93e4dbb04bd7635c6152