前言

做游戏开发时,我们经常遇到这样的场景:游戏从主菜单进入游戏,然后暂停,再返回游戏,最后回到主菜单。这背后就是状态机在工作。

传统写法用 if-else 或者 switch 来管理,代码量一多就乱成一团。本文要讲的状态机系统,解决了三个核心问题:

  1. 状态切换异步化 - 支持保存数据、加载资源等耗时操作
  2. 状态可回退 - 像浏览器后退按钮一样返回上一个状态
  3. 零配置使用 - 新增状态不需要注册,直接就能用

核心设计:泛型 + 多态

状态基类的设计

1
2
3
4
5
6
7
8
9
10
11
public abstract class StateBase<T> where T : class
{
public string TypeName { get; set; }
public bool AllowReentry { get; protected set; }
public bool HaveExitTask { get; protected set; }
public Action ExitFinishCall { get; set; }

public virtual void EnterState(int enterCount = 1, object parameters = null) { }
public virtual void UpdateState() { }
public virtual void ExitState() { }
}

这个设计有几个关键点:

1. 泛型参数 T 的作用

T 看起来多余,其实是为了类型隔离。你可以同时存在多个独立的状态机系统:

1
2
3
StateBase<GameStateBase>    // 游戏状态机
StateBase<CharacterState> // 角色状态机
StateBase<UIState> // UI状态机

三个系统互不干扰,各自管理自己的状态。

2. HaveExitTask 机制 - 核心创新

这是这个系统最巧妙的设计。先看问题:传统状态切换是这样的:

1
2
ExitState()  // 退出当前状态
EnterState() // 进入新状态

但实际开发中,ExitState 可能需要保存数据、上传服务器,这些操作是异步的。如果直接写成同步代码,要么阻塞主线程,要么状态切换不完整。

解决方案:

1
2
public bool HaveExitTask { get; protected set; }
public Action ExitFinishCall { get; set; }

状态管理器这样处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (currentState.HaveExitTask)
{
// 设置回调函数
currentState.ExitFinishCall = () => {
currentState = nextState;
nextState.EnterState();
};

// 启动异步任务
currentState.ExitState();
}
else
{
// 同步切换
currentState.ExitState();
currentState = nextState;
nextState.EnterState();
}

使用时这样写:

1
2
3
4
5
6
7
8
9
10
11
12
public class GamePauseState : GameStateBase
{
protected override bool DefaultHaveExitTask => true;

public override void ExitState()
{
// 启动异步保存
SaveGameAsync(() => {
ExitFinishCall?.Invoke(); // 保存完通知切换
});
}
}

3. enterCount 参数 - 区分首次进入和重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override void EnterState(int enterCount, object parameters)
{
if (enterCount == 1)
{
// 首次进入,需要初始化
LoadGameWorld();
}
else
{
// 重入(重新开始),先清理再初始化
ClearGameWorld();
LoadGameWorld();
}
}

这个参数特别适合”重新开始游戏”的场景。


状态管理器:双泛型的妙用

CRTP 设计模式

1
2
3
public class StateManagerBase<T, K> : MonoBehaviour 
where T : StateBase<T>
where K : StateManagerBase<T, K>

为什么要两个泛型参数?

T 是状态类型,确保注册的状态类型正确。

K 是管理器类型,用了 CRTP(奇异递归模板模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static K Instance
{
get
{
// 当 K = GameStateManager 时,这行代码变成:
// instance = FindObjectOfType<GameStateManager>();
instance = FindObjectOfType<K>();

if (instance == null)
{
GameObject go = new GameObject(typeof(K).Name);
instance = go.AddComponent<K>(); // 自动添加组件
}

return instance; // 返回 GameStateManager 类型,不是基类
}
}

好处是:Instance 返回的是具体类型,不需要强制转换。

1
2
// 使用时
GameStateManager.Instance.SetState<PlayingState>(); // 类型精确

状态切换的核心流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public void SetState<E>(object parameters = null) where E : StateBase<T>
{
// 1. 防止重复切换
if (_isTransitioning) return;

// 2. 查找或创建状态
if (!_statesDict.TryGetValue(name, out var nextState))
{
nextState = CreateAndRegisterState<E>(); // 自动创建
}

// 3. 检查是否需要切换
if (_currentState?.TypeName != nextState?.TypeName || nextState.AllowReentry)
{
_isTransitioning = true;

// 4. 当前状态压栈(用于回退)
if (_currentState != null)
_stateStack.Push(_currentState);

// 5. 执行切换(同步或异步)
if (_currentState?.HaveExitTask == true)
{
_currentState.ExitFinishCall = () => {
_currentState = nextState;
nextState.EnterState();
_isTransitioning = false;
};
_currentState.ExitState(); // 异步任务在这里启动
}
else
{
_currentState?.ExitState();
_currentState = nextState;
nextState.EnterState();
_isTransitioning = false;
}
}
}

整个流程就像接力棒:当前状态完成退出任务后,通过回调通知管理器切换到新状态。


状态栈:像浏览器一样后退

1
2
3
4
5
6
7
private Stack<StateBase<T>> _stateStack = new Stack<StateBase<T>>();

public void PopState(object parameters = null)
{
var prevState = _stateStack.Pop();
SetStateInternal(prevState, parameters);
}

每个 SetState 都会把当前状态压栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
初始状态:Menu

SetState<Playing>()
→ 栈:[Menu],当前:Playing

SetState<Pause>()
→ 栈:[Menu, Playing],当前:Pause

SetState<Settings>()
→ 栈:[Menu, Playing, Pause],当前:Settings

PopState()
→ 栈:[Menu, Playing],当前:Pause

PopState()
→ 栈:[Menu],当前:Playing

PopState()
→ 栈:[],当前:Menu

这个机制特别适合多级菜单:主菜单 → 设置 → 音频 → 返回音频 → 返回设置 → 返回主菜单。


自动注册:反射的威力

传统方式需要手动注册每个状态:

1
2
3
4
5
// 繁琐的写法
gameStateManager.RegisterState(new MenuState());
gameStateManager.RegisterState(new PlayingState());
gameStateManager.RegisterState(new PauseState());
// ... 每个状态都要写

这个系统用反射实现自动注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private StateBase<T> CreateAndRegisterState<E>() where E : StateBase<T>
{
try
{
E stateInstance = (E)Activator.CreateInstance(typeof(E));
RegisterState(stateInstance);
return stateInstance;
}
catch (Exception ex)
{
Debug.LogError($"创建状态失败: {ex.Message}");
return null;
}
}

状态只在第一次使用时创建,之后永久缓存在字典里:

1
2
3
4
5
6
7
8
9
10
11
private Dictionary<string, StateBase<T>> _statesDict;

public void SetState<E>()
{
if (!_statesDict.TryGetValue(name, out var state))
{
state = CreateAndRegisterState<E>(); // 创建一次,永久复用
_statesDict[name] = state;
}
// 后续切换都直接用缓存的实例
}

性能对比:

  • 反射创建:约 1ms(只在第一次)
  • 字典查找:约 0.00001ms(O(1))
  • 游戏运行时性能几乎无影响

编辑器工具:反射 + 缓存

自动查找所有状态类

编辑器工具需要知道项目中有哪些状态类。用反射扫描程序集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static Dictionary<Type, List<Type>> GetStateHierarchy()
{
var allTypes = GetAllUserTypes(); // 获取所有用户程序集的类型

// 查找 StateBase<T> 的子类
var stateBaseType = typeof(StateBase<>);
var intermediateTypes = allTypes.Where(type =>
type.IsClass &&
!type.IsAbstract &&
type.BaseType?.IsGenericType == true &&
type.BaseType.GetGenericTypeDefinition() == stateBaseType
).ToList();

// 为每个中间类查找其子类
var hierarchy = new Dictionary<Type, List<Type>>();
foreach (var intermediateType in intermediateTypes)
{
var derivedTypes = allTypes.Where(type =>
type.IsSubclassOf(intermediateType)
).ToList();

hierarchy[intermediateType] = derivedTypes;
}

return hierarchy;
}

扫描出来的结构是这样的:

1
2
3
4
5
StateBase<T>
└─ GameStateBase (中间类)
├─ MenuState
├─ PlayingState
└─ PauseState

缓存优化

反射扫描所有程序集很慢(约 50-200ms),所以用了 30 秒缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Dictionary<Type, List<Type>> _cachedHierarchy;
private static DateTime _lastCacheTime = DateTime.MinValue;
private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromSeconds(30);

public static Dictionary<Type, List<Type>> GetStateHierarchy(bool useCache = true)
{
if (useCache && _cachedHierarchy != null &&
DateTime.Now - _lastCacheTime < CACHE_DURATION)
{
return _cachedHierarchy; // 命中缓存,超快
}

var hierarchy = ComputeHierarchy();
_cachedHierarchy = hierarchy;
_lastCacheTime = DateTime.Now;
return hierarchy;
}

性能提升:

  • 无缓存:50-200ms
  • 有缓存:0.1ms
  • 性能提升 显著

线程安全:双重检查锁定

单例模式用了双重检查锁定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private static readonly object _lock = new object();
private static K instance;

public static K Instance
{
get
{
// 第一次检查(无锁,快速路径)
if (instance == null)
{
lock (_lock)
{
// 第二次检查(有锁,但只在第一次执行)
if (instance == null)
{
instance = FindObjectOfType<K>();
if (instance == null)
{
GameObject go = new GameObject(typeof(K).Name);
instance = go.AddComponent<K>();
}
DontDestroyOnLoad(instance.gameObject);
instance.Init();
}
}
}
return instance;
}
}

为什么要双重检查?

单次检查的话,两个线程可能同时通过第一次检查,导致创建两个实例。双重检查确保只创建一个实例,同时又保持高性能。


完整示例

看一个完整的使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 使用状态管理器
public class GameController : MonoBehaviour
{
void Start()
{
GameStateManager.Instance.SetState<MenuState>();
}
}

// 主菜单状态
public class MenuState : GameStateBase
{
public override void EnterState(int enterCount, object parameters)
{
Debug.Log("进入主菜单");
}
}

// 游戏运行状态(可重入)
public class PlayingState : GameStateBase
{
protected override bool DefaultAllowReentry => true;

public override void EnterState(int enterCount, object parameters)
{
if (enterCount == 1)
Debug.Log("首次进入游戏");
else
Debug.Log("重入游戏(重新开始)");
}

public override void UpdateState()
{
// 游戏逻辑更新
}
}

// 暂停状态(带异步保存)
public class PauseState : GameStateBase
{
protected override bool DefaultHaveExitTask => true;

public override void ExitState()
{
Debug.Log("保存游戏数据...");
CoroutineScheduler.Instance.DelaySecondsExecute(1f, () => {
Debug.Log("保存完成");
ExitFinishCall?.Invoke(); // 通知切换完成
});
}
}

// 按钮触发
void OnButtonClick()
{
GameStateManager.Instance.SetState<PlayingState>(); // 开始游戏
GameStateManager.Instance.SetState<PauseState>(); // 暂停
GameStateManager.Instance.PopState(); // 返回游戏
}

设计背后的思考

为什么用泛型而不是接口?

接口需要运行时类型检查,泛型可以在编译时检查类型。而且泛型基类可以提供默认实现,代码复用性更高。

为什么用单例而不是依赖注入?

Unity 生态中单例是主流,全局访问更方便。依赖注入需要额外的容器,学习曲线陡峭。

为什么用反射自动注册?

手动注册容易遗漏,每次新增状态都要修改代码。反射自动注册是”一次编写,永久使用”。


总结

这套状态机系统的核心价值:

  1. 异步切换 - 用回调机制优雅地处理异步状态转换
  2. 状态回退 - 用栈实现浏览器式的后退功能
  3. 零配置 - 用反射自动创建和注册状态
  4. 类型安全 - 用泛型在编译时检查类型
  5. 高效缓存 - 用 30 秒缓存将反射性能提升N倍

技术上,它结合了多个经典设计模式:状态模式、模板方法模式、工厂模式,通过泛型、反射、闭包等现代语言特性,将状态机这个经典模式提升到了新的高度。

实际项目中,这套系统大幅降低了状态管理的心智负担。新增一个状态只需要写一个类,不需要修改任何框架代码,符合开闭原则。调试时可以通过编辑器工具一目了然地看到所有状态结构,大大提升了开发效率。

状态机虽是老生常谈,但做对了细节,依然是游戏开发中不可或缺的基础设施。

DataBaker 是一个专业的Unity配置数据管理工具,能够将Excel表格转换为CSV文件,并自动生成高效的二进制数据和对应的C#类。它采用注册表模式,无需反射机制,完美兼容WebGL、iOS等所有Unity平台。

核心优势

  • 高性能: 使用GZip压缩的二进制格式,数据加载速度比纯CSV快5-10倍
  • 低内存占用: 字符串池技术减少内存占用40-60%
  • 零反射: 采用注册表模式,兼容AOT编译环境(WebGL/iOS/IL2CPP)
  • 自动化: 一键生成C#类和二进制文件,集成Addressable资源管理
  • 类型安全: 编译时类型检查,避免运行时错误
  • 易维护: 添加新表只需一行注册代码

核心特性

1. Excel到CSV转换

  • 使用Excel宏快速导出CSV文件
  • 支持复杂的Excel格式,包括公式、合并单元格等
  • 自动处理数据类型转换

2. CSV到二进制转换

  • GZip压缩算法,大幅减少文件体积
  • 自定义二进制格式,支持版本控制
  • 字符串池优化,减少重复字符串的存储

3. 自动代码生成

  • 自动生成数据类,包含所有字段
  • 自动生成表格管理类,继承DataTableBase
  • 支持自定义命名空间

4. 运行时管理

  • 支持异步/同步加载
  • 支持按需加载单个表或批量加载
  • 提供查询、筛选等数据操作API
  • 自动管理Addressable资源

5. 支持的数据类型

类型 说明 数组支持
int 整数 ✅ int[]
float 单精度浮点 ✅ float[]
double 双精度浮点 ✅ double[]
bool 布尔值 ✅ bool[]
string 字符串 ✅ string[]

系统要求

  • Unity 2020.3 或更高版本
  • Addressables Asset System 1.21.19 或更高版本
  • Microsoft Excel (用于CSV导出)

安装说明

方式一: Unity Package Manager

  1. 打开Unity项目
  2. 选择 Window > Package Manager
  3. 点击左上角 +
  4. 选择 Add package by name
  5. 输入包名: com.databaker.core
  6. 点击 Add

方式二: 手动导入

  1. 下载DataBaker.unitypackage
  2. 在Unity中双击导入
  3. 或选择 Assets > Import Package > Custom Package

方式三: 从源码导入

  1. Assets/DataBaker 文件夹复制到你的项目 Assets 目录下
  2. Unity会自动编译脚本

安装Addressables (如果未安装)

  1. 打开 Window > Package Manager
  2. 选择 Unity Registry
  3. 搜索 Addressables
  4. 点击 Install

快速开始

第一步: 创建Excel表格

创建一个Excel文件,包含以下格式的数据:

行号 内容 说明
1 字段名称 如: Id,Name,Count
2 字段类型 如: int,string,int[]
3 默认值 可选的默认值
4+ 数据行 实际配置数据

示例表格内容:

1
2
3
4
5
Id,Name,Level,Attack,Skills
int,string,int,int,string[]
0,"新手",1,10,"[]"
1,"战士",10,50,"[冲锋,斩击]"
2,"法师",15,80,"[火球,冰霜,闪电]"

第二步: 编辑数据并自动导出CSV

DataBaker 提供了集成了宏的Excel模板,可以自动将数据导出为CSV文件:

  1. 打开 Assets/DataBaker/Tables/conf~/excel 目录下的Excel模板文档(.xlsm格式)
  2. 根据第一步的格式要求编辑数据
  3. 保存Excel文件时,宏会自动将当前工作表导出为CSV文件到 Assets/DataBaker/Tables 目录
  4. CSV文件名与工作表名称一致

注意事项:

  • Excel文件必须启用宏功能(启用内容)
  • 只保存当前活动的工作表为CSV
  • 如果有多个工作表,每个工作表会生成对应的CSV文件
  • CSV文件会覆盖同名文件,请确保工作表名称正确

第三步: 生成代码和二进制文件

  1. 打开DataBaker编辑器: Tools > DataBaker > CSV Generator
  2. 配置路径:
    • CSV Folder Path: 放置CSV文件的目录
    • C# Class Output Path: 生成的C#类输出目录
    • Binary File Path: 生成的二进制文件输出目录
  3. 选择要处理的CSV文件
  4. 点击 Generate All 按钮
  5. 等待生成完成

第四步: 注册表格

打开 CSVTableRegistry.cs,在 RegisterTables() 方法中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void RegisterTables()
{
_tables.Clear();
_tableDict.Clear();

Debug.Log("=== Starting to register configuration tables ===");

// 手动注册所有配置表
// 添加新表后在这里添加一行注册代码: RegisterTable(YourTable.Instance);
RegisterTable(DataBaker.Generated.TestDataTable.Instance);

Debug.Log($"=== Configuration tables registration completed, total {_tables.Count} tables ===");
}

第五步: 加载配置数据

方式A: 使用GameConfigLoader (推荐)

GameConfigLoader 脚本添加到启动场景中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using DataBaker.Runtime;

public class GameManager : MonoBehaviour
{
void Start()
{
GameConfigLoader.OnConfigsLoaded += OnConfigsLoaded;
}

void OnDestroy()
{
GameConfigLoader.OnConfigsLoaded -= OnConfigsLoaded;
}

void OnConfigsLoaded(bool success)
{
if (success)
{
Debug.Log("配置加载成功,开始游戏逻辑");
// 开始你的游戏逻辑
}
else
{
Debug.LogError("配置加载失败");
}
}
}

方式B: 手动加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using DataBaker.Runtime;
using System.Threading.Tasks;

public class ConfigLoader : MonoBehaviour
{
async void Start()
{
// 初始化管理器
CSVTableManager.Instance.Initialize();

// 加载所有配置表
bool success = await CSVTableManager.Instance.LoadAllTablesAsync();

if (success)
{
Debug.Log("配置加载成功");
// 开始游戏逻辑
}
}
}

第六步: 使用配置数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通过ID获取单个数据
var itemData = CSVTableManager.Instance.GetData<ItemData>("Item", 1001);
if (itemData != null)
{
Debug.Log($"物品名称: {itemData.Name}");
Debug.Log($"价格: {itemData.Price}");
}

// 获取所有数据
var allItems = CSVTableManager.Instance.GetAllData<ItemData>("Item");
foreach (var item in allItems)
{
Debug.Log($"ID: {item.Id}, Name: {item.Name}");
}

// 获取表对象
var itemTable = CSVTableManager.Instance.GetTable<ItemDataTable>("Item");
if (itemTable != null)
{
var item = itemTable.GetInstance(1001);
var allData = itemTable.GetAllData();
var expensiveItems = itemTable.FindData(x => x.Price > 1000);
}

CSV格式规范

CSV文件结构

1
2
3
4
5
6
7
8
9
10
11
12
// 第1行: 字段名称 (必须以Id开头)
Id,Name,Level,Attack,Skills

// 第2行: 字段类型
int,string,int,int,string[]

// 第3行: 默认值 (可选)
0,"",0,0,"[]"

// 第4行及以后: 数据行
1,"战士",10,50,"[冲锋,斩击]"
2,"法师",15,80,"[火球,冰霜,闪电]"

字段类型规范

类型标识 C#类型 示例值
int / integer int 100, -50
float / single float 3.14, -0.5
double / decimal double 3.14159
bool / boolean bool true, false
string / text string “Hello World”
int[] int[] “[1,2,3]”
float[] float[] “[1.5,2.5,3.5]”
string[] string[] “[a,b,c]”

重要规则

  1. 必须以Id字段开头: 第一列必须是Id,类型为int
  2. 字段名称和类型必须匹配: 第1行的字段数必须等于第2行的类型数
  3. 数组格式: 数组值必须使用方括号 [] 包裹,元素用逗号分隔
  4. 字符串包含逗号: 如果字符串中包含逗号,请用双引号包裹
  5. 空值: 使用空字符串 "" 表示空值

示例CSV文件

1
2
3
4
5
6
7
Id,Name,Description,Level,Exp,Attributes,IsActive,Tags
int,string,string,int,int,int[],bool,string[]
0,"","",0,0,"[]",false,"[]"
1,"生命药水","恢复100点生命值",1,0,"[100,0,0]",true,"[消耗,药品]"
2,"魔法药水","恢复50点魔法值",1,0,"[0,50,0]",true,"[消耗,药品]"
3,"青铜剑","基础攻击力+15",5,100,"[0,0,15]",true,"[装备,武器]"
4,"铁甲","基础防御力+20",10,200,"[0,20,0]",true,"[装备,防具]"

API参考

CSVTableManager

配置表管理器,负责所有配置表的加载和管理。

主要属性

1
2
3
4
5
6
7
8
9
10
11
// 单例实例
public static CSVTableManager Instance { get; }

// 是否已初始化
public bool IsInitialized { get; }

// 已加载的表数量
public int LoadedTableCount { get; }

// 总表数量
public int TotalTableCount { get; }

主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 初始化管理器
public void Initialize()

// 异步加载所有配置表
public Task<bool> LoadAllTablesAsync()

// 异步加载指定表
public Task<bool> LoadTableAsync(string assetLabel)

// 通过ID获取数据
public T GetData<T>(string tableAssetLabel, int id)

// 获取表的所有数据
public List<T> GetAllData<T>(string tableAssetLabel)

// 获取表对象
public IDataTable GetTable(string assetLabel)
public TTable GetTable<TTable>(string assetLabel)

// 检查表是否已加载
public bool IsTableLoaded(string assetLabel)

// 卸载指定表
public void UnloadTable(string assetLabel)

// 卸载所有表
public void UnloadAllTables()

// 重新加载表
public Task<bool> ReloadTableAsync(string assetLabel)

// 获取已加载表信息
public string GetLoadedTableInfo()

DataTableBase

数据表基类,所有生成的表管理类都继承此类。

主要属性

1
2
3
4
5
6
7
8
9
10
11
// 是否已加载
public bool IsLoaded { get; }

// 数据数量
public int Count { get; }

// Addressable标签 (子类实现)
public abstract string AssetLabel { get; }

// 二进制资源键 (子类实现)
public abstract string BinaryAssetKey { get; }

主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 异步加载数据
public Task<bool> LoadAsync()

// 同步加载数据 (协程)
public IEnumerator LoadSync(Action onComplete = null)

// 通过ID获取数据
public T GetInstance(int id)

// 获取所有数据
public List<T> GetAllData()

// 根据条件查找数据
public List<T> FindData(Func<T, bool> predicate)

// 查找第一个匹配的数据
public T FindFirst(Func<T, bool> predicate)

// 检查是否包含指定ID
public bool Contains(int id)

// 卸载数据
public void Unload()

// 重新加载
public Task<bool> ReloadAsync()

CSVTableRegistry

配置表注册中心,管理所有表实例。

主要属性

1
2
3
4
5
6
7
8
// 单例实例
public static CSVTableRegistry Instance { get; }

// 总表数量
public int TotalTableCount { get; }

// 已加载表数量
public int LoadedTableCount { get; }

主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 注册所有表 (手动添加注册代码)
public void RegisterTables()

// 获取所有表
public List<IDataTable> GetAllTables()

// 通过标签获取表
public IDataTable GetTable(string assetLabel)

// 获取指定类型的表
public T GetTable<T>(string assetLabel)

// 通过ID获取数据
public T GetData<T>(string tableAssetLabel, int id)

// 获取表的所有数据
public List<T> GetAllData<T>(string tableAssetLabel)

// 检查表是否已加载
public bool IsTableLoaded(string assetLabel)

高级特性

自定义命名空间

  1. 打开CSV Generator窗口
  2. 勾选 Use Namespace
  3. 输入你的命名空间,例如: MyGame.Config
  4. 生成的类将使用该命名空间

按需加载

1
2
3
4
5
// 只加载需要的表
await CSVTableManager.Instance.LoadTableAsync("Item");

// 加载完成后使用
var itemData = CSVTableManager.Instance.GetData<ItemData>("Item", 1001);

卸载不用的表

1
2
3
4
5
// 卸载单个表
CSVTableManager.Instance.UnloadTable("Item");

// 卸载所有表
CSVTableManager.Instance.UnloadAllTables();

数据筛选和查询

1
2
3
4
5
6
7
8
9
10
var itemTable = CSVTableManager.Instance.GetTable<ItemDataTable>("Item");

// 查找价格大于1000的物品
var expensiveItems = itemTable.FindData(x => x.Price > 1000);

// 查找名称包含"剑"的物品
var swords = itemTable.FindData(x => x.Name.Contains("剑"));

// 查找第一个匹配条件的物品
var firstWeapon = itemTable.FindFirst(x => x.Tags.Contains("武器"));

异步/同步加载

异步加载 (推荐)

1
2
3
4
5
6
7
8
async void Start()
{
bool success = await CSVTableManager.Instance.LoadAllTablesAsync();
if (success)
{
// 使用数据
}
}

同步加载 (协程)

1
2
3
4
5
6
7
8
9
IEnumerator Start()
{
var itemTable = CSVTableManager.Instance.GetTable<ItemDataTable>("Item");
yield return itemTable.LoadSync(() =>
{
Debug.Log("加载完成");
// 使用数据
});
}

调试信息

1
2
3
4
5
6
// 打印所有表的加载状态
string info = CSVTableManager.Instance.GetLoadedTableInfo();
Debug.Log(info);

// 运行时查看GameConfigLoader的调试UI
// 点击"Print Load Status"按钮查看详细信息

常见问题

Q1: Addressable设置未找到

问题: 提示 “Addressable settings not found”

解决:

  1. 确保 Addressables 包已正确安装
  2. 打开 Window > Asset Management > Addressables > Groups
  3. 确保Addressables Asset Settings已创建

Q2: CSV文件格式错误

问题: 提示 “CSV file format error”

解决:

  • 确保CSV至少有4行(字段名、类型、默认值、数据)
  • 第一字段必须是Id
  • 字段名和类型数量必须匹配
  • 确保CSV使用UTF-8编码

Q3: 生成的代码报错

问题: 生成的C#类有编译错误

解决:

  • 检查CSV中的类型标识是否正确
  • 确保数组值使用正确的格式 [1,2,3]
  • 检查字符串是否包含未转义的引号

Q4: 数据加载失败

问题: 运行时提示数据加载失败

解决:

  • 检查Addressable Group中的资源路径是否正确
  • 确保二进制文件已生成
  • 检查Addressable Label是否与CSV文件名一致
  • 查看Console中的详细错误信息

Q5: WebGL平台数据加载问题

问题: WebGL上数据无法加载

解决:

  • 确保Addressables配置正确
  • 检查压缩设置
  • 确保Addressables服务器已启动(开发模式)

Q6: 如何更新已存在的表?

解决:

  1. 修改Excel数据
  2. 重新导出CSV
  3. 在CSV Generator中点击 Generate All
  4. Unity会自动刷新资源和脚本

Q7: 能否支持嵌套数据结构?

解决:
目前不支持嵌套类。建议:

  • 使用数组类型存储相关数据
  • 或者将复杂数据拆分为多个表
  • 使用ID建立表之间的关联

Q8: 如何支持Excel中的公式?

解决:
DataBaker不支持Excel公式,因为CSV只存储值。建议:

  1. 在Excel中使用公式计算
  2. 使用”复制-选择性粘贴-值”将公式结果转换为固定值
  3. 然后再导出为CSV

Q9: 生成的文件太大?

解决:
DataBaker使用GZip压缩,已经非常高效。如果仍然太大:

  • 考虑拆分大表为多个小表
  • 减少不必要的字符串字段
  • 使用数值枚举代替字符串

Q10: 多人协作时的冲突处理?

解决:
建议:

  1. 使用Excel的”共享工作簿”功能
  2. 指定专人负责维护配置表
  3. 使用版本控制系统管理CSV文件
  4. 配置表更新时通知团队成员

最佳实践

1. 表设计原则

  • 使用有意义的字段名称(英文)
  • 保持字段类型一致
  • 合理使用默认值
  • 为相关字段添加注释

2. 数据组织

  • 按功能模块划分表
  • 单个表不宜过大(<1000行)
  • 合理使用数组减少表数量
  • 建立表之间的关联关系

3. 命名规范

  • CSV文件名: PascalCase (如: ItemData.csv)
  • 字段名: PascalCase (如: ItemName)
  • 类名: 自动从文件名生成 (如: ItemData, ItemDataTable)

4. 性能优化

  • 按需加载,不要一次性加载所有表
  • 及时卸载不用的表
  • 缓存常用数据引用
  • 使用FindData而不是遍历

5. 版本控制

  • 将CSV文件纳入版本控制
  • 生成的C#类也纳入版本控制
  • 二进制文件通常不纳入版本控制(使用.gitignore)
  • 记录每次配置变更的原因

许可证

本插件受 Unity Asset Store End User License Agreement 约束。

通过购买或使用本插件,您同意遵守 Unity Asset Store 服务条款

使用许可

  • 在任意数量的商业或个人项目中使用
  • 修改源代码以适应您的项目需求
  • 将插件打包到最终发布的游戏产品中

使用限制

  • 不得转售、分发或共享源代码
  • 不得声称本插件是您自己的作品
  • 不得用于创建竞争性产品


联系方式


致谢

  • 感谢Unity引擎提供强大的开发环境
  • 感谢Addressables团队提供的优秀资源管理系统
  • 感谢所有使用和改进DataBaker的开发者

更新日志

v1.0.0 (2025-01-18)

  • 初始版本发布
  • 支持CSV到二进制转换
  • 自动生成C#类和表管理器
  • 集成Addressable资源管理
  • 支持多种数据类型和数组
  • 注册表模式,零反射
  • 完美兼容WebGL和iOS平台

让配置管理变得简单高效!

一个功能强大、易于使用的Unity UI管理和动画框架

目录

框架概述

AnimaFlow UI是一个专为Unity开发的UI管理框架,提供完整的UI生命周期管理、丰富的动画效果和性能优化。

主要特性

  • 双模式管理: 支持单例UI和多重实例UI
  • 丰富动画: 位移、缩放、透明度、旋转四种动画类型
  • 智能资源管理: 集成Addressables,支持对象池
  • 层级管理: 完善的UI层级系统,支持等待动画
  • 性能优化: 异步加载、内存管理、防重复加载

依赖项

  • Unity 2020.3+
  • DOTween (HOTween v2)(require)
  • Addressables (require)
  • TextMesh Pro (require)

快速开始

1. 环境配置

确保项目中已安装以下包:

1
2
3
// Package Manager中安装
- Addressables (1.19.19+),安装好后,创建好Addressable配置文件(否则提示:未找到Addressable Asset Settings!请确保项目已正确配置Addressables)
- DOTween (HOTween v2)

2. 场景设置(自动化)

AnimaFlow UI提供了自动化的场景设置工具,可以一键创建完整的UI结构。

方法:使用编辑器菜单(推荐),也可手动

  1. 创建Canvas结构

    1
    菜单栏 → Tools → AnimaFlow UI → Create Canvas Structure
  2. 创建示例UI面板

    1
    菜单栏 → Tools → AnimaFlow UI → Create UI Panel Object
  3. 检查Addressable配置

    1
    菜单栏 → Tools → AnimaFlow UI → Check Addressable Assets
  4. 修复Addressable配置(如果需要)

    1
    菜单栏 → Tools → AnimaFlow UI → Fix Addressable Assets

自动创建的功能包括:

完整的Canvas层级结构

  • Screen Space Overlay渲染模式
  • Canvas Scaler适配不同分辨率
  • Graphic Raycaster用于UI交互

9个标准UI层级

  • MultipleBase/MultipleTop(多实例层)
  • SingleNormal/SingleTop(单例层)
  • SingleMessageBox/SingleTips(消息提示层)
  • SingleGuide/SingleLoading(引导加载层)
  • SingleAlert(最高紧急层)
  • WaitAnimation(等待动画层)

EventSystem自动创建

  • 完整的输入事件系统
  • 支持鼠标、触摸、键盘输入

Addressable资源检查

  • 自动验证UILoginAnimation配置
  • 支持一键修复配置问题
  • 编辑器启动时自动检查

3. 基本使用

UIMainPanel、UITipPanel为面板的绑定脚本

MainPanel、TipPanel为Addressable中的Key

1
2
3
4
5
// 显示单例UI
UISingletonManager.Instance.ShowUI<UIMainPanel>("MainPanel", UILayer.SingleNormal);

// 显示多实例UI
UIMultiInstanceManager.Instance.ShowUI<UITipPanel>("TipPanel", 1, UILayer.MultipleTop);

核心功能

1. UI层级系统 (UILayer)

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum UILayer
{
MultipleBase,// 多层基础UI
MultipleTop,// 多层顶层UI
SingleNormal, // 基础UI层(最低层级)
SingleMessageBox, // 消息对话框
SingleTop, // 常驻顶层UI
SingleTips, // 临时提示
SingleGuide, // 新手引导
SingleLoading, // 加载界面
SingleAlert, // 最高层级(紧急通知)
WaitAnimation, // 显示加载动画
}

2. UI动画系统配置

2.1 添加面板动画组件

步骤

  1. 在Hierarchy中选择UI面板
  2. 点击Inspector中的Add Component按钮
  3. 选择路径:AnimaFlow UI → UIAnimation

功能

  • 配置面板的进入/退出动画
  • 支持位移、缩放、透明度、旋转四种动画类型
  • 提供5种预设动画效果(Smooth/Bouncy/Elastic/Fast/Dramatic)

2.2 添加动画事件监听器

步骤

  1. 确保已添加UIAnimation组件
  2. 点击Add Component
  3. 选择路径:AnimaFlow UI → UIAnimationEvent

功能

  • 监听动画开始/结束事件
  • 可绑定自定义逻辑(如:播放音效、触发后续动画)

事件类型

1
2
3
4
5
6
7
8
9
10
11
// 进入动画开始
public UnityEvent OnEnterStart;

// 进入动画完成
public UnityEvent OnEnterComplete;

// 退出动画开始
public UnityEvent OnExitStart;

// 退出动画完成
public UnityEvent OnExitComplete;

2.3 添加子组件动画

步骤

  1. 在Hierarchy中选择UI子对象(如Button/Text)
  2. 点击Add Component
  3. 选择路径:AnimaFlow UI → UIComponentAnimation

功能

  • 为子对象单独配置动画效果
  • 子对象可以通过配置调用
  • 子对象动画也可以主动调用
  • 支持父组件控制(通过Parent Component Control设置)
  • 支持Canvas Group交互控制

调用示例

1
2
3
4
5
6
7
8
// 进入动画定义
UIComponentAnimationConfig.PlayEnterAnimation();
//调用:
gameObject.GetComponent<UIComponentAnimation>().PlayEnterAnimation();
// 退出动画定义
UIComponentAnimationConfig.PlayExitAnimation();
//调用:
gameObject.GetComponent<UIComponentAnimation>().PlayExitAnimation();

父组件控制触发时机

1
2
3
4
5
6
7
public enum UIParentAnimationTrigger
{
OnEnterStart, // 面板进入动画开始时触发
OnEnterCompleted, // 面板进入动画完成时触发
OnExitStart, // 面板退出动画开始时触发
OnExitCompleted // 面板退出动画完成时触发
}

2.4 动画组件关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────┐
│ UI面板 (GameObject) │
│ │
│ ● UIAnimation │ ← 面板主动画
│ ● UIAnimationEvent │ ← 面板动画事件
│ │
│ ┌──────────────────┐ │
│ │ Button子对象 │ │
│ │ │ │
│ │ ● UIComponent │ │ ← 子组件独立动画
│ │ Animation │ │
│ └──────────────────┘ │
└───────────────────────┘

动画系统

预设动画类型

  • Simple: 简单动画(位移+缩放)
  • Smooth: 平滑过渡
  • Bouncy: 弹性效果
  • Elastic: 弹性动画
  • Fast: 快速动画
  • Dramatic: 戏剧化效果
  • Slide: 滑动效果
  • FadeSlide: 渐变滑动效果
  • NoAnimation: 无动画

API文档

UISingletonManager - 单例UI管理器

显示UI

1
2
3
4
5
6
// 基本显示
ShowUI<T>(string key, UILayer layer)

// 完整参数
ShowUI<T>(string key, UILayer layer, Action<T> onLoadedAction,
bool isLoadedActionFirst = false, bool isOnce = true)

关闭UI

1
2
3
4
5
6
// 关闭UI
CloseUI<T>(string key, bool isOnce = true)

// 切换UI显示状态
SwitchUI<T>(string key, UILayer layer, Action<T> onLoadedAction = null,
bool isLoadedActionFirst = false)

销毁UI

1
2
3
4
5
// 销毁单个UI
DestroyUI(string key)

// 销毁所有UI
DestroyAllUI()

UIMultiInstanceManager - 多实例UI管理器

显示多实例UI

1
2
3
4
5
6
7
// 显示单个实例
ShowUI<T>(string key, int id, UILayer layer, Action<T> onLoadedAction = null,
bool isLoadedActionFirst = false, bool isOnce = true)

// 批量显示实例
ShowMultipleUI<T>(string key, IEnumerable<int> ids, UILayer layer,
Action<T, int> onLoadedAction = null)

实例管理

1
2
3
4
5
6
// 获取实例
GetUI<T>(string key, int id)
TryGetUI<T>(string key, int id, out T uiBase)

// 检查实例
HasInstance(string key, int id)

UIBase - UI抽象基类(不能直接继承)

UIBase类是一个抽象基类,提供了UI的核心动画功能和生命周期管理。重要:不能直接继承UIBase类,必须选择继承UIBaseMultiple或UIBaseSingleton。

继承关系说明

1
2
3
4
5
6
7
8
// 错误:直接继承UIBase
public class MyUI : UIBase { } // 不允许!

// 正确:继承UIBaseMultiple(多实例UI)
public class MyMultiUI : UIBaseMultiple { }

// 正确:继承UIBaseSingleton<T>(单例UI)
public class MySingletonUI : UIBaseSingleton<MySingletonUI> { }

新增功能

可见性属性

1
2
public bool GetVisibility { get; }
// 返回UI是否真正可见(基于CanvasGroup.alpha或activeSelf)

简单显示/隐藏方法

1
2
3
4
5
// 简单显示(无动画)
protected void SetSimpleShow()

// 简单隐藏(无动画)
protected void SetSimpleClose()

动画回调

1
2
3
4
5
6
7
// 退出动画完成回调
protected System.Action onExitComplete;

// 使用示例:
OnExitAction(() => {
Debug.Log("退出动画完成");
});

基础功能

1
2
3
4
5
6
7
8
// 显示UI
public void OnEnterAction()

// 隐藏UI(支持完成回调)
public void OnExitAction(System.Action onComplete = null)

// 刷新UI
public virtual void Refresh(string strMsg = null)

动画控制

1
2
3
4
5
6
7
// 预设动画
protected void DirectionToCenter(UIAnimationDirection direction)
protected void CenterToDirection(UIAnimationDirection direction)

// 自定义动画
protected void CustomToCenter(UIAnimationParameter animationParameter)
protected void CenterToCustom(UIAnimationParameter animationParameter)

UIAnimation - 动画配置组件

属性配置

1
2
3
4
5
6
7
8
// 预设
public AnimationPreset Preset { get; set; }

// 动画参数
public float OffsetX { get; }
public float OffsetY { get; }
public bool EnablePositionEnterAnimation { get; }
// ... 更多属性

公共方法

1
2
3
4
5
// 应用预设
public void ApplyPreset(AnimationPreset preset)

// 停止动画
public void StopAnimation()

示例代码

基本UI使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class GameManager : MonoBehaviour
{
private void Start()
{
// 显示主菜单
UISingletonManager.Instance.ShowUI<UIMainPanel>("MainPanel", UILayer.SingleNormal);
}

private void ShowTip()
{
// 显示提示框(多实例)
UIMultiInstanceManager.Instance.ShowUI<UITipPanel>("TipPanel", 1, UILayer.MultipleTop,
(ui) => ui.SetMessage("Hello World!"));
}

private void CloseTip()
{
// 关闭提示框
UIMultilnstanceManager.Instance.CloseUI("TipPanel", 1);
}
}

自定义UI类

单例UI示例(继承UIBaseSingleton)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class UIMainPanel : UIBaseSingleton<UIMainPanel>
{
[SerializeField] private Text titleText;
[SerializeField] private Button startButton;

protected override void Start()
{
base.Start();

// 设置按钮事件
startButton.onClick.AddListener(OnStartClicked);
}

private void OnStartClicked()
{
// 隐藏当前面板
OnExitAction();

// 显示游戏界面
UISingletonManager.Instance.ShowUI<UIGamePanel>("GamePanel", UILayer.SingleNormal);
}

public override void Refresh(string strMsg = null)
{
if (!string.IsNullOrEmpty(strMsg))
{
titleText.text = strMsg;
}
}

protected override void PlayAppearAnimation()
{
// 自定义进入动画
DirectionToCenter(UIAnimationDirection.Top);
}
}

多实例UI示例(继承UIBaseMultiple)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UITipPanel : UIBaseMultiple
{
[SerializeField] private Text tipText;
[SerializeField] private Button closeButton;

public override void InitializeComponent()
{
base.InitializeComponent();

// 设置关闭按钮事件
closeButton.onClick.AddListener(() => OnExitAction());
}

public void SetMessage(string message)
{
if (tipText != null)
{
tipText.text = message;
}
}

public override void Refresh(string strMsg = null)
{
if (!string.IsNullOrEmpty(strMsg))
{
SetMessage(strMsg);
}
}

protected override void PlayAppearAnimation()
{
// 多实例UI使用弹性动画
DirectionToCenter(UIAnimationDirection.Top);
}
}

动画配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CustomAnimationUI : UIBaseSingleton<CustomAnimationUI>
{
protected override void PlayAppearAnimation()
{
// 创建自定义动画参数
var animParams = new UIAnimationParameter
{
EnablePositionEnterAnimation = true,
AppearDirection = UIAnimationDirection.Random,
AppearEase = AnimaFlowEase.OutBack,

EnableScaleEnterAnimation = true,
ScaleFrom = 0.5f,
ScaleTo = 1f,
AppearScaleEase = AnimaFlowEase.OutElastic,

AppearDuration = 1.0f
};

// 应用自定义动画
CustomToCenter(animParams);
}
}

使用自定义位置动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomPositionUI : UIBaseSingleton<CustomPositionUI>
{
protected override void PlayAppearAnimation()
{
// 创建自定义位置动画参数
var animParams = new UIAnimationParameter
{
UseCustomPositions = true,
EnterPosition = new Vector2(-500, 0), // 从左侧进入
DisplayPosition = Vector2.zero, // 显示在中心
ExitPosition = new Vector2(500, 0), // 向右侧退出

EnablePositionEnterAnimation = true,
EnablePositionExitAnimation = true,
AppearDuration = 0.5f,
DisappearDuration = 0.5f
};

// 应用自定义位置动画
CustomToCenter(animParams);
}
}

使用自定义缓动曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CustomCurveUI : UIBaseSingleton<CustomCurveUI>
{
protected override void PlayAppearAnimation()
{
// 创建自定义动画曲线
AnimationCurve customCurve = new AnimationCurve(
new Keyframe(0, 0, 0, 2),
new Keyframe(0.5f, 0.8f, 2, -1),
new Keyframe(1, 1, -1, 0)
);

// 创建使用自定义曲线的动画参数
var animParams = new UIAnimationParameter
{
EnablePositionEnterAnimation = true,
AppearDirection = UIAnimationDirection.Top,
AppearEase = AnimaFlowEase.Custom, // 必须设置为Custom
AppearCurve = customCurve, // 提供自定义曲线

EnableScaleEnterAnimation = true,
ScaleFrom = 0.1f,
ScaleTo = 1f,
AppearScaleEase = AnimaFlowEase.Custom,
AppearScaleCurve = customCurve,

AppearDuration = 0.75f
};

CustomToCenter(animParams);
}
}

世界空间UI跟踪示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class WorldTrackingExample : MonoBehaviour
{
public Transform targetObject;
public RectTransform canvasRect;
private Camera uiCamera;

void Start()
{
uiCamera = Camera.main;
}

public void ShowTrackingUI()
{
// 将3D世界坐标转换为屏幕坐标
Vector3 screenPos = uiCamera.WorldToScreenPoint(targetObject.position);

if (screenPos.z > 0) // 确保物体在相机前方
{
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(
canvasRect, screenPos, null, out Vector2 localPos))
{
// 使用自定义位置参数显示UI
UIMultiInstanceManager.Instance.ShowUI<WorldTrackingUIPanel>(
"WorldTrackingUIPanel",
"TrackID",
UILayer.MultipleTop,
ui =>
{
ui.worldTarget = targetObject;
ui.canvasRectTrans = canvasRect;
ui.CustomAheadParmater(localPos, localPos); // 设置进入和退出位置
},
true); // isLoadedActionFirst=true确保位置设置后再播放动画
}
}
}
}

最佳实践

1. 资源管理

1
2
3
4
5
6
7
8
9
10
// 正确使用Addressables
public class GameManager : MonoBehaviour
{
private void OnDestroy()
{
// 清理UI资源
UISingletonManager.Instance.DestroyAllUI();
UIMultilnstanceManager.Instance.DestroyAllUI();
}
}

2. 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用对象池管理频繁显示的UI
public class TipManager : MonoBehaviour
{
private void ShowTemporaryTip(string message)
{
// 使用对象池显示提示
UIMultiInstanceManager.Instance.ShowUI<UITipPanel>("TipPanel", GetNextId(),
UILayer.SingleTips, (ui) => ui.SetMessage(message));
}

private int GetNextId()
{
// 实现ID生成逻辑
return Time.frameCount % 1000;
}
}

3. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
// 安全的UI操作
public void SafeShowUI(string key)
{
try
{
UISingletonManager.Instance.ShowUI<UIBase>(key, UILayer.SingleNormal);
}
catch (Exception e)
{
Debug.LogError($"Failed to show UI {key}: {e.Message}");
}
}

编辑器工具详解

1. Check Addressable AssetsFix

功能

  • 检查场景中UILoginAnimation预设体的Addressable配置
  • 自动修复配置问题(Key=”UILoginAnimation”)

使用方法

1
2
3
4
5
6
7
// 代码调用
UIAddressableAssetChecker.CheckAddressableAssets();
UIAddressableAssetChecker.FixAddressableAssets();

// 菜单路径
AnimaFlow UI → Check Addressable Assets
AnimaFlow UI → Fix Addressable Assets

2. Create Canvas Structure

功能

  • 在新场景中创建标准Canvas层级结构
  • 包含9个预设层级(UILayer枚举)
  • 自动配置Canvas Scaler和EventSystem

使用方法

1
2
3
4
5
// 代码调用
CreateCanvasStructureEditor.CreateCanvasStructure();

// 菜单路径
AnimaFlow UI → Create Canvas Structure

3. Create UI Panel Object

功能

  • 在指定层级创建示例UI面板
  • 自动添加RectTransform、CanvasGroup等组件

使用方法

1
2
3
4
5
// 代码调用
CreateCanvasStructureEditor.CreateUIPanelExample();

// 菜单路径
AnimaFlow UI → Create UI Panel Object

4. Create UI Panel Script

功能

  • 生成UI脚本模板(支持单例/多实例)
  • 自动添加基础生命周期方法

代码模板示例

1
2
3
4
5
6
7
8
9
// 单例UI模板
public class NewUIPanel : UIBaseSingleton<NewUIPanel>
{
protected override void Start()
{
base.Start();
// 添加你的初始化代码
}
}

5. Path UI Crawler V1.0

功能

  • 可视化拖拽绑定UI组件
  • 生成组件引用代码(在多人协同开发中,热更新开发中,大部分使用代码绑定事件)

示例

1
2
3
// 自动生成的绑定代码
[SerializeField] private Button submitButton;
[SerializeField] private Text titleText;

6. Sample Setup Panel Addressables

功能

  • 批量设置示例场景中预设体的Addressable Key
  • 确保所有示例资源可正确加载

配置规则

  • 关键资源Key=”UILoginAnimation”
  • 其他资源Key=文件名

示例场景使用指南

可用示例场景

  1. SingleAndMultiple - 单例和多实例UI示例

    • 演示基本的单例UI和多实例UI使用
    • 展示不同层级的UI管理
  2. No Components - 无组件动画示例

    • 演示不使用动画组件的UI面板
    • 纯代码驱动的动画效果
  3. SubcomponentAnim - 子组件动画示例

    • 演示子组件独立动画
    • 展示父组件控制子组件动画的时机
  4. AnimationEvent - 动画事件示例

    • 演示动画事件监听
    • 展示在动画开始/完成时触发自定义逻辑
  5. WorldTracking - 世界空间UI跟踪示例

    • 演示UI跟随3D物体的功能
    • 支持固定位置和跟踪模式

配置步骤

  1. 导入示例包

    • 从Samples目录导入示例场景
  2. 配置Addressables

    • 运行菜单:
    • 1.Tools →AnimaFlow UI → Check Addressable Assets`
    • 2.Tools → AnimaFlow UI → Fix Addressable Assets`
    • 3.Tools → AnimaFlow UI → Sample Setup Panel Addressables
  3. 测试功能

    • 运行示例场景验证UI加载和动画效果
  4. 自定义修改

    • 通过Path UI Crawler调整组件绑定
    • 修改UIAnimationConfig调整动画效果

世界空间UI跟踪功能详解

WorldTrackingUIPanel 提供了UI跟随3D物体的功能,适用于:

  • 角色头顶名称显示
  • 物体选中高亮
  • NPC对话提示框

核心功能

1
2
3
4
5
6
7
8
9
// 设置UI跟随目标
ui.worldTarget = targetTransform;
ui.canvasRectTrans = canvasRect;

// 使用自定义位置参数(isLoadedActionFirst=true确保动画播放前设置位置)
UIMultiInstanceManager.Instance.ShowUI<WorldTrackingUIPanel>("WorldTrackingUIPanel", "A", UILayer.MultipleTop, ui =>
{
ui.CustomAheadParmater(enterPos, exitPos);
}, true);

两种模式

  1. 跟踪模式:UI实时跟随3D物体移动
  2. 固定模式:UI在固定位置显示

注意事项

  • 需要正确设置UI Camera
  • Canvas使用Screen Space Overlay模式
  • 物体必须在相机前方(z > 0)

智能特性

  • 自动检测现有Canvas:如果场景中已有Canvas,会询问是否使用现有Canvas
  • 完整的UI层级创建:基于UILAYER枚举自动创建所有层级
  • EventSystem自动设置:确保输入系统正常工作
  • Canvas优化配置
    • Screen Space Overlay渲染模式
    • Canvas Scaler自适应分辨率(1920×1080基准)
    • Graphic Raycaster UI交互支持

UIAddressableAssetChecker 类

功能:检查和修复Addressable资源配置

使用方法

1
2
3
4
5
// 检查配置
UIAddressableAssetChecker.CheckAddressableAssets();

// 修复配置
UIAddressableAssetChecker.FixAddressableAssets();

检查项目

  • UILoginAnimation预设体存在性:确保关键资源存在
  • Addressable Key验证:检查”UILoginAnimation” key是否正确
  • 资源类型检查:确保配置为本地资源而非远程资源
  • 自动修复功能:支持一键修复配置问题

自动启动检查

框架会在编辑器启动时自动检查关键配置:

1
2
3
4
5
6
[InitializeOnLoadMethod]
private static void AutoCheckOnStartup()
{
// 自动检查UILoginAnimation配置
// 发现问题时会给出修复建议
}

高级配置选项

自定义Canvas创建

如果你需要自定义Canvas配置,可以修改源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static Canvas CreateNewCanvas()
{
GameObject canvasGO = new GameObject("Canvas");
Canvas canvas = canvasGO.AddComponent<Canvas>();

// 自定义渲染模式
canvas.renderMode = RenderMode.ScreenSpaceCamera; // 可以修改

// 自定义CanvasScaler
CanvasScaler scaler = canvasGO.AddComponent<CanvasScaler>();
scaler.referenceResolution = new Vector2(1334, 750); // 自定义分辨率

return canvas;
}

自定义UI层级

要添加自定义UI层级,需要修改UILAYER枚举:

1
2
3
4
5
6
7
8
public enum UILAYER
{
// 原有层级...

// 添加自定义层级
CustomLayer1, // 自定义层级1
CustomLayer2, // 自定义层级2
}

常见问题

Q: 如何解决UI重复加载的问题?

A: 框架内置了防重复加载机制,使用isOnce参数控制:

1
2
3
// 确保UI只显示一次
UISingletonManager.Instance.ShowUI<UIMainPanel>("MainPanel", UILayer.SingleNormal,
null, false, true); // isOnce = true

Q: 如何自定义UI动画?

A: 有两种方式:

  1. 使用UIAnimationConfig组件在Inspector中配置
  2. 在代码中创建UIAnimationParameter对象

Q: 如何处理UI层级冲突?

A: 框架会自动处理同层级UI的互斥显示,你也可以手动控制:

1
2
// 手动设置层级
uiBase.transform.SetAsLastSibling();

Q: 如何优化UI性能?

A:

  • 使用对象池管理频繁显示的UI
  • 合理设置UI层级,避免不必要的重绘
  • 利用异步加载和等待动画
  • 及时销毁不使用的UI
  • 使用SetSimpleShow/SetSimpleClose避免动画开销
  • 子组件动画支持独立控制,可按需启用

Q: 子组件动画如何工作?

A:

  1. 添加UIComponentAnimation组件到子对象
  2. 设置EnableParentControl为true启用父组件控制
  3. 选择触发时机(OnEnterStart/OnEnterCompleted/OnExitStart/OnExitCompleted)
  4. 可以通过Parent Component Control控制自动禁用/启用交互
  5. 子组件动画可以独立调用PlayEnterAnimation/PlayExitAnimation

Q: 编辑器工具无法正常工作怎么办?

A:

  1. 检查Unity版本:确保使用Unity 2020.3+版本
  2. 检查Addressables包:确保已安装Addressables包
  3. 重新导入包:删除并重新导入AnimaFlow UI包
  4. 检查菜单项:确保Unity菜单栏出现”AnimaFlow UI”菜单项
  5. 查看控制台日志:编辑器工具会输出详细的调试信息

Q: UILoginAnimation配置失败怎么办?

A:

  1. 运行菜单:”AnimaFlow UI → Fix Addressable Assets”
  2. 手动检查:确保UILoginAnimation预设体存在
  3. Addressables配置:确认预设体已添加到Addressables,key为”UILoginAnimation”
  4. 资源类型:确保配置为本地资源而非远程资源

Q: Canvas创建失败或层级错误怎么办?

A:

  1. 删除现有Canvas:先删除场景中的Canvas和EventSystem
  2. 重新创建:运行”AnimaFlow UI → Create Canvas Structure”
  3. 层级检查:确认所有UILAYER枚举值对应的层级都已创建
  4. 手动修复:如果自动创建失败,可以手动创建Canvas和层级

Q: 编辑器启动时检查失败怎么办?

A:

  1. 忽略警告:如果项目配置正确,可以忽略启动检查警告
  2. 手动修复:运行”AnimaFlow UI → Fix Addressable Assets”
  3. 禁用自动检查:如果需要,可以注释掉AutoCheckOnStartup方法

技术支持

快速入门流程

1
2
3
4
5
6
7
8
9
10
// 第一步:在项目初始化时调用
[InitializeOnLoadMethod]
public static void InitializeAnimaFlowUI()
{
// 1. 创建Canvas结构
CreateCanvasStructureEditor.CreateCanvasStructure();

// 2. 检查Addressables配置
UIAddressableAssetChecker.CheckAddressableAssets();
}

批量操作示例

1
2
3
4
5
6
7
8
// 批量创建UI面板示例
public static void CreateMultipleUIPanels()
{
// 创建主菜单面板
CreateCanvasStructureEditor.CreateUIPanelExample();

// 可以继续创建其他面板...
}

自定义配置修改

如果你需要修改默认配置,可以直接编辑相应的编辑器脚本:

  • 修改Canvas分辨率:编辑CreateCanvasStructureEditor.cs中的referenceResolution
  • 修改UI层级:编辑UILAYER.cs枚举定义
  • 修改动画预设:编辑UIAnimation.cs中的预设参数

调试技巧

  1. 查看控制台日志:所有编辑器操作都有详细的日志输出
  2. 使用断点调试:可以在编辑器脚本中设置断点进行调试
  3. 验证配置:使用”AnimaFlow UI → Check Addressable Assets”验证配置
  4. 手动测试:创建测试场景验证UI功能正常

Q: 编辑器工具无法正常工作怎么办?

A:

  1. 检查Unity版本:确保使用Unity 2020.3+版本
  2. 检查Addressables包:确保已安装Addressables包
  3. 重新导入包:删除并重新导入AnimaFlow UI包
  4. 检查菜单项:确保Unity菜单栏出现”AnimaFlow UI”菜单项
  5. 查看控制台日志:编辑器工具会输出详细的调试信息

Q: UILoginAnimation配置失败怎么办?

A:

  1. 运行菜单:”AnimaFlow UI → Fix Addressable Assets”
  2. 手动检查:确保UILoginAnimation预设体存在
  3. Addressables配置:确认预设体已添加到Addressables,key为”UILoginAnimation”
  4. 资源类型:确保配置为本地资源而非远程资源

Q: Canvas创建失败或层级错误怎么办?

A:

  1. 删除现有Canvas:先删除场景中的Canvas和EventSystem
  2. 重新创建:运行”AnimaFlow UI → Create Canvas Structure”
  3. 层级检查:确认所有UILAYER枚举值对应的层级都已创建
  4. 手动修复:如果自动创建失败,可以手动创建Canvas和层级

Q: 编辑器启动时检查失败怎么办?

A:

  1. 忽略警告:如果项目配置正确,可以忽略启动检查警告
  2. 手动修复:运行”AnimaFlow UI → Fix Addressable Assets”
  3. 禁用自动检查:如果需要,可以注释掉AutoCheckOnStartup方法

技术支持

如果你在使用过程中遇到问题,可以通过以下方式获取帮助:

  1. 查看示例场景: 在Assets/AnimaFlowUI/Samples/中查看完整示例
  2. API文档: 参考本文档的API部分
  3. 作者博客: https://iamzhai.github.io/
  4. 联系邮箱: zhaijianwei502@163.com

AnimaFlow UI - 让UI开发变得更简单!

0%