FinalFrameWork使用手册

是一款自用的、简单、强大、现代化的unity框架

关联文章:

🚧施工中:

FinalFrameWork以下简称为FF

1.快速安装

1.1 通过 Unity PackageManager(推荐)

1
https://gitee.com/CodeInYNOU/final-core.git#master

image-20260202093040047

1.2 直接前往 gitee 下载 (对于需要修改源码)

git clone https://gitee.com/CodeInYNOU/final-core.git 到本地

通过Unity PackageManager Add Package from Disk 安装到项目中,通过此方法安装时可以对框架源码进行修改。

如果您对于框架有好想法 😽 ,欢迎Pull Request共享代码!

2.项目依赖

  • DoTween 动画插件 (弹窗组件依赖)
  • UniRX 响应式编程
  • UniTask 更好的Async
  • Odin(付费插件,框架不提供,请自行导入

3.启动框架

3.1 AA设置

FF 默认提供Resource资源加载和Addressable 资源加载,推荐使用Addressable 。

在导入框架时就已经自动安装了Addressable依赖项。 打开Addressles Groups 点击创建一个默认的aa包设置。

image-20260202100949038

3.2 启动器

创建一个启动器用于管理游戏启动流程:

  1. 首先初始化TextMeshPro

image-20260202102015172

  1. 创建一个场景Start 作为启动场景,这个场景中只为了启动游戏流程,不放任何资源。

  2. 创建一个GameBoot 脚本继承自GameStartAbs

1
2
3
4
5
6
7
8
9
10
using FFW.BaseSystems.ResKit;
using FFW.BaseSystems.SceneKit;
using FFW.Core;


public class GameBoot : GameStartAbs
{
protected override IResLoader ResLoader => resLoader ?? new AddressResLoader();
public override ISceneLoader SceneLoader => sceneLoader ?? new SceneLoaderDefault();
}
  1. 将这个脚本挂载在Core游戏对象上(在空场景中创建一个空游戏对象),点击启动!

image-20260202102253216

看到如上日志输出时说明,已完成框架启动!


3.3 扩展启动流程

对于自定义的启动流程最方便的方式是直接在Start中书写:

1
2
3
4
5
6
7
8
9
10
public class GameBoot : GameStartAbs
{
protected override IResLoader ResLoader => resLoader ?? new AddressResLoader();
public override ISceneLoader SceneLoader => sceneLoader ?? new SceneLoaderDefault();

private void Start()
{
//其余的 自定义的启动流程
}
}

FSM管理启动流程

FF中提供了FSM有限状态机工具,使用此工具我们可以将游戏按流程进行划分:

例如下图:

image-20260202103306356

在不同的流程中,我们可以对资源、游戏系统等进行控制,具体使用很自由。

在项目中FFW/Core/States/GameStartFsm.cs提供了一个示例,可参考实现。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
using Cysharp.Threading.Tasks;
using FFW.BaseSystems.ResKit;
using FFW.BaseSystems.SceneKit;
using FFW.BaseSystems.UiKit;
using FFW.Core.Models;
using FFW.Extension.FFSM;
using FFW.Template.HotKeyKit;
using FFW.Template.HotKeyKit.example;
using UniRx;
using Unity.Collections;
using UnityEngine;

namespace FFW.Core.States
{
/// <summary>
/// 以FSM启动游戏 进行流程管理示例
/// </summary>
public class GameStartFsm : GameStartAbs
{
protected override IResLoader ResLoader => new AddressResLoader();
public override ISceneLoader SceneLoader => new SceneLoaderDefault();

private FsmMachine<GameBlackboard> fsmMachine;

[SerializeField, ReadOnly] private string gameState = "菜单状态";

private void Start()
{
this.RegisterSystem(new HotKeySystem());

fsmMachine = new FsmMachine<GameBlackboard>(Blackboard);
fsmMachine.AddState(new MenuGameState("菜单状态", fsmMachine));
fsmMachine.AddState(new PlayGameState("游戏状态", fsmMachine));

fsmMachine.ChangeState("菜单状态");

fsmMachine.OnStateChangeAction += x => { gameState = x.StateName; };

Observable.EveryUpdate().Where(x => Input.GetKeyDown(KeyCode.Space)).Subscribe(x =>
{
fsmMachine.ChangeState("游戏状态");
}).AddTo(this);
}
}

public class MenuGameState : FsmReturnStateAbs<GameBlackboard>
{
private UIPanelBase hotkeyPanel;

public MenuGameState(string stateName, FsmMachine<GameBlackboard> fsmMachine) : base(stateName, fsmMachine)
{
}

public override UniTask Exit()
{
hotkeyPanel?.Pop();
return UniTask.CompletedTask;
}

public override UniTask Update()
{
return UniTask.CompletedTask;
}

public override async UniTask<bool> Execute()
{
hotkeyPanel = await GameCore.GetSystem<UISystem>().PushPanel<HotKeySettingPanel>();
return true;
}
}

public class PlayGameState : FsmReturnStateAbs<GameBlackboard>
{
public PlayGameState(string stateName, FsmMachine<GameBlackboard> fsmMachine) : base(stateName, fsmMachine)
{
}

public override UniTask Exit()
{
GameCore.GetSystem<UISystem>().PopWindowManager.CloseAllPopWindows();
return UniTask.CompletedTask;
}

public override UniTask Update()
{
return UniTask.CompletedTask;
}

public override async UniTask<bool> Execute()
{
return true;
}
}
}

3.4 版本号管理和UI创建器

快捷键按下 Ctrl+L 可以打开管理器,如下图:

  1. 点击创建项目资源结构文件夹
  2. 点击选择路径选择AARes
  3. 点击刷新addressable 可以自动扫描文件将其加入aa分组管理。

image-20260202112551301

image-20260202140200183

版本号:用于区分引用版本,默认显示在右下角。

UI模板:可以通过这个创建UI,默认面板以Panel结尾;弹窗以PopWindow结尾。

4.IOC容器

FFW框架,使用类似IOC容器的概念管理 带状态的数据对象:

可进入 GameCore.cs 查看源码

  • Model 带状态的游戏数据实体对象
  • System 带状态的游戏

在任何地方均可使用:

1
2
3
4
this.RegisterSystem(T t); //来注册一个系统
this.GetSystem<T>(); //获取系统
this.RegisterModel(T t);//注册一个数据模型
this.GetModel<T>(); //获取数据模型

5.基础系统

5.1 资源加载

继承自IResLoader,在启动器中使用接口实现。

1
2
var go= await this.GetSystem<IResLoader>().Instantiate<GameObject>("aa");
await this.GetSystem<IResLoader>().LoadAsync<Texture>("bb");

5.2 场景加载

继承自ISceneLoader,在启动器中使用接口实现。LoadSceneMode 自由选择。

1
2
3
4
5
6
7
8
9
var handle= this.GetSystem<ISceneLoader>().LoadSceneAsync("sc", LoadSceneMode.Additive);
handle.completed += x =>
{
Debug.Log("加载完成");
};
while (!handle.isDone)
{
Debug.Log($"加载进度: {handle.progress}");
}

5.3 事件总线

支持事件注册、派发、注销、状态管理等。同时为了维护事件系统的无序性,支持根据优先级进行事件执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
//注册监听
var unHandle = this.RegisterEvent<GameStartEvent>(x => { Debug.Log(x.PlayerName + "游戏启动事件 高优先级"); }, 100);
var unHandle2 = this.RegisterEvent<GameStartEvent>(x => { Debug.Log(x.PlayerName + "游戏启动事件 默认优先级"); });

//发送事件
this.SendEvent(new GameStartEvent() { PlayerName = "AA" });

//移除监听
unHandle?.Unregister();
unHandle2?.Unregister();

//查看事件中心状态
this.GetSystem<EventCenter>().GetAllEventsInfo();

image-20260202143240935

5.4 音效管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//播放音效
this.GetSystem<MusicSystem>().PlayBgmAsync("aa");
this.GetSystem<MusicSystem>().PlayAudioAsync("bb");
//静音
this.GetSystem<MusicSystem>().ConfigAsset.bgmMute.Value = true;
this.GetSystem<MusicSystem>().ConfigAsset.audioMute.Value = true;
this.GetSystem<MusicSystem>().ConfigAsset.totalMute.Value = true;

//设置音量
this.GetSystem<MusicSystem>().ConfigAsset.bgmVolume.Value = 0.5f;
this.GetSystem<MusicSystem>().ConfigAsset.audioVolume.Value = 0.5f;
this.GetSystem<MusicSystem>().ConfigAsset.totalVolume.Value = 0.5f;

//获取音量
Debug.Log(this.GetSystem<MusicSystem>().ConfigAsset.BgmVolume);
Debug.Log(this.GetSystem<MusicSystem>().ConfigAsset.AudioVolume);

5.5 UI管理

UI系统使用栈对UI面板进行管理,将UI划分为:bot、mid、top、popWindow、system 五层。

5.5.1UIPanel的创建和使用

可以使用Packer创建UIPanel,创建后的位置在AARes/UI/Panels/

image-20260202144455673

创建后在目录下Scripts下创建一个C#脚本,挂载在预制体上。点击Packer的刷新Addressable 按钮,会自动将其加入aa管理。

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
using FFW.BaseSystems.UiKit;
using UnityEngine;

public class AAPanel : UIPanelBase
{
protected override void OnInitReady()
{
base.OnInitReady();
Debug.Log("AAPanel初始化完成");
}

public override void OnEnter()
{
base.OnEnter();
Debug.Log("AAPanel显示");
}

public override void OnExit()
{
base.OnExit();
Debug.Log("AAPanel退出");
}

public override void UpdateUI(object data = null)
{
base.UpdateUI(data);
Debug.Log("AAPanel更新");
}
}

image-20260202145053046

1
2
3
4
5
6
7
8
9
//显示UIpanel
var panel= await this.GetSystem<UISystem>().PushPanel<AAPanel>();

//更新UI
panel.UpdateUI(new { PlayerName = "AA" });

//关闭UIpanel
panel.Pop(); //方法1
this.GetSystem<UISystem>().PopPanel<AAPanel>(); //方法2

5.5.2UIView

UIView是UI的最小单位,包含一个UI组件,一个UIPanel下可以有多个UIView。用法基本同UiPanel,继承自:UIViewBase.

在Panel初始化时,View会自动初始化,在Panel OnXXX 事件时,View也会同步执行。

5.5.3UIPopWindow的创建和使用

所有在游戏中需要以弹窗展示的UI内容都可以使用此系统进行处理。

创建同UIPanel,直接在Packer中创建即可,继承自PopWindowAbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AAPopWindow : PopWindowAbs
{
public override void OnInit()
{
base.OnInit();
Debug.Log("AAPopWindow初始化");
}

public override void OnEnter()
{
base.OnEnter();
Debug.Log("AAPopWindow显示");
}

public override void OnExit()
{
base.OnExit();
Debug.Log("AAPopWindow退出");
}
}
1
2
3
4
5
6
7
8
//显示PopWindow
var popWindow = await this.GetSystem<UISystem>().PopWindowManager.PushPopWindow<AAPopWindow>();

//关闭PopWindow
popWindow.OnExit();

//关闭所有PopWindow
this.GetSystem<UISystem>().PopWindowManager.CloseAllPopWindows();

5.5.4确认框Confim

创建一个名位ConfirmationPopWindow的弹窗,该弹窗至少包含如下元素

确认框预制件

挂载ConfirmationPopWindow脚本,并将mask设置上。

设置拖拽

注意:对于确认框来说,我们希望在弹出时遮挡其下的UI操作 ,所以我们设置父布局占满canvas;设置完成后,如果你不希望遮挡,就不需要mask。

  • 完成Prefab后第一次需要点击Packer的刷新aa将其加入到aa包中。
1
2
3
4
5
6
7
this.GetSystem<UISystem>().PopWindowManager.ShowConfirmWindow<ConfirmationPopWindow>("tips", "Are you sure you want to close it?", () =>
{
Debug.Log("确定");
}, () =>
{
Debug.Log("取消");
});

弹窗效果

5.5.5提示Tip

创建预制体、添加CenterTip脚本,并给变量赋值;将其加入aa包中。

创建预制体

1
2
3
4
5
6
7
8
9
//载入预制体,初始化
var centTip = await this.ResLoader.Instantiate<CenterTip>("CenterTip");
this.GetSystem<UISystem>().InitTip(centTip,new Vector2(0,300f));

if (Input.GetKeyDown(KeyCode.K))
{
//显示提示
this.GetSystem<UISystem>().ShowTip("data:"+ Random.Range(0, 1000));
}

提示效果

6.扩展功能

6.1 Csv读取

创建如下实体类,为方便管理可继承自CsvModel,CsvModel默认提供了Id字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[System.Serializable]
public class WeaponData: CsvModel
{
[CSVColumn("武器名称")]
public string weaponName;
[CSVColumn("武器等级")]
public int weaponLevel;
[CSVColumn("武器攻击力")]
public int weaponAttack;
[CSVColumn("武器防御力")]
public int weaponDefense;

public override string ToString()
{
return $"{weaponName}| 武器等级: {weaponLevel}, 武器攻击力: {weaponAttack}, 武器防御力: {weaponDefense}";
}
}
Id 武器名称 武器等级 武器攻击力 武器防御力
Id weaponName weaponLevel weaponAttack weaponDefense
int string int int int
1 小刀 1 5 2
2 双节棍 2 12 4
3 银月斧 3 25 3

注意:Csv文件需要以*,*分隔,以UTF-8为编码。

1
2
3
4
5
//读取CSV
var csvData =
await CSVParser.ParseByPathAsync<WeaponData>(Path.Combine(Application.streamingAssetsPath, "weapon.csv"));

csvData.ForEach(Debug.Log);

CSV导入读取效果

使用CSVParser即可将Csv直接解析为对象集,CSVParser提供多种方法可尝试。

6.2 CSV转SO

FF框架提供了将Csv转换为So的功能,可方便的将Csv数据转换到So。

Q:为什么要将Csv转换到So?

A:比如Csv中包含一些二进制资源(图片地址、预制体地址、音效地址等),在Csv中无法预览,容易配错。

特性对比 CSV文件 ScriptableObject (SO)
数据形态 外部文本,存储为TextAsset Unity资源文件 (.asset)
运行时性能 需实时解析,效率较低 直接引用,无需解析,性能高
内存占用 字符串形式加载,解析后产生额外GC(垃圾回收) 序列化对象,内存效率更优
类型安全 所有字段初始为字符串,需手动转换类型,易出错 强类型,字段在Inspector中清晰可见,不易出错

当然选择什么样的存储形式,完全取决于您,FF仅提供支持。

要将Csv转换为So,首先需要将文件从StreamingAssets文件夹移出,选中csv文件右键:

image-20260203094231320

6.3 FSM状态机

创建状态类

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
public class IdleState : FsmStateAbs<GameBlackboard>
{
public IdleState(string stateName, FsmMachine<GameBlackboard> fsmMachine) : base(stateName, fsmMachine)
{
}

public override UniTask Enter()
{
Debug.Log("进入IdleState");
return UniTask.CompletedTask;
}

public override UniTask Exit()
{
Debug.Log("退出IdleState");
return UniTask.CompletedTask;
}

public override UniTask Update()
{
Debug.Log("IdleState Update");
return UniTask.CompletedTask;
}
}

public class AttackState: FsmStateAbs<GameBlackboard>
{
public AttackState(string stateName, FsmMachine<GameBlackboard> fsmMachine) : base(stateName, fsmMachine)
{
}

public override UniTask Enter()
{

Debug.Log("进入AttackState");
return UniTask.CompletedTask;
}

public override UniTask Exit()
{

Debug.Log("退出AttackState");
return UniTask.CompletedTask;
}

public override UniTask Update()
{
Debug.Log("AttackState Update");
return UniTask.CompletedTask;
}
}

设置并启动状态机:

提供一个上下文Context 作为状态机的公用数据黑板,可以是任意类型,这里使用都是全局黑板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//创建状态机
var fsm = new FsmMachine<GameBlackboard>(Blackboard);
var idleState=new IdleState("Idle",fsm);
var attackState=new AttackState("Attack",fsm);
//注册状态

fsm.AddState(idleState).AddState(attackState);

//注册 转换 (任意状态只要条件满足都会转换到目标状态 attackState )
fsm.AddTransition(new FsmTransition<GameBlackboard>(() => Input.GetKeyDown(KeyCode.P), attackState));

//注册 转换(按下o且 当前状态是idle 才能转换到 attack)
fsm.AddTransition(new FsmTransition<GameBlackboard>(() => Input.GetKeyDown(KeyCode.O), idleState, attackState));


//注册刷新
Observable.EveryUpdate().Subscribe(_ =>
{
fsm.Update();
}).AddTo(this);


//改变状态
await fsm.ChangeState(new IdleState("Idle", fsm));

转换 是可选的,如果不使用转换可以使用ChangeState强制转换状态;如果注册了转换会自动转换

6.4 对象池

FF框架提供2种对象池:通用对象对象池和游戏对象池。

  • ObjectPool where T : new()
  • GameObjectPool where T : MonoBehaviour

6.5 单例

默认FF只提供SingletonConfigScriptObject<T> 单例类型,用于So 单例。

对于其他的单例,框架建议使用IOC容器进行管理实现全局调用。

6.6 So背包

So背包是对Unity ScripttableObject 的数据存储集资源。

可直接继承自 BaseAssetBag<T>使用。

提供方法:

  • Save
  • Load
  • Clean

So背包的设计用意是 方便存储和读取so数据,将相同的so资源数据用Bag存储调用。

例如:背包和背包道具、成就数据等

7.常用工具

7.1 全局黑板

FF默认提供了一个全局游戏黑板,方便进行全局的数据暂存取用。

1
2
3
4
//获得全局游戏黑板
var blackboard = this.GetModel<GameBlackboard>();
blackboard.SetData("level", 1);
var level = blackboard.GetData<int>("level");

同时也可以创建自己的数据黑板,继承自 BlackboardAbs即可。

7.2 常用颜色

访问 FastyColor 静态类进行使用

7.3 其他工具

  • Dict 自定义字典 在属性面板序列化
  • FastyTool 一些常用工具
  • RichTextTool 文字着色
  • SkipUnityLogo 跳过Logo
  • TaskQueue 任务队列
  • MyLog 日志扩展