1. 项目概述与核心思路
最近几年,国粹类游戏在移动端和微信小游戏平台的热度一直居高不下。无论是经典的麻将、斗地主,还是地方性的棋牌玩法,都拥有庞大的用户基础。作为一个有十多年开发经验的老兵,我观察到很多开发者想入局,但往往被客户端与服务端联动的复杂性劝退。今天,我就以“从零撸一个国粹类游戏”为目标,和大家分享一下如何用 Cocos Creator 3.x 搭配 Node.js,搭建一个麻雀虽小但五脏俱全的游戏项目。这个组合的优势在于,Cocos Creator 提供了成熟高效的 2D/3D 游戏开发体验和跨平台发布能力,而 Node.js 凭借其事件驱动、非阻塞 I/O 的特性,非常适合处理游戏房间管理、实时状态同步这类高并发、低延迟的 IO 密集型场景。我们最终要实现的目标是:一个包含基础房间匹配、游戏核心逻辑(以麻将为例)、实时通信和简单数据统计的完整游戏原型。
选择这个技术栈,我主要基于以下几点考量。首先,技术栈的亲和度。Cocos Creator 使用 TypeScript/JavaScript 作为主要开发语言,Node.js 同样如此,这意味着前后端开发者在语言层面是统一的,降低了学习和协作成本。一套逻辑甚至可以在某些校验环节前后端复用。其次,开发效率与生态。Cocos Creator 的编辑器可视化程度高,组件化开发模式成熟,资源管理方便;Node.js 拥有 npm 这个庞大的包生态系统,我们可以轻松引入 Socket.IO 处理 WebSocket 通信、引入 Redis 做缓存和会话管理,用 Mongoose 操作 MongoDB 存储游戏记录,极大地加速了开发进程。最后,性能与架构清晰度。对于国粹类游戏,核心压力在于房间内的实时状态同步和大量并发连接的管理。Node.js 单线程异步模型在处理大量并发连接时,相较于传统的多线程同步模型,在资源消耗和开发复杂度上更有优势,配合 Redis 等中间件,可以构建出清晰、可扩展的服务端架构。
这个项目适合有一定前端或 Cocos Creator 基础,想向全栈游戏开发迈进的开发者。即使你是 Node.js 新手也没关系,我会把环境搭建、核心模块的每一步都拆解清楚。我们不会追求一个商业级的复杂系统,而是聚焦于打通从客户端界面到服务端逻辑的完整链路,让你掌握这类游戏最核心的开发模式。
2. 开发环境搭建与项目初始化
工欲善其事,必先利其器。在开始写代码之前,我们需要把前后端的开发环境准备好。这个过程可能会遇到一些小坑,我会把常见的“坑点”和解决方案一并说明。
2.1 Node.js 与 npm 环境配置
服务端我们选择 Node.js。首先去官网下载 LTS(长期支持)版本。这里有一个关键点:强烈建议使用 Node 版本管理工具(如 nvm-windows 或 nvm)来安装,而不是直接下载安装包。原因很简单,日后切换不同项目所需的 Node 版本会非常方便,也能避免因权限问题导致的安装失败。
以 Windows 平台为例,我们可以使用nvm-windows。在 GitHub 上找到其发布页,下载安装包。安装完成后,以管理员身份打开命令行(CMD 或 PowerShell),执行以下命令安装并使用一个特定的 Node 版本:
nvm list available # 查看可安装的版本列表 nvm install 18.18.0 # 安装指定版本,这里以18.18.0为例 nvm use 18.18.0 # 切换到该版本安装完成后,分别运行node -v和npm -v检查版本。如果遇到类似npm.ps1 无法加载,因为在此系统上禁止运行脚本的错误,这是因为 PowerShell 的执行策略限制。
注意:这是 Windows PowerShell 的一个常见安全策略问题,与任何网络工具或代理无关。它仅仅是为了防止运行未签名的脚本。
解决方法有两种。第一种是以管理员身份运行 PowerShell,然后执行:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser输入Y确认。这个命令将当前用户的执行策略设置为“RemoteSigned”,允许运行本地脚本和来自互联网的已签名脚本。第二种更安全的方法是,在报错的命令行窗口直接输入cmd回车,切换到传统的命令提示符窗口,在那里使用 npm 命令,因为 CMD 不受 PowerShell 执行策略的影响。
2.2 Cocos Creator 编辑器安装与项目创建
前往 Cocos 官网下载 Cocos Dashboard。通过 Dashboard 可以方便地安装和管理不同版本的 Cocos Creator 编辑器。对于新项目,我推荐使用最新的 3.x 稳定版。安装过程比较简单,主要是选择安装路径。
安装完成后,打开 Dashboard,点击“新建”按钮。在项目模板中,我们选择“Empty(2D & 3D)”创建一个空项目。项目名称可以定为MajiangGame,选择纯 TypeScript 作为编程语言。这里有个小建议:将项目创建在一个没有中文和空格的路径下,比如D:\Projects\MajiangGame,可以避免后续一些潜在的模块加载或构建路径问题。
项目创建好后,我们先熟悉一下编辑器界面。资源管理器(Assets)是我们存放所有游戏资源(场景、脚本、图片、音效)的地方。场景(Scene)是游戏内容的载体。层级管理器(Hierarchy)显示当前场景中的所有节点。属性检查器(Inspector)用于查看和修改选中节点或组件的属性。我们第一个任务是在场景中创建一个 Canvas(画布)节点,所有 UI 元素都需要放在这个节点下。
2.3 服务端项目初始化与基础依赖安装
接下来,我们在另一个独立的文件夹(例如D:\Projects\MajiangServer)初始化我们的 Node.js 服务端项目。打开命令行,进入该目录,执行:
npm init -y这会生成一个默认的package.json文件。然后,我们安装项目所需的核心依赖:
npm install express socket.io mongoose redis npm install -D typescript ts-node-dev @types/node @types/express @types/socket.io解释一下这些包的作用:
express: 轻量级的 Web 框架,用于提供 HTTP 接口(如登录、获取排行榜)。socket.io: 实现 WebSocket 双向实时通信的库,是游戏房间内状态同步的核心。mongoose: MongoDB 的对象模型工具,用于优雅地操作数据库。redis: Redis 客户端,用于存储用户会话、房间状态等高速缓存数据。typescript,ts-node-dev等是开发依赖,让我们能用 TypeScript 编写服务端代码并实现热重载。
安装完成后,我们需要配置 TypeScript。在项目根目录创建tsconfig.json文件:
{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }这个配置指定了源代码放在src目录,编译输出到dist目录。接着,在package.json的scripts字段中添加启动脚本:
"scripts": { "dev": "ts-node-dev --respawn --transpile-only src/app.ts", "build": "tsc", "start": "node dist/app.js" }现在,执行npm run dev就可以启动一个支持热更新的开发服务器了(当然,现在src/app.ts还不存在,会报错,我们下一步就创建它)。
3. 服务端核心架构设计与实现
服务端是整个游戏的大脑,负责逻辑运算、数据持久化和客户端间的消息转发。一个清晰的架构能让我们后续的开发事半功倍。
3.1 项目目录结构与基础服务搭建
我们先在src目录下创建如下的目录结构:
src/ ├── app.ts # 应用入口文件 ├── config/ # 配置文件 ├── models/ # 数据库模型 (Mongoose Schemas) ├── services/ # 业务逻辑服务 ├── sockets/ # Socket.IO 事件处理 └── utils/ # 工具函数首先,在app.ts中搭建一个最基础的 HTTP 和 WebSocket 服务器:
import express from 'express'; import http from 'http'; import { Server } from 'socket.io'; import path from 'path'; const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: "http://localhost:7456", // Cocos Creator 编辑器预览地址 credentials: true } }); // 提供静态文件,未来可以放一些客户端下载资源 app.use(express.static(path.join(__dirname, '../public'))); // 一个简单的健康检查接口 app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Socket.IO 连接事件 io.on('connection', (socket) => { console.log(`用户已连接: ${socket.id}`); socket.on('disconnect', () => { console.log(`用户断开连接: ${socket.id}`); // 这里需要处理用户断线,从房间移除等逻辑 }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`服务器运行在 http://localhost:${PORT}`); });运行npm run dev,如果看到服务器启动的日志,说明基础框架就跑通了。
3.2 数据模型设计与 MongoDB 集成
国粹游戏需要持久化存储用户信息、游戏记录、排行榜等数据。我们使用 MongoDB 这种文档型数据库,它 schema 灵活,非常适合游戏数据。通过mongoose来建模。
首先,确保你本地安装了 MongoDB 或者有一个远程连接地址。然后在src/models下创建User.ts和GameRecord.ts。
User.ts (用户模型)
import mongoose, { Schema, Document } from 'mongoose'; export interface IUser extends Document { username: string; openid?: string; // 微信小游戏等平台唯一标识 avatarUrl: string; goldCoin: number; // 游戏金币 score: number; // 积分,用于排行榜 lastLoginAt: Date; createdAt: Date; } const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, openid: { type: String, sparse: true }, // sparse index,允许为null但唯一 avatarUrl: { type: String, default: 'default_avatar.png' }, goldCoin: { type: Number, default: 1000 }, // 新用户送1000金币 score: { type: Number, default: 0 }, lastLoginAt: { type: Date, default: Date.now }, createdAt: { type: Date, default: Date.now } }); // 创建索引以加速查询 UserSchema.index({ score: -1 }); // 排行榜按分数降序 UserSchema.index({ openid: 1 }, { unique: true, sparse: true }); export default mongoose.model<IUser>('User', UserSchema);GameRecord.ts (游戏记录模型)
import mongoose, { Schema, Document } from 'mongoose'; // 定义玩家在单局游戏中的快照 interface PlayerSnapshot { userId: mongoose.Types.ObjectId; username: string; scoreDelta: number; // 本局得分变化 finalScore: number; // 本局结束后总分(可用于回放校验) } export interface IGameRecord extends Document { roomId: string; gameType: string; // 如 'mahjong_standard' players: PlayerSnapshot[]; steps: any[]; // 存放每一步操作,用于回放。可以序列化为JSON字符串。 startTime: Date; endTime: Date; } const GameRecordSchema: Schema = new Schema({ roomId: { type: String, required: true }, gameType: { type: String, required: true }, players: [{ userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, username: { type: String, required: true }, scoreDelta: { type: Number, required: true }, finalScore: { type: Number, required: true } }], steps: { type: Schema.Types.Array, default: [] }, startTime: { type: Date, default: Date.now }, endTime: { type: Date } }); export default mongoose.model<IGameRecord>('GameRecord', GameRecordSchema);在app.ts中,我们需要连接 MongoDB。将连接逻辑抽象到src/config/database.ts中是个好习惯。
3.3 游戏房间管理服务
这是服务端最复杂的部分之一。房间(Room)是游戏对局的基本单位。我们需要管理房间的创建、加入、退出、状态同步和销毁。这里会用到 Socket.IO 的“房间”功能和 Redis。
为什么引入 Redis?
- 状态共享:Node.js 是多进程友好的,未来如果部署多个服务实例,内存中的房间状态无法共享。Redis 可以作为中心化的状态存储。
- 发布/订阅 (Pub/Sub):用于向特定房间广播消息,非常高效。
- 过期键:可以给房间设置过期时间,比如30分钟无人操作自动解散,防止僵尸房间。
我们先安装ioredis(一个功能强大的 Redis 客户端)并创建房间服务src/services/RoomService.ts。其核心职责包括:
- 创建房间:生成唯一房间号,初始化房间状态(玩家列表、牌局状态等),存入 Redis。
- 加入房间:检查房间是否存在、是否满员,将玩家 Socket ID 加入 Socket.IO 房间和 Redis 房间成员列表。
- 房间状态同步:当某个玩家执行操作(如出牌)时,服务端验证逻辑后,通过 Redis Pub/Sub 或直接在 Socket.IO 房间内广播状态更新。
- 解散房间:游戏结束或超时后,清理 Redis 和 Socket.IO 中的房间数据,并持久化游戏记录。
由于麻将逻辑非常复杂,我们在第一版原型中,可以简化状态。一个房间在 Redis 中的 Hash 结构可能如下:
Key: room:{roomId} Fields: - status: 'waiting' | 'playing' | 'ended' - players: [JSON数组,包含玩家ID、座位号、手牌(加密或仅服务端持有)、分数等] - currentTurn: 当前操作玩家的座位号 - deck: 剩余的牌堆(数组序列化后的字符串) - lastAction: 上一个动作(如“出牌”、“摸牌”)房间服务会提供一系列方法,如createRoom(gameType, creatorId),joinRoom(roomId, playerId),makeAction(roomId, playerId, action, data)等,供 Socket 事件处理器调用。
实操心得:在房间状态设计上,一定要区分“权威状态”和“视图状态”。权威状态(服务器 Redis 中存储的)是唯一可信源,任何游戏逻辑判断都基于此。客户端只持有用于渲染的视图状态。当网络延迟或客户端异常时,要以服务端状态为准进行同步或纠正,这是防止作弊和保证一致性的关键。
4. 客户端 Cocos Creator 核心功能开发
客户端是玩家直接交互的界面,需要做到逻辑清晰、反馈及时。我们以麻将游戏为例,分解几个核心模块。
4.1 网络通信层封装
在 Cocos Creator 的assets/scripts目录下,我们创建一个network文件夹,里面放置网络相关的脚本。首先是一个SocketManager.ts单例类,用于管理 Socket.IO 连接。
// assets/scripts/network/SocketManager.ts import { io, Socket } from "socket.io-client"; import { _decorator } from 'cc'; export class SocketManager { private static _instance: SocketManager = null; private socket: Socket = null; private serverUrl: string = 'http://localhost:3000'; // 根据环境配置 public static get instance(): SocketManager { if (!this._instance) { this._instance = new SocketManager(); } return this._instance; } private constructor() { // 私有构造函数,防止外部 new } public connect(): Promise<void> { return new Promise((resolve, reject) => { if (this.socket?.connected) { resolve(); return; } this.socket = io(this.serverUrl, { transports: ['websocket'], // 优先使用 WebSocket autoConnect: true, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, }); this.socket.on('connect', () => { console.log('Socket.IO 连接成功,ID:', this.socket.id); resolve(); }); this.socket.on('connect_error', (error) => { console.error('Socket.IO 连接失败:', error); reject(error); }); }); } public emit(event: string, data?: any): void { if (!this.socket?.connected) { console.warn('Socket 未连接,尝试发送事件:', event); // 可以在这里加入重连逻辑或队列 return; } this.socket.emit(event, data); } public on(event: string, callback: (data: any) => void): void { this.socket?.on(event, callback); } public off(event: string, callback?: (data: any) => void): void { this.socket?.off(event, callback); } public disconnect(): void { this.socket?.disconnect(); } public getSocketId(): string { return this.socket?.id || ''; } }这个管理器提供了连接、发送、监听事件的基础功能。在游戏入口场景(如大厅)的onLoad方法中,调用SocketManager.instance.connect()建立连接。
4.2 游戏大厅与房间匹配界面
大厅是玩家进入游戏的第一个界面。我们需要在这里实现:
- 用户登录/注册:调用 HTTP 接口(使用
fetch或axios)与后端express路由交互。 - 快速开始匹配:客户端发送“开始匹配”事件到服务端。服务端维护一个匹配队列,当凑齐一桌玩家(如4人)时,创建一个房间,并将房间信息推送给所有匹配玩家。
- 房间列表:展示当前可加入的公开房间,允许玩家输入房间号加入。
匹配逻辑可以放在服务端的MatchService中。当玩家请求匹配时,将其加入一个 Redis 有序集合(Sorted Set),分数为当前时间戳。然后启动一个定时任务或每次加入时检查,从集合中取出最早加入的 N 个玩家,组成房间。
大厅场景的脚本要点:
- 要有“快速开始”按钮,点击后显示“匹配中...”的提示,并禁用按钮。
- 监听服务端发来的
match_success事件,事件数据中包含roomId。收到后,自动跳转到游戏房间场景。 - 提供“创建房间”和“加入房间”的输入框和按钮,用于好友约战。
4.3 麻将游戏房间场景与核心逻辑
这是最复杂的部分。一个麻将房间场景通常包含以下节点:
- 牌桌背景:一个 Sprite 节点。
- 玩家区域(4个):每个区域包含头像、名字、分数、手牌区、打出的牌区。
- 公共区域:牌墙(剩余牌堆)、当前打出的牌、计时器。
- 操作按钮组:吃、碰、杠、胡、摸牌、出牌等,根据游戏状态动态显示/隐藏。
核心游戏循环与状态同步:
- 初始化:进入房间后,客户端向服务端发送
ready事件。当所有玩家准备就绪,服务端初始化牌局(洗牌、发牌),通过game_start事件将初始状态(玩家手牌、庄家、风圈等)广播给所有客户端。 - 游戏进行:
- 客户端:渲染手牌(通常是牌的背面或正面,取决于是否是自己)。监听玩家的触摸事件,当玩家选择一张手牌并点击“出牌”按钮时,向服务端发送
play_card事件,附带牌的唯一ID。 - 服务端:收到
play_card事件后,进行严格验证:是否是该玩家的回合?这张牌是否在其手牌中?验证通过后,更新 Redis 中的房间状态(从该玩家手牌移除该牌,添加到牌池),然后通过card_played事件广播给所有客户端,包含出牌玩家和牌的信息。 - 其他客户端:收到
card_played事件后,更新牌桌中央的“打出的牌”显示。
- 客户端:渲染手牌(通常是牌的背面或正面,取决于是否是自己)。监听玩家的触摸事件,当玩家选择一张手牌并点击“出牌”按钮时,向服务端发送
- 特殊操作(吃碰杠胡):
- 当其他玩家打出一张牌时,符合条件的玩家(如可以碰、杠、胡)客户端会收到服务端下发的
action_available事件,其中包含可执行的操作列表。 - 客户端收到后,立即显示对应的操作按钮(如“碰”、“杠”、“胡”),并启动一个倒计时(例如10秒)。
- 玩家点击按钮后,客户端发送
action_taken事件(如{action: 'pong', card: targetCardId})到服务端。 - 服务端验证并处理该动作,更新全局状态,然后广播
action_confirmed事件,所有客户端更新界面(如执行碰牌的玩家亮出三张相同的牌)。
- 当其他玩家打出一张牌时,符合条件的玩家(如可以碰、杠、胡)客户端会收到服务端下发的
- 游戏结束:当有玩家胡牌或流局时,服务端计算分数,发送
game_over事件,包含最终排名和分数变化。客户端展示结算界面,并更新本地的玩家金币、积分信息。
注意事项:客户端的所有操作指令,都必须经过服务端的验证和确认。绝对不能在客户端直接修改游戏状态然后广播,这是线上游戏的大忌,极易被破解。客户端的角色是“输入采集器”和“状态渲染器”,真正的游戏状态机在服务端。
5. 关键问题排查与性能优化实录
在实际开发中,你一定会遇到各种各样的问题。这里我记录几个最典型的问题和解决方案。
5.1 Socket.IO 连接不稳定与断线重连
在移动网络环境下,连接断开是常事。我们需要一套健壮的重连机制。
问题现象:玩家偶尔会卡住,然后看到“连接断开”的提示。解决方案:
- 客户端主动心跳:除了 Socket.IO 自带的心跳,客户端可以每隔20秒向服务端发送一个
ping事件,服务端回应pong。如果连续3次没收到pong,则认为连接异常,触发重连。 - 监听连接状态:Socket.IO 客户端提供了
connect,disconnect,reconnect等事件。我们需要在SocketManager中监听这些事件,并更新 UI 状态(如显示“连接中...”)。 - 重连时的状态恢复:断线重连后,玩家可能还在游戏中。客户端在连接成功后,应立即发送一个
rejoin_room事件,附带之前的roomId。服务端检查该房间是否存在且玩家是否在其中,如果是,则将完整的当前房间状态(full_sync)下发给该玩家,使其界面恢复到最新状态。
// 在 SocketManager 中增强重连逻辑 private setupEventListeners() { this.socket.on('disconnect', (reason) => { console.log('连接断开,原因:', reason); // 显示断线提示UI this.showDisconnectTip(); // 如果是网络错误等原因,可以尝试自动重连 if (reason === 'io server disconnect' || reason === 'transport close') { setTimeout(() => this.connect(), 2000); } }); this.socket.on('reconnect', (attemptNumber) => { console.log(`第${attemptNumber}次重连成功`); // 隐藏断线提示 this.hideDisconnectTip(); // 尝试重新加入之前的房间 if (this.lastRoomId) { this.emit('rejoin_room', { roomId: this.lastRoomId }); } }); }5.2 服务端麻将逻辑验证的复杂性
麻将的规则校验极其复杂,不同地区规则差异巨大。如何保证服务端逻辑的正确性和可维护性?
问题:胡牌判断、算番等逻辑代码容易变成一团乱麻,难以调试和扩展。解决方案:采用状态机与规则引擎分离的设计。
- 定义游戏状态枚举:将一局麻将抽象成几个状态,如
Dealing(发牌),Playing(行牌),Pong/Kong/Chow(等待副露操作),WaitingForDraw(等待摸牌),GameOver。 - 使用有限状态机:每个状态只处理特定的事件。例如,在
Playing状态,只接受play_card事件;在Pong状态,只接受action_taken事件(且动作只能是“碰”、“过”等)。状态转移由服务端的RoomService控制。 - 规则引擎抽离:将胡牌判断、番种计算等独立成纯函数模块。例如,创建一个
MahjongRuleEngine.ts文件,提供如下函数:
这样,主逻辑代码只关心状态流转和事件分发,具体的规则计算交给专门的引擎,结构清晰,也便于单元测试。// 判断一组手牌+刚摸的牌/别人打的牌是否能胡 export function canWin(handCards: Card[], targetCard: Card, gameRule: string): { success: boolean, fanTypes: FanType[] } { // 根据 gameRule ('standard', 'guobiao'等) 调用不同的规则实现 // 返回是否能胡以及胡的番种 } // 计算分数 export function calculateScore(fanTypes: FanType[], dealer: boolean): number { // 根据番种和是否庄家计算得分 }
5.3 Cocos Creator 资源管理与包体优化
当游戏图片、音效资源多起来后,加载速度和包体大小会成为问题。
问题:首次加载白屏时间长,游戏包体过大。解决方案:
- 使用 Asset Bundle:Cocos Creator 的 Asset Bundle 功能可以将资源按场景或功能模块划分。例如,将大厅资源、麻将房间资源、通用UI资源分成不同的 Bundle。首次只加载大厅 Bundle,进入房间时再动态加载房间 Bundle。
- 纹理压缩与合图:对于 UI 图片,尽量使用 Sprite Atlas(合图)功能,将多张小图合并成一张大图,减少 Draw Call。同时,根据目标平台选择合适的纹理压缩格式(如 Web 平台用 PNG,小游戏平台用 PVRTCC 等)。
- 音频优化:背景音乐使用较长的
mp3,音效使用较短的wav或ogg。注意控制同时播放的音效数量,避免卡顿。 - 代码分割:使用 TypeScript 的模块化,将网络模块、游戏逻辑模块、UI 组件模块分开。Cocos Creator 在构建时会进行一定的代码优化。
踩坑记录:曾经遇到一个诡异的问题,在微信小游戏平台,部分安卓机型上,游戏进行中会突然卡死。经过大量排查,发现是某个循环中频繁创建临时
Vec2对象,导致垃圾回收(GC)过于频繁。解决方案是,对于高频调用的函数(如每帧执行的 update 或触摸事件处理),避免在函数内部 new 对象,改为使用预先声明的类成员变量或对象池。例如,在麻将牌拖动判断时,将临时坐标变量声明在类作用域,在touchMove回调中复用,而不是每次都new Vec2()。
6. 部署上线与基础运维考虑
当本地开发测试完成后,就需要考虑如何让其他人能玩到你的游戏了。
6.1 服务端部署
Node.js 服务端可以部署在云服务器(如腾讯云、阿里云的轻量应用服务器)或容器平台。以下是基本步骤:
- 环境准备:在服务器上安装 Node.js、PM2(进程管理工具)、MongoDB 和 Redis。
- 代码上传与构建:将服务端代码上传到服务器,运行
npm install --production安装依赖,然后运行npm run build编译 TypeScript。 - 使用 PM2 启动:PM2 可以保持应用常驻,并在崩溃时自动重启。
npm install -g pm2 pm2 start dist/app.js --name mahjong-server pm2 save pm2 startup # 设置开机自启 - 配置反向代理:使用 Nginx 将域名(如
api.yourgame.com)的请求代理到 Node.js 服务的端口(如3000),并配置 SSL 证书启用 HTTPS。WebSocket 连接也需要在 Nginx 中做相应配置。 - 环境变量管理:将数据库连接字符串、Redis 地址、密钥等敏感信息通过环境变量(如
.env文件)传入,不要硬编码在代码中。
6.2 客户端构建与发布
Cocos Creator 支持一键构建到多个平台。
- 构建配置:在 Cocos Creator 编辑器的“项目 -> 构建”中,选择目标平台(如 Web Mobile、微信小游戏)。根据平台要求配置 AppID、启动画面等。
- 资源处理:构建时,注意选择合适的“MD5 Cache”选项,这会给资源文件名添加哈希值,有利于浏览器缓存更新。
- 小游戏发布:如果发布微信小游戏,构建完成后,会生成一个
wechatgame文件夹。用微信开发者工具打开这个文件夹,上传代码,提交审核即可。 - 版本更新:对于 Web 版本,如果更新了资源,需要处理好 CDN 缓存和客户端资源版本号,避免玩家加载到旧资源。
6.3 基础监控与日志
游戏上线后,需要知道它的运行状况。
- 日志记录:使用
winston或log4js等日志库,将不同级别的日志(错误、警告、信息)输出到文件和控制台。错误日志要包含详细的上下文信息(如用户ID、房间号、操作数据),方便排查。 - 基础监控:
- 进程监控:PM2 自带
pm2 monit可以查看 CPU/内存占用。 - 接口监控:可以写一个简单的中间件,记录每个 HTTP 接口的响应时间和状态码。
- 关键业务指标:在代码中埋点,记录每日活跃用户(DAU)、对局数、平均对局时长等,这些数据可以写入 MongoDB 或专门的统计系统。
- 进程监控:PM2 自带
- 错误报警:将
error级别的日志通过邮件、钉钉机器人或 Sentry 等工具发送报警,让你能第一时间知道线上问题。
从零开始构建一个完整的国粹游戏确实是一个不小的工程,它涉及了前端渲染、实时通信、服务端逻辑、数据存储等多个领域。但通过 Cocos Creator 和 Node.js 这套组合,你可以用熟悉的 JavaScript/TypeScript 语言贯穿始终,极大地降低了全栈开发的门槛。这个过程中,最重要的不是一开始就做出一个功能完美的游戏,而是先把核心链路跑通——能创建房间、能发牌、能出一张牌并让其他玩家看到。然后,再像搭积木一样,一步步加入碰杠胡、算番、结算、重连、音效、动画等细节。每完成一个功能模块,你都会对游戏开发有更深的理解。最后,多测试,尤其是弱网络环境下的测试,你会发现并解决很多在理想环境下遇不到的问题,这才是让游戏真正变得“可用”和“可靠”的关键。