MeteorCat / QFramework架构

Created Sun, 03 Nov 2024 13:06:06 +0800 Modified Wed, 29 Oct 2025 23:25:05 +0800
2859 Words

QFramework架构

QFramework 官方样例推荐的架构推荐以 MVC 架构方式构建项目目录, 这里以 Scripts 目录为主要代码基准目录:

以此 Scripts 目录下为主, 同级推荐生成以下目录:

  • ViewController: IController 接口,负责接收输入和状态变化时的表现,一般情况下,MonoBehaviour 均为表现层
  • System: ISystem 接口,帮助IController承担一部分逻辑,在多个表现层共享的逻辑,比如计时系统、商城系统、成就系统等
  • Model: IModel 接口,负责数据的定义数据的增删查改方法
  • Utility: IUtility 接口,负责提供基础设施,比如存储方法、序列化方法、网络连接方法、蓝牙方法、SDK、框架继承等或者第三方库集成
  • Command: ICommand 接口,核心业务区用于获取对象做游戏逻辑计算, 包括且不限于游戏初始化|关卡进入等
  • Event: IEvent 接口,统一的全局事件目录, 定义游戏所需的所有调配事件

各层级调用规则:

  • IController 更改 ISystem|IModel 的状态必须用 Command 来处理
  • ISystem|IModel 状态发生变更后通知 IController 必须用 Event|BindableProperty
  • IController 可以获取 ISystem|IModel 对象来进行数据查询
  • ICommand 不能有状态和内部属性相关
  • 上层可以直接获取下层, 下层不能获取上层对象, 下层如果想要向上层通信必须用 Event
  • 上层向下层通信用方法调用(只是做查询,状态变更用Command),IController的交互逻辑为特别情况,只能用Command

这里简单以玩家对战的战斗场景来说明:

  1. 玩家进入战斗场景, 这时候其实是处理表现层 IController
  2. 这时候玩家确认攻击指令, 其实就是调用 ICommand 指令功能
  3. ICommand 接受指令直接先查询玩家 IModel 信息确认玩家攻击力造成的伤害
  4. ICommand 查询到伤害推送给 ISystem 战斗系统确认是否已经打败
  5. ISystem 确认伤害打败怪物采用 IEvent 通知 ICommand 开始进行胜利结算
  6. ICommand 接收到结算的时候, IUtility 负责保存胜利奖励信息和状态信息

上面就是各层级所需的逻辑流程, 后续就是规划具体游戏业务功能.

模型设计

这里编写基础的游戏玩家模型信息, 项目刚开始的时候必须确定所需的业务数据:

using System;
using QFramework;

// 这里我放到 项目目录/Assets/QGame/Model, 主要是防止命名空间污染
namespace QGame.Model
{
    /// <summary>
    /// 玩家信息模型, 可以和游戏服务端保持一致
    /// </summary>
    [Serializable]
    public class PlayerInfoEntity
    {
        public int uid; // 玩家uid
        public string nickname; // 玩家名称
        public int gold; // 游戏资源
        public int diamond; // 充值资源
    }

    /// <summary>
    /// 玩家模型操作接口
    /// </summary>
    public interface IPlayerInfoModel : IModel
    {
        public BindableProperty<int> Uid { get; }

        public BindableProperty<string> Nickname { get; }

        public BindableProperty<int> Gold { get; }

        public BindableProperty<int> Diamond { get; }

        public void SetPlayerInfo(PlayerInfoEntity entity); // 设置玩家信息

        public PlayerInfoEntity GetPlayerInfo(); // 获取玩家信息
    }


    /// <summary>
    /// 具体的实现模型
    /// </summary>
    public class PlayerInfoModel : AbstractModel, IPlayerInfoModel
    {
        public BindableProperty<int> Uid { get; } = new BindableProperty<int>();

        public BindableProperty<string> Nickname { get; } = new BindableProperty<string>();

        public BindableProperty<int> Gold { get; } = new BindableProperty<int>();

        public BindableProperty<int> Diamond { get; } = new BindableProperty<int>();

        /// <summary>
        /// 设置玩家信息
        /// </summary>
        /// <param name="entity">PlayerInfoEntity</param>
        public void SetPlayerInfo(PlayerInfoEntity entity)
        {
            Uid.Value = entity.uid;
            Nickname.Value = entity.nickname;
            Gold.Value = entity.gold;
            Diamond.Value = entity.diamond;
        }

        /// <summary>
        /// 获取玩家信息结构
        /// </summary>
        /// <returns>PlayerInfoEntity</returns>
        public PlayerInfoEntity GetPlayerInfo()
        {
            var entity = new PlayerInfoEntity
            {
                uid = Uid.Value,
                nickname = Nickname.Value,
                gold = Gold.Value,
                diamond = Diamond.Value
            };
            return entity;
        }
        
        
        /// <summary>
        /// 模型初始化
        /// </summary>
        protected override void OnInit()
        {
            Debug.Log("PlayerInfoModel OnInit");
            // 有些内部游戏管理需要加载GM指令栏, 判断之后渲染高级服务指令

            // 加载玩家信息完成, End
        }
    }
}

同时需要创建查询模板功能, 用于数据修改变动:

using QFramework;

// 查询功能, 目录: 项目目录/Assets/QGame/Model
namespace QGame.Model
{
    
    /// <summary>
    /// 查询方法
    /// </summary>
    public class PlayerInfoQuery : AbstractQuery<PlayerInfoEntity>
    {
        /// <summary>
        /// 默认初始化执行
        /// </summary>
        /// <returns>PlayerInfoEntity</returns>
        protected override PlayerInfoEntity OnDo()
        {
            var model = this.GetModel<IPlayerInfoModel>();
            var entity = new PlayerInfoEntity();
            entity.uid = model.Uid.Value;
            entity.nickname = model.Nickname.Value;
            entity.gold = model.Gold.Value;
            entity.diamond = model.Diamond.Value;
            return entity;
        }
    }
}

OK, 目前已经获得所需要的登录玩家的结构数据( UserInfoModel ), 接下来就是 UI 表现层和逻辑执行层|玩家管理系统.

玩家系统

这里先简单定义玩家系统, 方便后续处理:

using QFramework;
using UnityEngine;

// 玩家系统, 目录: 项目目录/Assets/QGame/System
// 项目文件: QGame/System/PlayerMgrSystem.cs
namespace QGame.System
{
    /// <summary>
    /// 玩家系统接口
    /// </summary>
    public interface IPlayerMgrSystem : ISystem
    {
        // 后续补充玩家系统所需属性
    }


    /// <summary>
    /// 玩家系统最后句柄
    /// </summary>
    public class PlayerMgrSystem : AbstractSystem, IPlayerMgrSystem
    {
        
        
        /// <summary>
        /// 系统初始化
        /// </summary>
        protected override void OnInit()
        {
            Debug.Log("PlayerMgrSystem OnInit");
        }
        
        
        /// <summary>
        /// 暴露给外部的设置玩家模型信息
        /// </summary>
        public void SetSelfPlayerInfo(string nickname,int uid, int gold,int diamond)
        {
            var model = this.GetModel<PlayerInfoModel>();
            model.Nickname.Value = nickname;
            model.Uid.Value = uid;
            model.Gold.Value = gold;
            model.Diamond.Value = diamond;
        }
    }
}

PlayerInfoEntity 结构就是服务器和客户端交换数据的实体, 这里以 ProtoBuf 为例, 假设推送登录之后服务端同步数据结构体:

syntax = "proto3"; // 声明调用 v3 版本结构

// 响应结构体对象
message LoginResponse{
  uint32 state = 1; // 状态, 如果 0 代表成功, 其他参考错误表
  uint32 uid = 2; // 玩家Uid
  string nickname = 3; // 玩家昵称
  uint32 gold = 4; // 玩家金币
  uint32 diamond = 5; // 充值货币
  // 其他玩家属性字段 ....
}

按照不同编程语言就可以同步生成二进制让客户端和服务端做数据同步.

后续问题就是按照项目开始所说的, 必须注册 Model|System 从而方便挂载到全局对象.

功能注册

上面已经完成 Model|System 定义, 后续就是考虑注册到游戏当中了; 首先需要说明游戏其实也是视为 App(应用), 而作为 App 就需要定义应用启动入口( main ), 所以这里创建全局应用入口来处理( 个人喜欢 项目名 + App 来做命名, 所以入口文件取名 QGameApp.css ):

using QFramework;
using QGame.Model;
using QGame.System;
using UnityEngine;

// 项目文件: QGame/QGameApp.cs
namespace QGame
{
    /// <summary>
    /// 默认游戏启动入口, Architecture 声明这是 QFramework 管理器
    /// </summary>
    public class QGameApp: Architecture<QGameApp>
    {
        /// <summary>
        /// 初始化
        /// </summary>
        protected override void Init()
        {
            Debug.Log("QGameApp Init...");
            
            // 注册系统, 自己编写的系统对象
            RegisterSystem(new PlayerMgrSystem());
            
            
            // 注册模型, 自己编写的模型对象
            RegisterModel(new PlayerInfoModel());
            
            // 后续如果想调用直接采用以下方式在实现 IController 接口组件当中调用 
            // GetModel<PlayerMgrSystem>() | GetSystem<PlayerMgrSystem>()
        }
    }
}

这里就是完成注册 App 管理器的样例, 后续如果追加新的 IModel|ISystem|ICommand 也是到内部注册.

游戏逻辑

现在涉及到都是内部封装数据和系统管理, 没有涉及到关于游戏逻辑业务 MonoBehaviour 之类管理, 这是因为游戏逻辑层是单独抽离出来处理, 不允许直接接触到数据的行为( 需要 ICommand 通知事件调用 ).

这里直接实现基础视图控制器就可以:

using QFramework;
using UnityEngine;

// 项目文件: QGame/ViewController/MainController.cs
namespace QGame.ViewController
{
    /// <summary>
    /// 视图控制器
    /// </summary>
    public class MainController : MonoBehaviour, IController
    {
        /// <summary>
        /// 提供暴露应用句柄
        /// </summary>
        /// <returns>IArchitecture</returns>
        public IArchitecture GetArchitecture()
        {
            return QGameApp.Interface;
        }


        /// <summary>
        /// 初始化
        /// </summary>
        private void Start()
        {
            Debug.Log("Start Application");
        }


        /// <summary>
        /// 昵称
        /// </summary>
        private string mNickname = "";

        /// <summary>
        /// UID
        /// </summary>
        private string mUid = "";

        /// <summary>
        /// 是否已经登录
        /// </summary>
        private bool mIsLogin = false;

        /// <summary>
        /// 采用GUI布局来模拟游戏登录UI
        /// </summary>
        private void OnGUI()
        {
            GUILayout.BeginVertical();

            // 状态显示
            GUILayout.BeginHorizontal();
            GUILayout.Label("状态   : ", GUILayout.Width(50));
            GUILayout.Label(mIsLogin ? "已登录" : "未登录", GUILayout.Width(150));
            GUILayout.EndHorizontal();

            // 昵称布局
            GUILayout.BeginHorizontal();
            GUILayout.Label("昵称  :", GUILayout.Width(50));
            mNickname = GUILayout.TextField(mNickname, GUILayout.Width(150));
            GUILayout.EndHorizontal();


            // UID布局
            GUILayout.BeginHorizontal();
            GUILayout.Label("UID  :", GUILayout.Width(50));
            mUid = GUILayout.TextField(mUid, GUILayout.Width(150));
            GUILayout.EndHorizontal();


            // 登录触发
            if (!mIsLogin)
            {
                if (GUILayout.Button("登录", GUILayout.Width(200)))
                {
                    // 登录字段确认
                    mNickname = mNickname.Trim();
                    mIsLogin = int.TryParse(mUid, out var uidRes) && OnLogin(mNickname, uidRes);
                }
            }

            GUILayout.EndVertical();
        }


        /// <summary>
        /// 登录回调
        /// </summary>
        /// <param name="nickname">账户名</param>
        /// <param name="uid">UID</param>
        /// <returns></returns>
        private bool OnLogin(string nickname, int uid)
        {
            // todo: 具体登录授权转发
            Debug.Log($"登录请求: {nickname}, {uid}");
            return false;
        }
    }
}

这里已经假定设计出游戏登录页面, 主要逻辑集中在 OnLogin 方法内部, 这里就是围绕这个方法来做调度, 不过在此之前需要构建个 ICommand 来编写登录指令:

using QFramework;
using QGame.System;

// 项目文件: QGame/Command/LoginCommand.cs
namespace QGame.Command
{
    /// <summary>
    /// 登录指令
    /// </summary>
    public class LoginCommand : AbstractCommand
    {
        private string mNickname;

        private int mUid;

        public LoginCommand(string nickname, int uid)
        {
            mNickname = nickname;
            mUid = uid;
        }

        /// <summary>
        /// 执行方法
        /// </summary>
        protected override void OnExecute()
        {
            // 这里获取玩家系统写入信息
            this.GetSystem<PlayerMgrSystem>().SetSelfPlayerInfo(
                mNickname, mUid, 0, 0
            );

            // 有的涉及比较复杂的逻辑事务, 必须采用事件处理
            //this.SendEvent<>();
            
            // 如果只是读取数据而不做修改的操作, 建议直接读取数据即可
            // 也就是遵循规则: 读取不修改直接 Model 读取, 写入的情况需要 Event 推送
        }
    }
}

最后就是在 MainController 推送登录指令 LoginCommandPlayerMgrSystem 加载:

namespace QGame.ViewController
{
    /// <summary>
    /// 视图控制器
    /// </summary>
    public class MainController : MonoBehaviour, IController
    {
        // 其他代码, 略
    
        /// <summary>
        /// 登录回调
        /// </summary>
        /// <param name="nickname">账户名</param>
        /// <param name="uid">UID</param>
        /// <returns></returns>
        private bool OnLogin(string nickname, int uid)
        {
            Debug.Log($"登录请求: {nickname}, {uid}");
            
            // 推送登录指令
            this.SendCommand(new LoginCommand(nickname, uid));
            
            // 因为只需要登录读取信息, 只需要读取信息判断下即可
            return this.GetModel<PlayerInfoModel>().Uid.Value != 0;
        }
    }
}

之后挂载脚本到部件执行下就可以看到最终效果:

image