Oops Framework-8-由空项目创建第一个登录界面
一、项目架构
因为Oops Framework整个的流程是基本固定的,所以要做的就是手动填满所有的流程,做的所有东西实际上都要按部就班的按规则做。在动手编码前,先明确整体架构流程,确保各模块职责清晰。
┌─────────────────────────────────────────────────┐ │ Main.ts │ │ 游戏入口,继承框架Root,初始化ECS/UI/SDK │ └──────────────┬──────────────────────────────────┘ │ ┌───────────▼────────────┐ │ Initialize (ECS) │ │ 首个ECS实体,启动资源加载流程 │ └───────────┬────────────┘ │ ┌───────────▼────────────┐ │ InitResSystem │ │ 异步队列加载:Bundle→多语言→通用资源 │ └───────────┬────────────┘ │ ┌───────────▼────────────┐ │ LoadingViewComp │ │ 加载界面+原生热更逻辑 │ └───────────┬────────────┘ │ ┌───────────▼────────────┐ │ Login Component │ │ 多渠道登录(测试/SDK/微信)+ 账号鉴权 │ └───────────┬────────────┘ │ ┌───────────▼────────────┐ │ Account / AccountData │ │ 账号数据管理+登录状态维护 │ └─────────────────────────┘二、环境准备
开始前确保以下环境就绪:
- 安装 Cocos Creator 3.8+(兼容 Oops Framework);
- 安装 Oops Framework 插件(执行框架提供的
update-oops-plugin-framework.bat/.sh); - 确认项目目录结构规范,
assets/resources里面只保证config.json文件 assets/bundle为框架默认资源加载目录。
三、核心模块手动实现
步骤 1:基础配置文件
1.1 框架核心配置(config.json)
创建assets/resources/config.json,定义项目基础配置(版本、服务器、多语言、UI 层级等):
json
{ "type": "prod", "config": { "prod": { "version": "1.0.0", "package": "com.example.game", "frameRate": 60, "localDataKey": "your-encrypt-key", // AES加密密钥(16位) "localDataIv": "your-encrypt-iv-", // AES加密向量(16位) "httpServer": "", "httpTimeout": 10000, "mobileSafeArea": false, "stats": 0 } }, "language": { "type": ["zh", "en"], "default": "zh", "path": { "json": "language/json" } }, "bundle": { "default": "bundle" }, "gui": [ { "type": "UI", "name": "LayerUI" }, { "type": "PopUp", "name": "LayerPopUp" }, { "type": "Dialog", "name": "LayerDialog" } ] }1.2 多语言配置
这里只是示例
创建多语言 JSON 文件,统一管理文本,避免硬编码:
assets/bundle/language/json/zh.json
{ "loading_load_player": "加载玩家数据", "loading_load_json": "加载配置表", "update_tips_check_update": "检查更新中" }assets/bundle/language/json/en.json:
{ "loading_load_player": "Loading player data", "loading_load_json": "Loading config tables", "update_tips_check_update": "Checking for updates" }1.3 常量与枚举定义
存储键枚举(StorageKey.ts)-assets/script/game/common/config/StorageKey.ts:
export enum StorageKey { Account = 'Account', Server = 'Server', LoginUsePolicy = 'LoginUsePolicy', }游戏常量(GameConst.ts)-assets/script/game/common/config/GameConst.ts:
// 协议ID枚举 export enum MsgID { Login = 1001, LoginAuth = 1002, } // 埋点类型枚举 export enum SubmitTypes { EnterGame = 1, ExitGame = 2, }游戏事件枚举(GameEvent.ts)-assets/script/game/common/config/GameEvent.ts:
export enum GameEvent { GameServerConnected = "GameServerConnected", LoginSuccess = "LoginSuccess", LoginError = 'LoginError', Tick1S = 'Tick1S', // 1秒心跳 }资源路径工具(GameResPath.ts)-assets/script/game/common/config/GameResPath.ts:
export class GameResPath { // 获取龙骨模型路径 static getModelPath(name: string): { skeleton: string; atlas: string } { return { skeleton: `model/${name}/${name}_ske`, atlas: `model/${name}/${name}_tex`, }; } // 获取通用精灵帧路径 static getSpriteFrameCommon(name: string): string { return `common/texture/${name}/spriteFrame`; } } // 音频路径常量 export const AudioClipMusic = { Main: 'audio/bgm_main', Game: 'audio/bgm_game', };UI 配置(GameUIConfig.ts)-assets/script/game/common/config/GameUIConfig.ts:
import { LayerType } from "../../../../../extensions/oops-plugin-framework/assets/core/gui/layer/LayerEnum"; import { UIConfig } from "../../../../../extensions/oops-plugin-framework/assets/core/gui/layer/UIConfig"; // UI唯一标识 export enum UIID { Loading = 1, Login = 2, Main = 3, } // UIID与层/预制体映射 export var UIConfigData: { [key: number]: UIConfig } = { [UIID.Loading]: { layer: LayerType.UI, prefab: "gui/loading/loading" }, [UIID.Login]: { layer: LayerType.UI, prefab: "gui/login/login" }, [UIID.Main]: { layer: LayerType.UI, prefab: "gui/main/main" }, };步骤 2:单例模块注册
创建assets/script/game/common/SingletonModuleComp.ts,统一管理全局单例模块,简化调用:
运行
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { account, Account } from "../account/Account"; import { Initialize } from "../initialize/Initialize"; @ecs.register('SingletonModule') export class SingletonModuleComp extends ecs.Comp { // 初始化模块 initialize: Initialize = null!; // 账号模块(getter封装) get account(): Account { return account; } reset() { } } // 全局单例访问器 export var smc: SingletonModuleComp = ecs.getSingleton(SingletonModuleComp);步骤 3:游戏入口(Main.ts)
创建assets/script/Main.ts,作为游戏根入口,继承框架Root类,初始化核心能力:
运行
import { director, game, macro, _decorator } from 'cc'; import { oops } from '../../extensions/oops-plugin-framework/assets/core/Oops'; import { Root } from '../../extensions/oops-plugin-framework/assets/core/Root'; import { ecs } from '../../extensions/oops-plugin-framework/assets/libs/ecs/ECS'; import { GameEvent } from './game/common/config/GameEvent'; import { UIConfigData, UIID } from './game/common/config/GameUIConfig'; import { smc } from './game/common/SingletonModuleComp'; import { EcsInitializeSystem, Initialize } from './game/initialize/Initialize'; import { PlatformSDK } from './game/common/sdk/PlatformSDK'; const { ccclass } = _decorator; @ccclass('Main') export class Main extends Root { start() { // 初始化平台SDK PlatformSDK.init(); // 注册定时心跳事件 this.schedule(() => { oops.message.dispatchEvent(GameEvent.Tick1S) }, 1, macro.REPEAT_FOREVER, 0.1); } onDestroy() { this.unscheduleAllCallbacks(); } // 配置加载完成后,启动初始化ECS实体 protected run() { smc.initialize = ecs.getEntity<Initialize>(Initialize); } // 注册UI配置 protected initGui() { oops.gui.init(UIConfigData); } // 注册ECS系统 protected initEcsSystem() { oops.ecs.add(new EcsInitializeSystem()); } } // 全局快捷访问 export var app = new Main();心跳网络游戏开发中使用,单机可以注释掉
步骤 4:初始化模块(ECS)
4.1 初始化实体(Initialize.ts)
创建assets/script/game/initialize/Initialize.ts,作为首个 ECS 实体,启动资源加载:
运行
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCEntity } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCEntity"; import { InitResComp, InitResSystem } from "./bll/InitRes"; @ecs.register('Initialize') export class Initialize extends CCEntity { protected init() { this.add(InitResComp); // 添加资源加载组件 } } // ECS初始化系统 export class EcsInitializeSystem extends ecs.System { constructor() { super(); this.add(new InitResSystem()); } }4.2 资源加载逻辑(InitRes.ts)
创建assets/script/game/initialize/bll/InitRes.ts,实现异步资源加载队列:
typescript
运行
import { oops } from "../../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { AsyncQueue, NextFunction } from "../../../../../extensions/oops-plugin-framework/assets/libs/collection/AsyncQueue"; import { ecs } from "../../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { UIID } from "../../common/config/GameUIConfig"; import { Initialize } from "../Initialize"; import { LoadingViewComp } from "../view/LoadingViewComp"; @ecs.register('InitRes') export class InitResComp extends ecs.Comp { reset() { } } // 资源加载系统:Bundle→多语言→通用资源→加载界面 export class InitResSystem extends ecs.ComblockSystem implements ecs.IEntityEnterSystem { filter(): ecs.IMatcher { return ecs.allOf(InitResComp); } entityEnter(e: Initialize): void { var queue: AsyncQueue = new AsyncQueue(); // 1. 加载远程Bundle this.loadBundle(queue); // 2. 加载多语言包 this.loadLanguage(queue); // 3. 加载通用资源 this.loadCommon(queue); // 4. 加载完成后打开加载界面 this.onComplete(queue, e); // 执行队列 queue.play(); } private loadBundle(queue: AsyncQueue) { queue.push(async (next: NextFunction) => { const bundleName = oops.res.defaultBundleName; if (bundleName && bundleName !== "resources") { await oops.res.loadBundle(bundleName); } next(); }); } private loadLanguage(queue: AsyncQueue) { queue.push((next: NextFunction) => { let lan = oops.storage.get("language"); if (!lan) { lan = oops.config.game.languageDefault; oops.storage.set("language", lan); } oops.language.setLanguage(lan, next); }); } private loadCommon(queue: AsyncQueue) { queue.push((next: NextFunction) => { oops.res.loadDir("common", next); }); } private onComplete(queue: AsyncQueue, e: Initialize) { queue.complete = async () => { var node = await oops.gui.open(UIID.Loading); if (node) e.add(node.getComponent(LoadingViewComp)!); e.remove(InitResComp); }; } }步骤 5:加载界面与热更
5.1 加载界面组件(LoadingViewComp.ts)
创建assets/script/game/initialize/view/LoadingViewComp.ts,实现加载进度展示与资源加载:
import { Prefab, sys, _decorator } from "cc"; import { oops } from "../../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { ecs } from "../../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCViewVM } from "../../../../../extensions/oops-plugin-framework/assets/module/common/CCViewVM"; import { UIID } from "../../common/config/GameUIConfig"; import { HotUpdate } from "./HotUpdate"; import { PlatformSDK } from "../../common/sdk/PlatformSDK"; import { Initialize } from "../Initialize"; const { ccclass } = _decorator; @ccclass('LoadingViewComp') @ecs.register('LoadingView', false) export class LoadingViewComp extends CCViewVM<Initialize> { // 绑定UI的数据(进度、提示文本) data: any = { finished: 0, total: 0, progress: "0", prompt: "" }; private progress: number = 0; reset(): void { this.data.prompt = oops.language.getLangByID("loading_load_player"); oops.gui.open(UIID.Login); // 加载完成打开登录界面 oops.gui.remove(UIID.Loading); // 关闭加载界面 } start() { // 原生平台且不跳过热更时,启动热更 if (sys.isNative && !PlatformSDK.isSkipHotUpdate()) { this.addComponent(HotUpdate); } else { this.enter(); } } enter() { this.loadRes(); } private async loadRes() { this.data.progress = 0; await this.loadCustom(); // 加载JSON配置表 this.loadGameRes(); // 加载游戏预制体/纹理 } private loadCustom() { this.data.prompt = oops.language.getLangByID("loading_load_json"); return new Promise(async (resolve) => { // 此处扩展:加载自定义JSON配置表 resolve(); }); } private loadGameRes() { this.data.prompt = oops.language.getLangByID("loading_load_game"); oops.res.loadDir( "gui/main", Prefab, this.onProgressCallback.bind(this), this.onCompleteCallback.bind(this) ); } // 加载进度回调 private onProgressCallback(finished: number, total: number) { this.data.finished = finished; this.data.total = total; var progress = finished / total; if (progress > this.progress) { this.progress = progress; this.data.progress = (progress * 100).toFixed(2); } } // 加载完成回调 private onCompleteCallback() { this.ent.remove(LoadingViewComp); } }5.2 热更管理(HotUpdate.ts + Hot.ts)
HotUpdate.ts-assets/script/game/initialize/view/HotUpdate.ts:
运行
import { Component, game, _decorator } from "cc"; import { oops } from "../../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { Hot, HotOptions } from "./Hot"; import { LoadingViewComp } from "./LoadingViewComp"; const { ccclass } = _decorator; @ccclass('HotUpdate') export class HotUpdate extends Component { private hot = new Hot(); private lv: LoadingViewComp = null!; onLoad() { this.lv = this.getComponent(LoadingViewComp)!; this.lv.data.prompt = oops.language.getLangByID("update_tips_check_update"); this.startHotUpdate(); } private startHotUpdate() { let options = new HotOptions(); // 热更进度回调 options.onUpdateProgress = (event: jsb.EventAssetsManager) => { let pc = event.getPercent(); if (!isNaN(pc)) { this.lv.data.finished = event.getDownloadedFiles(); this.lv.data.total = event.getTotalFiles(); this.lv.data.progress = (pc * 100).toFixed(2); } }; // 无需热更 options.onNoNeedToUpdate = () => { this.lv.enter(); }; // 热更成功(重启游戏) options.onUpdateSucceed = () => { this.lv.data.progress = 100; setTimeout(() => { game.restart(); }, 1000); }; // 热更失败(重试) options.onUpdateFailed = () => { this.hot.checkUpdate(); }; this.hot.init(options); } }Hot.ts-assets/script/game/initialize/view/Hot.ts(热更核心逻辑):
运行
export class HotOptions { onUpdateProgress?: (event: any) => void; onNoNeedToUpdate?: () => void; onUpdateSucceed?: () => void; onUpdateFailed?: (code: number, msg: string) => void; } export class Hot { private options: HotOptions = null!; init(options: HotOptions) { this.options = options; this.checkUpdate(); } // 检查热更(需对接项目实际的manifest地址) checkUpdate() { if (!jsb) { this.options.onNoNeedToUpdate?.(); return; } // 扩展点:实现jsb.AssetsManager的热更逻辑 this.options.onNoNeedToUpdate?.(); } // 版本对比工具方法 versionCompareHandle(versionA: string, versionB: string): number { const partsA = versionA.split('.').map(Number); const partsB = versionB.split('.').map(Number); for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { const a = partsA[i] || 0; const b = partsB[i] || 0; if (a !== b) return a - b; } return 0; } }步骤 6:账号与登录模块
6.1 账号管理(Account.ts + AccountData.ts)
Account.ts-assets/script/game/account/Account.ts(登录 / 登出 / 鉴权):
运行
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { MsgID } from "../common/config/GameConst"; import { httpChannel } from "../common/net/HttpChannelManager"; import { AccountData } from "./AccountData"; export class Account { data: AccountData = null; // 账号密码登录 login(req: LoginReq) { httpChannel.req(MsgID.Login, req, this.onLoginHttpResp.bind(this), () => { oops.message.dispatchEvent(GameEvent.LoginError); } ); } // SDK token登录 loginAuth(req: LoginAuthReq) { httpChannel.req(MsgID.LoginAuth, req, this.onLoginHttpResp.bind(this), () => { oops.message.dispatchEvent(GameEvent.LoginError); } ); } // 登出 logout() { this.data = null; } // 检查是否登录 isLogin(): boolean { return this.data != null; } // 登录成功回调 private onLoginHttpResp(httpResp: HttpSuccResp) { if (httpResp.code !== 0) { oops.message.dispatchEvent(GameEvent.LoginError, `登录失败(${httpResp.message})`); return; } const resp = httpResp.data as LoginResp; if (!resp?.ok) { oops.message.dispatchEvent(GameEvent.LoginError, `登录失败(${resp.msg})`); return; } this.data = new AccountData(httpResp.data); oops.storage.setUser(this.data.id); httpChannel.setAuthorization(this.data.token); oops.message.dispatchEvent(GameEvent.LoginSuccess); } } // 全局账号实例 export var account = new Account(); // 类型定义(可迁移到NetProtocol.ts) export interface LoginReq { username: string; password: string; serverid: number; } export interface LoginAuthReq { userid: string; gameLoginToken: string; serverid: number; channelType: number; } export interface HttpSuccResp { code: number; message: string; data: any; } export interface LoginResp { ok: boolean; msg?: string; gt: string; role: { id: string; name: string }; info: { rolelevel: number }; }AccountData.ts-assets/script/game/account/AccountData.ts(账号数据封装):
运行
import { LoginResp } from "./Account"; export class AccountData { protected data: LoginResp = null; constructor(data: LoginResp) { this.data = data; } get info() { return this.data; } get id(): string { return this.data.role.id; } get name(): string { return this.data.role.name; } get token(): string { return this.data.gt; } get heroLevel(): number { return this.data.info.rolelevel; } }6.2 登录 UI 组件(Login.ts)
创建assets/script/game/login/Login.ts,实现登录界面交互:
运行
import { Component, EditBox, Toggle, _decorator } from 'cc'; import { oops } from '../../../../extensions/oops-plugin-framework/assets/core/Oops'; import { app } from '../../Main'; import { GameEvent } from '../common/config/GameEvent'; import { UIID } from "../common/config/GameUIConfig"; import { StorageKey } from "../common/config/StorageKey"; import { SubmitTypes } from "../common/config/GameConst"; import { smc } from '../common/SingletonModuleComp'; import { PlatformSDK } from "../common/sdk/PlatformSDK"; import { LoginReq, LoginAuthReq } from "../account/Account"; const { ccclass, property } = _decorator; @ccclass('Login') export class Login extends Component { @property(EditBox) private edtAccount: EditBox; @property(Toggle) private tglUsePolicy: Toggle; @property(Node) private nodeUsePolicy: Node; private _inLoading = false; get loading() { return this._inLoading; } set loading(v: boolean) { this._inLoading = v; } async onLoad() { // 监听登录事件 oops.message.on(GameEvent.LoginSuccess, this._eventHandler, this); oops.message.on(GameEvent.LoginError, this._eventHandler, this); } async start() { // 测试渠道预填账号 if (PlatformSDK.isTestChannel()) { var user = oops.storage.get(StorageKey.Account); if (user) { this.edtAccount.string = user; } } else { this.edtAccount.node.active = false; // 已同意协议则自动登录 if (oops.storage.getNumber(StorageKey.LoginUsePolicy, 0)) { PlatformSDK.doLogin(); } } this.initView(); } onDestroy() { oops.message.off(GameEvent.LoginSuccess, this._eventHandler, this); oops.message.off(GameEvent.LoginError, this._eventHandler, this); } // 初始化协议勾选状态 initView() { var storage = oops.storage.get(StorageKey.LoginUsePolicy); this.nodeUsePolicy.active = (storage == "" || Number(storage) != 1); } // 事件处理 private async _eventHandler(event: string, msg: string = null) { if (event == GameEvent.LoginSuccess) { PlatformSDK.doSubmit(SubmitTypes.EnterGame); await oops.gui.open(UIID.Main); oops.gui.remove(UIID.Login, true); } else if (event == GameEvent.LoginError) { this.loading = false; oops.gui.toast(msg || "登录失败"); } } // 登录按钮点击 onLoginClick() { if (this.loading) return; if (PlatformSDK.isTestChannel()) { this.testLogin(); } else { this.sdkLogin(); } } // 测试渠道登录 private testLogin() { const account = this.edtAccount.string.trim(); if (!account) { oops.gui.toast("请输入账号"); return; } oops.storage.set(StorageKey.Account, account); const req: LoginReq = { username: account, password: "123123", serverid: 1 }; smc.account.login(req); this.loading = true; } // SDK登录 private async sdkLogin() { const data = await PlatformSDK.doLogin(); if (!data) return; await this.serverLogin(data); } // 对接游戏服务器登录 private async serverLogin(data: RespLogin) { this.loading = true; // 扩展点:对接平台鉴权服务器、获取游戏服务器信息、最终登录 const reqLogin: LoginAuthReq = { userid: data.uid, gameLoginToken: "", // 从平台鉴权服务器获取 serverid: 1, channelType: PlatformSDK.cfg.channel_type, }; smc.account.loginAuth(reqLogin); } // 同意协议 onClickSure() { if (!this.tglUsePolicy.isChecked) { oops.gui.toast('请同意隐私政策'); return; } oops.storage.set(StorageKey.LoginUsePolicy, 1); PlatformSDK.doLogin(); this.nodeUsePolicy.active = false; } // 退出游戏 onClickCancel() { app.exit(); } } // 类型定义 export interface RespLogin { uid: string; token: string; }步骤 7:网络与 SDK 封装
7.1 HTTP 请求管理(HttpChannelManager.ts)
创建assets/script/game/common/net/HttpChannelManager.ts,封装游戏服务器 HTTP 请求:
typescript
运行
import { oops } from "../../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { GameServerConfig } from "../../login/Login"; class HttpChannelManager { private authorization: string = ""; private gameServer: GameServerConfig = null; // 设置鉴权Token setAuthorization(token: string) { this.authorization = token; } // 设置游戏服务器配置 setGameServer(s: GameServerConfig) { this.gameServer = s; } // 发送HTTP请求 req(msgId: number, req: any, onOk: Function, onErr?: Function) { if (!this.gameServer?.gameUrl) { onErr?.("游戏服务器未配置"); return; } const url = `${this.gameServer.gameUrl}/api/${msgId}`; const headers: Record<string, string> = { "Content-Type": "application/json", }; if (this.authorization) { headers["Authorization"] = `Bearer ${this.authorization}`; } // 扩展点:实现oops.http.post请求 // oops.http.post(url, req, headers, onOk, onErr); } } export var httpChannel = new HttpChannelManager();7.2 平台 SDK 抽象(PlatformSDK.ts)
创建assets/script/game/common/sdk/PlatformSDK.ts,统一多平台 SDK 接入:
运行
import { game } from "cc"; import { SubmitTypes } from "../config/GameConst"; import { RespLogin } from "../../login/Login"; class PlatformSDK { cfg = { channel_type: 0, skip_hot_update: false, }; // 初始化SDK init() { /* 扩展点:对接微信/字节/QQ等SDK */ } // 销毁SDK destroy() { /* 扩展点:清理SDK资源 */ } // 是否跳过热更 static isSkipHotUpdate(): boolean { return platformSDK.cfg.skip_hot_update; } // SDK登录 static async doLogin(): Promise<RespLogin | null> { /* 扩展点:实现平台登录 */ return null; } // 埋点上报 static doSubmit(type: SubmitTypes) { /* 扩展点:实现埋点逻辑 */ } // 是否测试渠道 static isTestChannel(): boolean { return true; } // 退出游戏 static doExit() { game.end(); } } export var platformSDK = new PlatformSDK(); export { PlatformSDK };四、项目验证与扩展
4.1 验证要点
- 确认
Main组件挂载到场景根节点,且根节点包含game和gui子节点; - 检查
config.json的localDataKey/localDataIv为 16 位,JSON 格式合法;这步也可以忽略 - 确保 UI 预制体路径与
UIConfigData中的配置一致; - 多语言文件包含所有使用的
labId,无缺失。
五、核心 API 速查
| API | 用途 |
|---|---|
oops.gui.open(UIID.X) | 打开指定 UI |
oops.gui.toast(msg) | 显示提示弹窗 |
oops.message.dispatchEvent(ev, data) | 派发全局事件 |
oops.res.loadDir(path, type, progCb, doneCb) | 加载目录资源 |
oops.storage.get/set(key, val) | 本地存储读写 |
oops.language.getLangByID(id) | 获取多语言文本 |
ecs.getEntity<T>(Class) | 获取 ECS 实体 |
最终测试界面如下
这个基本算是游戏开发的最简流程,可基于此骨架,补充业务逻辑,快速落地各类小游戏 / 手游项目。
