MeteorCat / QFramework初探

Created Sat, 02 Nov 2024 14:41:21 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
2523 Words

QFramework初探

Unity 当中其实可以从0开始编写内部游戏框架, 但是出于项目加快上线计划从而会采用开源框架, 而这里采用开源社区的 QFramework 来引入.

内部只有 QFramework.cs 单个文件, 直接创建文件夹之后复制黏贴到项目就能直接开始项目搭建.

这里先编写个官方测试样例, 代码之中会具体说明相关的功能作用:

using System.Collections.Generic;
using QFramework;
using UnityEngine;
using Random = UnityEngine.Random;

// 自定义项目的命名空间, 防止命名空间污染
namespace Examples
{
    /// <summary>
    /// 挂载的控制器名称, 需要实现 IController 接口
    /// </summary>
    public class QueryModelController : MonoBehaviour, IController
    {
        #region 数据模型

        /// <summary>
        /// 生成随机数模型
        /// </summary>
        public class RandomValueModel : AbstractModel
        {
            /// <summary>
            /// 模型初始化, 可以通过本地配置|网络获取到数据
            /// </summary>
            protected override void OnInit()
            {
            }

            /// <summary>
            /// 模拟服务器|数据库查询获得信息
            /// </summary>
            /// <returns></returns>
            public int Execute()
            {
                return Random.Range(0, 100);
            }
        }

        /// <summary>
        /// 随机模型查询命令
        /// </summary>
        public class RandomValueQuery : AbstractQuery<int>
        {
            /// <summary>
            /// 执行查询
            /// </summary>
            /// <returns>int</returns>
            protected override int OnDo()
            {
                return this.GetModel<RandomValueModel>().Execute();
            }
        }

        #endregion

        #region 状态机声明和暴露接口

        /// <summary>
        /// 声明内部管理状态机对象, 约等于生成 App 对象
        /// </summary>
        public class QueryModelApp : Architecture<QueryModelApp>
        {
            /// <summary>
            /// 声明状态机的初始化
            /// </summary>
            protected override void Init()
            {
                Debug.Log("内部管理器App初始化");

                // 注册数据模型应用让其内部管理
                RegisterModel(new RandomValueModel());
            }
        }


        /**
         * 暴露给QFramework的接口对象
         */
        public IArchitecture GetArchitecture()
        {
            return QueryModelApp.Interface;
        }

        #endregion

        #region UI业务功能

        /// <summary>
        /// 随后显示的数据结果
        /// </summary>
        private List<int> mValues = new List<int>();

        /// <summary>
        /// 这里采用界面GUI做游戏界面模拟
        /// </summary>
        private void OnGUI()
        {
            GUILayout.BeginVertical();

            if (GUILayout.Button("生成随机值"))
            {
                mValues.Add(this.SendQuery(new RandomValueQuery()));
            }

            GUILayout.Label(string.Join(",", mValues));
            GUILayout.EndVertical();
        }

        #endregion
    }
}

这里面的关键点:

  • IController: 声明控制器接口, 主要和 MonoBehaviour 同级声明
  • Architecture: 声明Actor对象, 其实就是构建内部状态管理器
  • Architecture:GetArchitecture: 内部状态机必须实现的方法, 用于暴露本地 Actor 接口给外部调用
  • XXXArchitecture:Interface: 暴露给外部的接口对象, 由方便框架进行维护
  • AbstractModel: 声明模型继承类, 用于保存状态机内部数据维护
  • AbstractQuery: 声明查询继承类, 用于对状态机内部数据做CRUD操作

可以看到如果过渡到 QFramework 游戏框架是相对简单, 流程如下:

  1. MonoBehaviour 追加 IController 接口实现
  2. 内部声明 AppActor, 并继承 Architecture<?>
  3. 继承 Architecture<?> 之后实现 GetArchitecture() 方法暴露 Actor 接口
  4. 如果内部需要数据维护则需要通过 AbstractModel|AbstractQuery 做数据维护和修改处理

这里的好处就是将数据和业务抽离提取, 有效接触代码耦合的问题, 具体可以查看官方 GitHub 库关于架构分层的说明.

事件驱动

QFramework 内置精简的事件系统用于注册和绑定业务:

using UnityEngine;

namespace QFramework.Example
{
    public class TypeEventSystemBasicExample : MonoBehaviour
    {
        /// <summary>
        /// 抽象出来的事件接口
        /// </summary>
        public interface IEventA
        {
            int Age { get; set; }
        }
    
        /// <summary>
        /// 自己编写事件结构, 继承抽象出来的接口A
        /// </summary>
        public struct TestEventA:IEventA
        {
            public int Age { get; set; }
        }
        
        public struct TestEventB: IEventA
        {
            public int Age { get; set; }
        }
        

        /// <summary>
        /// 初始化方法
        /// </summary>
        private void Start()
        {
            // 全局注册 TestEventA 结构的事件, 注意要对应注册对象子类做准确声明
            TypeEventSystem.Global.Register<TestEventA>(e =>
                {
                    // 当被唤醒时候调用,用于唤起业务事件
                    Debug.Log(e.Age);
                    Debug.Log(e.GetType().Name); // 继承的对象名
                })
                .UnRegisterWhenGameObjectDestroyed(gameObject); // 关联所属 GameObject, 如果接触绑定之后需要手动对该对象析构处理
            
            // 注册第二结构, 并且将回调修改成成员方法
            TypeEventSystem.Global.Register<TestEventB>(OnTestEventB)
                .UnRegisterWhenGameObjectDestroyed(gameObject); 
            
        }

        
        /// <summary>
        /// 单独抽离成方法
        /// </summary>
        void OnTestEventB(TestEventB e)
        {
            // 当被唤醒时候调用,用于唤起业务事件
            Debug.Log(e.Age);
            Debug.Log(e.GetType().Name); // 继承的对象名
        }

        /// <summary>
        /// 逻辑帧更新
        /// </summary>
        private void Update()
        {
            // 鼠标左键点击
            if (Input.GetMouseButtonDown(0))
            {
                // 推送全局事件, 通知唤起注册事件
                TypeEventSystem.Global.Send(new TestEventA()
                {
                    Age = 18
                });
            }

            // 鼠标右键点击
            if (Input.GetMouseButtonDown(1))
            {
                //TypeEventSystem.Global.Send<TestEventA>();// 精确声明唤醒
                TypeEventSystem.Global.Send(new TestEventB()
                {
                    Age = 80
                });
            }
            
            // 鼠标中键直接解除绑定, 后续不会再被唤醒
            if (Input.GetMouseButtonDown(2))
            {
                Debug.Log("TestEventB Unbind");
                TypeEventSystem.Global.UnRegister<TestEventB>(OnTestEventB);
            }
            
        }
        
        
        
        /// <summary>
        /// 部件析构的时候调用
        /// </summary>
        private void OnDestroy()
        {
            //TypeEventSystem.Global.UnRegister<TestEventB>(OnTestEventB);
        }
    }
}

事件是游戏系统必须依赖的功能, 把游戏UI逻辑编写封装成事件由外部数据驱动执行, 有效UI业务和数据功能分离(网络游戏频繁用到)

所以游戏内部业务一般等定义好 UI 所需展示事件( 如升级|奖励通知等 ), 之后注册完毕就是等全局通知调用唤醒这些内容.

上面编写在 MonoBehaviour 过于复杂, 所以后续内部方案当中带有接口快捷实现处理:

using UnityEngine;

namespace QFramework.Example
{
    /// <summary>
    /// 外部定义的接口A
    /// </summary>
    public struct InterfaceEventA
    {
            
    }

    /// <summary>
    /// 外部定义的接口B
    /// </summary>
    public struct InterfaceEventB
    {
        
    }

    
    /// <summary>
    /// MonoBehaviour 继承, 同时实现 IOnEvent<?> 内部追加 OnEvent 回调实现
    /// </summary>
    public class InterfaceEventModeExample : MonoBehaviour
        , IOnEvent<InterfaceEventA>
        , IOnEvent<InterfaceEventB>
    {
        
        /// <summary>
        /// 鉴定实现 InterfaceEventA 接口的事件回调
        /// </summary>
        /// <param name="e"></param>
        public void OnEvent(InterfaceEventA e)
        {
            Debug.Log(e.GetType().Name);
        }
        
        /// <summary>
        /// 同上, 只是作为 InterfaceEventB 的事件回调
        /// </summary>
        /// <param name="e"></param>
        public void OnEvent(InterfaceEventB e)
        {
            Debug.Log(e.GetType().Name);
        }

        /// <summary>
        /// 初始化方法
        /// </summary>
        private void Start()
        {
            this.RegisterEvent<InterfaceEventA>()
                .UnRegisterWhenGameObjectDestroyed(gameObject);

            this.RegisterEvent<InterfaceEventB>();
        }

        /// <summary>
        /// 析构处理回调
        /// </summary>
        private void OnDestroy()
        {
            this.UnRegisterEvent<InterfaceEventB>();
        }

        /// <summary>
        /// 逻辑帧业务处理
        /// </summary>
        private void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                TypeEventSystem.Global.Send<InterfaceEventA>();
                TypeEventSystem.Global.Send<InterfaceEventB>();
            }
        }
    }
}

这里的 InterfaceEventA|InterfaceEventB 可以另外提取到指定的全局配置文件方便统一管理事件; 另外还有网络请求时间的回调方式, 一般网络事件大概率会把消息封包成 cmd + bytes 这种方式:

[pack len(消息长度, int32)] [cmd number(消息Id, int32)] [bytes....(消息字节内容)]

所以事件调用方式可以简单处理抽象做出类似事件唤起的操作:

using UnityEngine;

namespace QFramework.Example
{
    /// <summary>
    /// 假设网络消息唤醒调用事件
    /// </summary>
    public class EasyEventExample : MonoBehaviour
    {
        /// <summary>
        /// 简单的无参数句柄事件成员对象
        /// 该句柄仅仅作为局部事件句柄而非全局事件, 不过可以暴露给外层对象方便监听
        /// </summary>
        private EasyEvent<int> mServerMessage = new EasyEvent<int>();

        /// <summary>
        /// 多参数事件结构体, 附带有 (int,byte[]) 参数 
        /// </summary>
        public class ServerMessageWithBytes : EasyEvent<int, byte[]>
        {
        }

        /// <summary>
        /// 声明数据结构
        /// </summary>
        private ServerMessageWithBytes mServerMessageWithBytes = new ServerMessageWithBytes();

        /// <summary>
        /// 初始化方法
        /// </summary>
        private void Start()
        {
            // 绑定单纯获取无字节数据的网络消息
            mServerMessage.Register(value =>
                {
                    Debug.Log($"值变更:{value}");
                })
                .UnRegisterWhenGameObjectDestroyed(gameObject);

            // 绑定带有字节数据的网络消息
            mServerMessageWithBytes.Register((a, b) =>
                {
                    Debug.Log($"自定义事件:{a} {b}");
                })
                .UnRegisterWhenGameObjectDestroyed(gameObject);
        }

        
        /// <summary>
        /// 逻辑帧更新
        /// </summary>
        private void Update()
        {
            // 模拟触发网络消息到达, 假设服务器推送 1001 协议
            if (Input.GetMouseButtonDown(0))
            {
                mServerMessage.Trigger(1001);
            }

            // 模拟触发网络消息到达, 假设服务器推送 1002 协议
            if (Input.GetMouseButtonDown(1))
            {
                byte[] data = System.Text.Encoding.UTF8.GetBytes("hello world");
                mServerMessageWithBytes.Trigger(1002, data);
            }
        }
    }
}

上面就是模拟网络消息协议传递过来的情况, 具体如果做网络游戏设计的时候可以再深入细化处理, 另外这种事件还支持 属性绑定( BindableProperty ) 来对属性值做事件调用修改:

using UnityEngine;

namespace QFramework.Example
{
    
    /// <summary>
    /// 值绑定事件监听
    /// </summary>
    public class BindablePropertyExample : MonoBehaviour
    {
        /// <summary>
        /// 事件监听 int 属性
        /// </summary>
        private BindableProperty<int> mSomeValue = new BindableProperty<int>(0);

        /// <summary>
        /// 事件监听 string 属性
        /// </summary>
        private BindableProperty<string> mName = new BindableProperty<string>("QFramework");
        
        
        /// <summary>
        /// 初始化
        /// </summary>
        void Start()
        {
            // 绑定执行监听
            mSomeValue.Register(newValue =>
            {
                Debug.Log(newValue);
            }).UnRegisterWhenGameObjectDestroyed(gameObject);

            mName.RegisterWithInitValue(newName =>
            {
                Debug.Log(mName);
            }).UnRegisterWhenGameObjectDestroyed(gameObject);
        }
        
        void Update()
        {
            if (Input.GetMouseButtonDown(0))
            {
                mSomeValue.Value++;
            }

            if (Input.GetMouseButtonUp(1))
            {
                mName.Value = "Value = " + mSomeValue.Value;

            }
        }
    }
}

建议如果组件需要用到外部访问内部参数属性都采用这种事件唤醒方式处理, QFramework 基本架构和使用已经完成, 后续就是游戏方面的业务结合.

依赖注入(IOC)

依靠内部事件机制扩展出依赖注入容器( IOCContainner ), 用于对全局服务进行注册管理:

using UnityEngine;

namespace QFramework.Example
{
    /// <summary>
    /// IOC管理器
    /// </summary>
    public class IOCContainerExample : MonoBehaviour
    {
        /// <summary>
        /// 假设声名某个服务, 内部带有 Say 服务
        /// </summary>
        public class SomeService
        {
            public void Say()
            {
                Debug.Log("SomeService Say Hi");
            }
        }
        
        /// <summary>
        /// 假设声明网络连接服务, 内部带有 Connect 服务
        /// </summary>
        public interface INetworkService
        {
            void Connect();
        }
        
        
        /// <summary>
        /// 继承网络服务: Tcp
        /// </summary>
        public class TcpNetworkService : INetworkService
        {
            public void Connect()
            {
                Debug.Log("TcpNetworkService Connect Succeed");
            }
        }
        
        /// <summary>
        /// 继承网络服务: Udp
        /// </summary>
        public class UdpNetworkService : INetworkService
        {
            public void Connect()
            {
                Debug.Log("UdpNetworkService Connect Succeed");
            }
        }
        
        

        private void Start()
        {
            var container = new IOCContainer();
            
            container.Register(new SomeService());
            
            container.Register(new TcpNetworkService());
            container.Register(new UdpNetworkService());
            
            container.Get<SomeService>().Say();
            container.Get<TcpNetworkService>().Connect();
            container.Get<UdpNetworkService>().Connect();
        }
    }
}

如果想构建全局服务可以这样来注册的获取.