FinalFrameWork
是一款自用的、简单、强大、现代化的unity框架
关联文章:
🚧施工中:
1.快速安装
1.1 通过 Unity PackageManager(推荐)
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包设置。
3.2 启动器
创建一个启动器用于管理游戏启动流程:
首先初始化TextMeshPro
创建一个场景Start 作为启动场景,这个场景中只为了启动游戏流程,不放任何资源。
创建一个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(); }
将这个脚本挂载在Core游戏对象上(在空场景中创建一个空游戏对象),点击启动!
看到如上日志输出时说明,已完成框架启动!
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有限状态机工具,使用此工具我们可以将游戏按流程进行划分:
例如下图:
在不同的流程中,我们可以对资源、游戏系统等进行控制,具体使用很自由。
在项目中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 { 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 可以打开管理器,如下图:
点击创建项目资源结构文件夹
点击选择路径选择AARes
点击刷新addressable 可以自动扫描文件将其加入aa分组管理。
版本号:用于区分引用版本,默认显示在右下角。
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();
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/下
创建后在目录下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更新" ); } }
1 2 3 4 5 6 7 8 9 var panel= await this .GetSystem<UISystem>().PushPanel<AAPanel>();panel.UpdateUI(new { PlayerName = "AA" }); panel.Pop(); this .GetSystem<UISystem>().PopPanel<AAPanel>();
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 var popWindow = await this .GetSystem<UISystem>().PopWindowManager.PushPopWindow<AAPopWindow>();popWindow.OnExit(); 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 var csvData = await CSVParser.ParseByPathAsync<WeaponData>(Path.Combine(Application.streamingAssetsPath, "weapon.csv" )); csvData.ForEach(Debug.Log);
使用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文件右键:
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); fsm.AddTransition(new FsmTransition<GameBlackboard>(() => Input.GetKeyDown(KeyCode.P), attackState)); 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>使用。
提供方法:
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 日志扩展