当前位置: 首页 > news >正文

使用MVC模式开发cocos游戏功能

1.MVC 模式说明

MVC(Model - View - Controller)是一种软件设计模式,它将应用程序分为三个主要部分:模型(Model)、视图(View)和控制器(Controller),其核心思想是将数据处理、用户界面展示和业务逻辑分离,从而提高代码的可维护性、可扩展性和可测试性。在 Cocos Creator 游戏开发中,MVC 模式同样适用,以下是各部分的具体解释:

  • 模型(Model):负责存储和管理应用程序的数据和业务逻辑,它不依赖于视图和控制器,是独立的数据处理单元。例如,在游戏中,模型可以是玩家的角色信息、游戏关卡数据等。
  • 视图(View):负责将模型中的数据以可视化的方式呈现给用户,它只关注界面的显示和用户交互,不涉及业务逻辑。在 Cocos Creator 中,视图通常是由场景、节点和组件组成的游戏界面。
  • 控制器(Controller):作为模型和视图之间的桥梁,负责接收用户的输入,根据输入更新模型,并根据模型的变化更新视图。它处理业务逻辑,协调模型和视图之间的交互。

2.简单的demo

我们以游戏最简单的登录流程来实现这个模式,效果图如下:

2.1.视图组件 

cocos创建一个登录窗口预制体。cocos客户目录下assets\resources\prefabs新建一个预制体,比较简单,如下:

新建一个ts文件,命名为:LoginPaneView。代码如下:

import { _decorator, Button, EditBox } from 'cc';
import { BaseUiView } from '../../ui/BaseUiView';

const { ccclass, property } = _decorator;

@ccclass('LoginPaneView')
export class LoginPaneView extends BaseUiView {
  @property(EditBox)
  usernameInput: EditBox = null!;

  @property(EditBox)
  passwordInput: EditBox = null!;

  @property(Button)
  loginButton: Button = null!;

  @property(Button)
  logoutButton: Button = null!;

  start() {
    // 注册登录按钮点击事件
    this.passwordInput.inputFlag = EditBox.InputFlag.PASSWORD;
    this.registerClickEvent(
      this.loginButton.node,
      () => {
        this.node.emit('accountLogin');
      },
      this
    );
  }

  public getUserId(): string {
    return this.usernameInput.string;
  }

  public getUserPwd(): string {
    return this.passwordInput.string;
  }
}

界面绑定

在cocos刚才的预制体文件里,选择ui子节点,在属性检查器里新建一个脚本组件,完成控件绑定

2.2.模型组件

登录的模型比较简单,模型一般采用单例模式,这里,把网络请求的代码也放到模型了。 

import GameContext from '../../GameContext';
import ReqLogin from '../../net/protocol/ReqLogin';
import RespLogin from '../../net/protocol/RespLogin';

export class LoginModel {
  private static _instance: LoginModel;

  private userId: string = '';

  private userPwd: string = '';

  public static get instance(): LoginModel {
    if (!LoginModel._instance) {
      LoginModel._instance = new LoginModel();
    }
    return LoginModel._instance;
  }

  public setUserId(userId: string) {
    this.userId = userId;
  }

  public setUserPwd(userPwd: string) {
    this.userPwd = userPwd;
  }

  public getUserId(): string {
    return this.userId;
  }

  public getUserPwd(): string {
    return this.userPwd;
  }

  public login(): Promise<RespLogin> {
    return new Promise<RespLogin>((resolve, reject) => {
      GameContext.instance.WebSocketClient.sendMessage(
        ReqLogin.cmd,
        {
          id: this.userId,
          pwd: this.userPwd,
        },
        (msg: RespLogin) => {
          resolve(msg);
        }
      );
    });
  }
}

2.3.控制器组件

控制器承担主要的业务逻辑,需要同时依赖组件及模型,典型地,控制器也申明为单例模式,如下:

import { _decorator } from 'cc';
import { BaseController } from '../../ui/BaseController';
import { MainPaneController } from '../main/MainPaneController';
import { LoginModel } from './LoginModel';
import { LoginPaneView } from './LoginPaneView';
const { ccclass, property } = _decorator;

@ccclass('LoginPaneController')
export class LoginPaneController extends BaseController {
  @property(LoginPaneView)
  loginView: LoginPaneView | null = null;

  loginModel: LoginModel = LoginModel.instance;

  start() {
    this.initView(this.loginView);
  }

  protected bindViewEvents() {
    this.loginView.node.on('accountLogin', this.onLoginClick, this);
  }

  async onLoginClick() {
    const username = this.loginView.getUserId();
    const password = this.loginView.getUserPwd();

    // 这里添加登录验证逻辑
    if (username && password) {
      this.loginModel.setUserId(username);
      this.loginModel.setUserPwd(password);

      try {
        const response = await this.loginModel.login();
        console.log('登录成功');
        MainPaneController.openUi();
      } catch (error) {
        console.error('登录失败:', error);
      }
    } else {
      console.log('请输入用户名和密码');
    }
  }
}

回到 cocos界面,点击刚刚创建的预制体,选择LoginPane根节点,在右边属性检查器界面,新建一个脚本组件,选择上面的控制器,并把预制体的UI节点拖到LoginView控件上。

3.三者依赖关系

  • 视图依赖模型:视图需要从模型获取数据来进行展示。
  • 控制器依赖模型和视图:控制器既需要操作模型来实现业务逻辑,又需要与视图进行交互以响应用户操作和更新视图。例如,LoginPaneController类代码上同时依赖LoginPaneView和LoginModel
  • 模型不依赖视图和控制器:模型是独立的,它只负责存储和管理数据以及执行相关的业务逻辑,不关心数据是如何展示给用户的,也不关心用户如何与界面进行交互。

关于第一点,视图依赖模型是有一定的争议性的。有些观点认为,如果视图不依赖模型,那么它可以更加独立地进行设计和开发。一个视图组件可以在不同的场景或项目中被复用,而不需要受到特定模型的限制。例如,一个通用的按钮视图组件,它只负责处理自身的外观和交互逻辑,不关心具体的业务数据,这样可以提高代码的复用性和可维护性。当需要在不同的地方使用按钮时,只需要将按钮视图引入即可,而不必担心它与不同的模型之间的兼容性问题。然而,考虑到大部分游戏界面需要当模型属性发生时,界面实时刷新,例如升级后,同时刷新剩余的金币数量。如果把这部分逻辑放在控制器,代码比较啰嗦。因此,从开发效率来讲,笔者更倾向于视图依赖模型。 读者请自行斟酌。

4.MVC通用基类

考虑到MVC的开发模式会贯穿到大部分游戏业务, 这里对三层进行了抽象。

4.1.视图基类

import { _decorator, Button, Component, Node, Toggle } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BaseUiView')
export class BaseUiView extends Component {
  public display() {
    this.node.active = true;
    this.onDisplay();
  }

  protected onDisplay() {}

  public hide() {
    this.node.active = false;
    this.onHide();
  }

  protected onHide() {}

  /**
   * 为节点注册点击事件
   * @param node 需要注册点击事件的节点
   * @param callback 点击回调函数
   * @param target 回调函数的this指向,默认为当前组件
   */
  protected registerClickEvent(node: Node, callback: () => void, target: any = this) {
    // 检查节点上是否有Button组件
    const button = node.getComponent(Button);
    if (button) {
      // 如果有Button组件,注册Button的点击事件
      button.node.on(Button.EventType.CLICK, callback, target);
      return;
    }

    // 检查节点上是否有Toggle组件
    const toggle = node.getComponent(Toggle);
    if (toggle) {
      // 如果有Toggle组件,注册Toggle的点击事件
      toggle.node.on(Toggle.EventType.CLICK, callback, target);
      return;
    }

    // 如果既没有Button也没有Toggle,作为普通节点注册点击事件
    node.on(Node.EventType.TOUCH_END, callback, target);
  }
}

4.2.控制器基类

import { Component } from 'cc';

export class BaseController extends Component {
  protected view: any;

  public initView(view: any) {
    this.view = view;
    // 进行一些通用的视图初始化操作,比如设置视图的父节点等
    this.view.node.parent = this.node;
    // 调用绑定事件的方法
    this.bindViewEvents();
  }

  protected bindViewEvents() {
    // 子类可以重写此方法来绑定具体的事件
  }
}

4.3.模型基类

export class BaseModel {
  private changeCallbacks: Map<string, ((value: any) => void)[]> = new Map();

  // Register change listener for any field
  onChange(field: string, callback: (value: any) => void) {
    if (!this.changeCallbacks.has(field)) {
      this.changeCallbacks.set(field, []);
    }
    this.changeCallbacks.get(field)!.push(callback);
    return () => {
      const callbacks = this.changeCallbacks.get(field);
      if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
          callbacks.splice(index, 1);
        }
      }
    };
  }

  // Notify change for any field
  protected notifyChange(field: string, value: any) {
    const callbacks = this.changeCallbacks.get(field);
    if (callbacks) {
      callbacks.forEach((callback) => callback(value));
    }
  }
}

该基类主要是赋予模型通知视图的能力。当模型的数据发生变更的时候,视图可以马上监听到属性变更。例如英雄主界面上,需要实时更新剩余的金币数量和道具数据,如下:

这种需求可以直接在英雄主界面的逻辑里进行注册,如下:

@ccclass('HeroMainPaneController')
export class HeroMainPaneController extends BaseController {
      protected start(): void {
    this.initView(this.mainView);

    // 监听道具变化
    BagpackModel.getInstance().onChange('item', (items: Map<number, Item>) => {
      let itemCount = items.get(2003);
      if (this.mainView.itemLabel) {
        this.mainView.itemLabel.string = itemCount?.count.toString() || '0';
      }
    });

    // 监听金币变化
    PurseModel.getInstance().onChange('gold', (value) => {
      if (this.mainView.goldLabel) {
        this.mainView.goldLabel.string = value.toString();
      }
    });
  }
}

当钱包金币发生变化时,只需广播一个属性变更事件即可:

import { BaseModel } from '../../ui/BaseModel';

export class PurseModel extends BaseModel {
  private static instance: PurseModel;

  private _diamond: number = 0;
  private _gold: number = 0;

  public static getInstance(): PurseModel {
    if (!PurseModel.instance) {
      PurseModel.instance = new PurseModel();
    }
    return PurseModel.instance;
  }

  // 使用 getter/setter 来触发数据变化通知
  get diamond(): number {
    return this._diamond;
  }

  set diamond(value: number) {
    if (this._diamond !== value) {
      this._diamond = value;
      this.notifyChange('diamond', value);
    }
  }

  get gold(): number {
    return this._gold;
  }

  set gold(value: number) {
    if (this._gold !== value) {
      this._gold = value;
      this.notifyChange('gold', value);
    }
  }
}

完整项目请参考:  cocos客户端+go服务器

相关文章:

  • 基于springboot餐饮连锁店管理系统
  • HTML — 浮动
  • 2-刷力扣问题记录
  • .py文件和.ipynb文件的区别:完整教程
  • 【安装配置教程】在linux使用nginx部署vue项目
  • 【玩泰山派】5、点灯,驱动led (使用python库操作)
  • GMSL 使用 GPIO Forward 功能实现 Frame Sync
  • MetaGPT深度解析:重塑AI协作开发的智能体框架实践指南
  • 云服务器租用费用都受哪些因素影响?
  • QML实现RTSP以及本地解码播放
  • spring cloud OpenFeign 详解:安装配置、客户端负载均衡、声明式调用原理及代码示例
  • 系分论文《论面向服务开发方法在设备租赁行业的应用》
  • 软考高级-系统架构设计师 其他知识补充
  • Laravel源码进阶
  • AWTK-MVVM 如何让多个View复用一个Model记录+关于app_conf的踩坑
  • 再看 MPTCP 时的思考
  • Spring 是如何解决循环依赖的?
  • Redis-分布式锁
  • Shell打印命令返回的数组只显示第一个元素
  • 云豹录屏大师:多功能免费录屏工具
  • 沈阳做微信和网站的公司/企业网站seo优化外包
  • 杭州的做网站公司/石家庄seo排名外包
  • 佳木斯网站制作/官方百度app下载安装
  • 帮传销组织做网站/中小企业网站制作
  • 太原网站建设51sole/推广产品最好的方式
  • 大连比较好的软件公司/上海城市分站seo