QFramework
QFramework簡介
QFramework是一套漸進式、快速開發框架,適用于任何類型的游戲及應用項目,它包含一套開發架構和大量的工具集
QFramework的特性
-
簡潔性:QFramework 強調代碼的簡潔性和易用性,讓開發者能夠快速上手,減少學習成本。
-
模塊化設計:該框架采用了模塊化的架構設計,方便開發者根據項目需求自由組合和擴展功能模塊。
-
事件驅動:支持事件驅動編程模型,便于實現不同游戲對象之間的通信和交互。
-
數據綁定:提供了數據綁定的支持,可以輕松實現UI與邏輯代碼的數據同步。
-
資源管理:內置了資源加載和釋放機制,幫助開發者更高效地管理游戲中的各種資源。
-
MVC/MVVM架構支持:支持傳統的MVC(Model-View-Controller)架構模式,有助于更好地組織和分離代碼。
-
熱更新支持:對于需要進行熱更新的游戲,QFramework 提供了相應的支持,使得代碼或資源的在線更新變得更加容易。
-
豐富的工具集:包含了一系列實用工具,如調試工具、配置管理等,進一步提升了開發效率。
-
社區支持:擁有活躍的社區支持,開發者可以在遇到問題時尋求幫助或者分享自己的經驗。
QFramework架構
QFramework架構是一套簡單、強大、易上手的系統設計架構
這套架構基于MVC架構模式,可 分層,CQRS支持,事件驅動,IOS模塊化,領域驅動設計(DDD)支持,符合SOLID原則,并且源碼不到1000行
架構圖
QFramework的MVC
QFramework基于MVC的開發模式
我們可以通過一個案例來學習MVC模式:計數器應用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{//Contarollerpublic class CounterAppController : MonoBehaviour{//Viewpublic Button BtnAdd;public Button BtnSub;public Text CounterText;//Modelpublic int Count = 0;private void Start(){BtnAdd.onClick.AddListener(() =>{//交互邏輯Count++;//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯Count--;//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = Count.ToString();}}
}
但是這還沒有導入QFramework
代碼很簡單 這是一個非常簡易的MVC的實現,但是我們要用發展的眼光看待問題,如果在未來需要做一個需要大量邏輯代碼,那么count可能會在多個Controller中使用,甚至需要針對count這個數據寫一些其他邏輯,比如增加多個分數,或者需要存儲count等,那目前cout只屬于CounterAppController,顯然在未來是不夠用的。那么就需要count成員變量變成一個共享的數據,最快的做法是把count變量變成靜態變量或者單例,這樣寫起來雖然很快,但在后期維護的時候會產生一些問題
然而,QFramework架構提供了Model的概念
首先導入QFramework框架https://gitee.com/liangxiegame/QFramework/blob/master/QFramework.cs
導入QFramework的方式:復制QFramework.cs的代碼到Unity工程中即可
導入后,我們將CounterAppController的代碼改成:
//CounterModel
namespace QFramework.Example
{public class CounterModel : AbstractModel{public int Count = 0;protected override void OnInit(){Count = 0;}}
}
//CounterApp
namespace QFramework.Example
{//Architecture:用于管理模塊,或者說是Architecture提供了一整套架構的解決方案,而模塊管理和提供Mvc只是功能的一部分public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterModel(new CounterModel());}}
}
//CounterAppController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯mModel.Count++;//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯mModel.Count--;//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}
注意:需要共享的數據放在Model里,不需要共享的能不放就不放?Model的引入是為了解決數據共享的問題,而不是說但只是為了讓數據和表現分離
數據共享分為兩種:空間上的共享和時間上的共享
空間上的共享非常簡單,就是多個點的代碼需要訪問Model里的數據
時間上的共享就是存儲功能,將上一次關閉App之前的數據存儲到一個文件里,這次打開時獲得上次關閉App之前的數據
雖然以上代碼引入了Model,但是這套代碼隨著項目規模的發展還是有很多的問題,其中的Controller會越來越臃腫
什么是交互邏輯和表現邏輯
交互邏輯:就是從用戶輸入開始到數據變更的邏輯 順序是View->Controller->Model
表現邏輯:就是數據變更到在界面顯示的邏輯 順序是Model->Controller->View
View、Controller和Model的交互邏輯和表現邏輯形成了一個閉環,構成了完整的MVC閉環
引入Command
Controller本身之所以臃腫,是因為,她負責了兩種職責,即改變Model數據的交互邏輯,以及Model數據變更之后更新到界面的表現邏輯
而在一個有一定規模的項目中,表現邏輯和交互邏輯非常多,而一個controller很容易就做到上千行代碼,而大部分的MVC方案,解決Controller臃腫用的是引入Command的方式,即引入命令模式,通過命令來分擔Controller的交互邏輯的職責
將Command引入代碼中:
創建IncreaseCountCommand.cs文件:
namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count++;}}
}
創建DecreaseCountCommand.cs文件:
namespace QFramework.Example
{public class DecreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count--;}}
}
修改CounterAppController.cs文件:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();//表現邏輯updateview();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();//表現邏輯updateview();});//表現邏輯updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}
通過引入Command,幫助分擔了Controller的交互邏輯。使得Controller成為一個薄薄一層,在需要修改Model的時候?,Controller只要調用一句簡單的Command即可
Command的作用
- Command可以復用,Command也可以調用Command
- Commad可以比較方便實現撤銷功能
- 如果遵循一定規范,可以實現使用Command跑自動化測試
- Command可以指定Command可以制定Command隊列,也可以讓Command按照特定的方式執行
- 一個Command也可以封裝成一個Http或者Tcp里的一次數據請求
- Command可以實現Command中間件模式等等
Command的優點
Command最明顯的好處就是就算代碼再亂,也只是一個Command對象里亂,而不會影響其他對象,將方法封裝成命令對象,可以實現對命令對象的組織、排序、延時等操作
引入Event
以上引入Command后,幫助Controller分擔了一部分的交互邏輯,但是表現邏輯的代碼目前看起來不是很智能。不如說在每次調用邏輯之后,表現邏輯都需要手動調用一次UpdateView方法
在一個項目中表現邏輯的調用次數很多。因為只要修改了數據,對應的就要把數據的變化在界面上表現出來。所以可以引入一個事件機制來解決這個問題
這個事件機制的使用其實是和Command一起使用的,通過Command修改數據,當數據發生修改后發送對應的數據變更事件,這個是簡化版本的CQRS原則,即讀寫分離原則。引入這項原則會很容易實現事件驅動、數據驅動架構
首先定義數據變更事件CountChangedEvent.cs
namespace QFramework.Example
{public struct CountChangedEvent{}
}
然后在IncreaseCountCommand、DecreaseCountCommand引入Event事件
//DecreaseCountCommand
namespace QFramework.Example
{public class DecreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count--;this.SendEvent<CountChangedEvent>();}}
}//IncreaseCountCommand
namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){this.GetModel<CounterModel>().Count++;this.SendEvent<CountChangedEvent>();}}
}
最后在CounterAppController中編寫表現邏輯部分代碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();});//表現邏輯this.RegisterEvent<CountChangedEvent>(e =>{updateview();}).UnRegisterWhenGameObjectDestroyed(gameObject);updateview();}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}
通過事件方式,將表現邏輯更新進行解耦,就是說我們并不要主動調用表現邏輯,而是定義好表現邏輯后,然后在數據變更的同時發送對應的事件,表現邏輯只需要訂閱這個事件并定義好對應執行的邏輯即可。這樣不論任何角色發生了數據變更,同時需要負責發送事件
引入Utility
在學Utility之前,先來用之前學習的內容來支持CounterApp的數據存儲功能
使用PlayerPrefs
using UnityEngine;namespace QFramework.Example
{public class CounterModel : AbstractModel{public int mCount = 0;public int Count{get{ return mCount; }set{if (mCount!=value){mCount = value;PlayerPrefs.SetInt(nameof(Count),value);}}}protected override void OnInit(){Count = PlayerPrefs.GetInt(nameof(Count),0);}}
}
當然我們現在存儲少量的數據是非常可行的,但如果需要存儲大量數據的時候,Model層就會有大量的存儲、加載相關的代碼,還有如果以后不想使用PlayerPrefs時,需要修改的時候,就會造成大量修改工作量
在QFramework中提供了一個Utility層,專門用來解決上述問題,使用方法非常簡單
首先創建Storage類,定義Utility層
using UnityEngine;namespace QFramework.Example
{public class Storage:IUtility{public void SaveInt(string key,int value){PlayerPrefs.SetInt(key, value);}public int LoadInt(string key, int value) { return PlayerPrefs.GetInt(key, value);}}
}
在CounterApp中注冊Model
namespace QFramework.Example
{public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterUtility(new Storage());this.RegisterModel(new CounterModel());}}
}
在CounterModel中編寫要存儲數據的代碼
using UnityEngine;namespace QFramework.Example
{public class CounterModel : AbstractModel{public int mCount = 0;private Storage storage;public int Count{get{ return mCount; }set{if (mCount!=value){mCount = value;storage.SaveInt(nameof(Count), value);}}}protected override void OnInit(){storage = this.GetUtility<Storage>();Count = storage.LoadInt(nameof(Count),0);}}
}
這樣的話,如果我們想要修改PlayerPrefs為其他存儲函數時只需要對Storage.cs進行相應的修改即可
引入System
我們設置一個功能,及策劃提出了一個成就達成的功能,當Count點擊到10的時候,觸發一個點擊達人成就,點擊到20的時候,觸發一個點擊專家的成就
讓我們編寫相關的代碼
using UnityEngine;
namespace QFramework.Example
{public class IncreaseCountCommand : AbstractCommand{protected override void OnExecute(){var couterModel = this.GetModel<CounterModel>();couterModel.Count++;this.SendEvent<CountChangedEvent>();if (couterModel.Count == 10){Debug.Log(couterModel.Count + "點擊達人成就完成");}else if (couterModel.Count==20){Debug.Log(couterModel.Count + "點擊專家成就完成");}}}
}
ok,這個功能完成了,但策劃又說,希望再增加一個點擊到-10時,觸發一個點擊菜鳥成就,并且點擊達人成就和點擊專家成就太容易達成了,需要改成1000和2000次時,就需要我們去修改兩處的代碼,結果在造成了多處修改,這說明代碼有問題
那么針對以上的問題QFramework提供了一個System對象
首先創建AchievementSystem.cs類
using UnityEngine;
namespace QFramework.Example
{public class AchievementSystem:AbstractSystem{protected override void OnInit(){var model = this.GetModel<CounterModel>();this.RegisterEvent<CountChangedEvent>(e =>{if (model.Count == 10){Debug.Log("點擊達人成就達成");}else if (model.Count == 20){Debug.Log("點擊專家成就達成");}else if (model.Count == -10){Debug.Log("點擊菜鳥成就達成");}});}}
}
然后在CounterApp里注冊AchievementSystem
namespace QFramework.Example
{public class CounterApp : Architecture<CounterApp>{protected override void Init() {this.RegisterSystem(new AchievementSystem());this.RegisterUtility(new Storage());this.RegisterModel(new CounterModel());}}
}
QFramework的四個層級
- 表現層:IController
- 系統層:ISystem
- 數據層:IModel
- 工具層:IUtility
除了四個層級,還接觸了為Controller的交互邏輯減負的Command和為表現邏輯減負的Event
BindableProperty 優化事件
BindableProperty是包含數據+數據變更事件的一個對象
BindableProperty的基本使用
namespace QFramework.Example
{public class CounterModel : AbstractModel{public BindableProperty<int> Count = new BindableProperty<int>(0);protected override void OnInit(){var storage = this.GetUtility<Storage>();Count.Value = storage.LoadInt(nameof(Count),0);Count.Register(count =>{storage.SaveInt(nameof(Count), count);});}}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;namespace QFramework.Example
{public class CounterAppController : MonoBehaviour,IController{public Button BtnAdd;public Button BtnSub;public Text CounterText;private CounterModel mModel;private void Start(){mModel = this.GetModel<CounterModel>();BtnAdd.onClick.AddListener(() =>{//交互邏輯this.SendCommand<IncreaseCountCommand>();});BtnSub.onClick.AddListener(() => {//交互邏輯this.SendCommand<DecreaseCountCommand>();});//表現邏輯mModel.Count.RegisterWithInitValue(count =>{updateview();}).UnRegisterWhenGameObjectDestroyed(gameObject);}void updateview(){CounterText.text = mModel.Count.ToString();}public IArchitecture GetArchitecture(){return CounterApp.Interface;}}
}using UnityEngine;
namespace QFramework.Example
{public class AchievementSystem:AbstractSystem{protected override void OnInit(){var model = this.GetModel<CounterModel>();model.Count.Register(count =>{if (count == 10){Debug.Log("點擊達人成就達成");}else if (count == 20){Debug.Log("點擊專家成就達成");}else if (count == -10){Debug.Log("點擊菜鳥成就達成");}});}}
}
BindableProperty 除了提供 Register 這個 API 之外,還提供了 RegisterWithInitValue API,意思是 注冊時 先把當前值返回過來
BindableProperty是一個獨立的工具,可以脫離QFramework框架使用,也就是說不用非要用QFramework的MVC才能用BindableProperty,而是可以在自己的項目中隨意使用
一般情況下,像主角的金幣、分數等數據非常適合用 BindableProperty 的方式實現。