ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于Cocos Creator与TsrPC的轻量级多人游戏状态同步方案实践

2026/8/8 6:31:28 拓冰建站 浏览量
基于Cocos Creator与TsrPC的轻量级多人游戏状态同步方案实践 1. 项目概述从零构建一个可落地的多人同步方案最近在做一个休闲小游戏核心需求就是让几个朋友能在同一个房间里实时看到彼此的角色移动。市面上成熟的解决方案不少像 Photon、Colyseus 这些后端服务或者 Mirror、Fish-Net 这类 Unity 的框架功能强大但要么需要付费要么学习成本不低对于想快速验证玩法、或者像我一样想深入理解底层同步逻辑的开发者来说总感觉隔了一层。于是我把目光投向了 Cocos Creator 和 Tsrpc 这个组合。Cocos Creator 做 2D/3D 内容开发足够轻快Tsrpc 则是一个基于 TypeScript 的全栈 RPC 框架前后端共享类型定义开发体验非常流畅。最关键的是它足够轻量、透明让你能完全掌控网络同步的每一个环节。这个项目就是我用 Cocos Creator 3.x 和 Tsrpc 搭建的一套房间制多人在线状态同步的完整解决方案。它不追求大而全的 MMORPG 功能而是聚焦于最核心的“状态同步”问题如何让一个房间内的所有玩家实时、平滑地看到其他玩家的移动和状态变化。如果你正在寻找一个轻量级、可完全自托管、代码清晰且易于二次开发的多人联机原型方案特别是对于回合制、休闲竞技、棋牌或者简单的社交应用场景这套方案会是一个非常好的起点。它帮你解决了网络连接、房间管理、状态广播这些脏活累活让你能更专注于游戏玩法本身的实现。2. 技术选型与架构设计思路为什么是 Cocos Creator Tsrpc这个组合背后是一套非常务实的全栈 JavaScript/TypeScript 开发思路。2.1 为什么选择 Tsrpc 作为网络层首先看后端。对于中小型项目尤其是原型阶段我们往往希望后端足够简单、快速迭代。Node.js 生态在这方面有天然优势。Tsrpc 的核心价值在于“类型安全”和“开发效率”。传统的 WebSocket 开发前后端需要手动约定数据格式写一堆 JSON 解析和校验代码很容易出错。Tsrpc 通过共享 TypeScript 类型定义让前后端像调用本地函数一样进行网络通信。你在后端定义好一个协议比如PtlJoinTsrpc 会自动生成前端的调用代码包括完整的类型提示。这意味着如果你在后端修改了某个字段的类型前端 TypeScript 编译时会直接报错将运行时错误提前到编译时极大地提升了联调效率和代码健壮性。此外Tsrpc 内置了 HTTP 和 WebSocket 双协议支持对于状态同步这种需要长连接、高频数据交换的场景WebSocket 是更自然的选择。它避免了 HTTP 轮询带来的延迟和性能开销。注意Tsrpc 的“全栈”特性意味着你需要将前后端共享的类型定义Protocols放在一个独立的目录或npm包中。在项目结构上我采用了shared文件夹来存放这些协议定义前后端都通过相对路径或npm link引用确保类型定义唯一源。2.2 Cocos Creator 客户端的考量Cocos Creator 3.x 对 TypeScript 的支持已经非常完善其组件化开发模式与前端开发习惯一脉相承。选择它一方面是看中其跨平台发布能力Web、iOS、Android、小游戏等另一方面是其活跃的社区和相对温和的学习曲线。对于多人游戏客户端核心挑战在于网络状态的渲染与本地操作的响应。我们需要将接收到的网络状态其他玩家的位置平滑地呈现在画面上同时又要将本地的操作摇杆输入及时发送给服务器。这就要求客户端有一个清晰的分层架构网络管理层、状态同步层、实体表现层玩家角色需要解耦。2.3 整体架构设计整个项目的架构可以概括为“客户端-服务器-客户端”的星型拓扑。服务器作为权威状态源和消息中转站。客户端 (Cocos Creator)网络管理器 (NetworkManager)单例负责 WebSocket 连接的建立、维护以及 API 调用和消息监听。游戏控制器 (GameController)场景的总控负责房间的加入/离开、创建/销毁玩家实体、分发网络状态到具体的玩家控制器。玩家控制器 (PlayerController)每个玩家实体包括自己和其他玩家的控制器负责接收移动指令本地摇杆或网络同步的位置并驱动 Spine 动画、更新位置。输入模块 (VirtualJoystick)虚拟摇杆将屏幕触摸输入转换为标准化方向向量。服务器 (Node.js Tsrpc)房间管理器 (Room)核心类。管理所有房间Mapstring, RoomState处理玩家加入/离开维护每个房间内所有玩家的状态位置、缩放、名称等。状态广播当任一玩家的状态发生变化如移动服务器会将该玩家所在房间的完整状态快照广播给房间内所有其他连接。输入监听监听每个客户端发来的移动输入指令更新服务器端该玩家的权威状态然后触发广播。共享协议 (Shared Protocols)定义了前后端通信的所有数据结构如ReqJoin加入房间请求、ResJoin加入响应、MsgRoomState房间状态消息、PlayerInput玩家输入、RoomState房间状态等。这是保证前后端一致性的基石。这种架构的优势在于逻辑清晰服务器拥有最终决定权可以有效防止客户端作弊虽然本项目原型阶段未做严格校验。缺点是服务器压力随房间内玩家数量增加而线性增长因为每次状态变化都需要广播全量状态。对于小房间如2-8人的休闲游戏这完全在可接受范围内。3. 核心实现细节拆解理解了整体架构我们深入到代码层面看看几个最关键的环节是如何实现的。3.1 共享类型与协议定义这是 Tsrpc 项目的起点也是保证类型安全的关键。在shared/protocols/目录下我们定义所有类型。GameState.ts- 核心状态定义// 玩家输入指令 export interface PlayerInput { playerId: string; scale: number; // 角色朝向通过缩放实现1为右-1为左 pos: { x: number, y: number }; } // 单个玩家的状态 export interface PlayerState { x: number; y: number; scale: number; name: string; } // 整个房间的状态一个房间ID对应一个RoomState export interface RoomState { players: { [playerId: string]: PlayerState; }; } // 当前玩家信息 export interface CurrentPlayer { roomId: string; playerId: string; playerName: string; }这里定义了数据传输的“形状”。PlayerInput是客户端发送给服务器的操作指令只包含必要信息谁往哪走。RoomState是服务器广播给所有客户端的权威状态包含了房间里所有玩家的完整信息。PtlJoin.ts- 加入房间协议定义export interface ReqJoin { roomId: string; playerId: string; } export interface ResJoin { success: boolean; players: RoomState; roomId: string; currentPlayerId: string; currentPlayerName?: string; // 可选的用于显示名称 }Tsrpc 会根据这个文件在后端启动时自动生成对应的服务端处理代码桩在前端生成强类型的 API 调用客户端代码ApiJoin.ts。你不需要手动写任何序列化/反序列化代码。3.2 服务器端房间管理与状态同步服务器端的核心是Room类它管理着房间的生命周期和状态同步。Room.ts- 房间业务逻辑import { WsConnection } from tsrpc; import { ServiceType } from ./shared/protocols/serviceProto; // Tsrpc生成的服务类型 export class Room { // 内存存储所有房间状态 private rooms: { [roomId: string]: RoomState } {}; // 存储所有活跃连接 private conns: WsConnectionServiceType[] []; async joinRoom(request: ReqJoin, conn: WsConnectionServiceType): PromiseResJoin { const { roomId, playerId } request; // 1. 房间不存在则创建 if (!this.rooms[roomId]) { this.rooms[roomId] { players: {} }; } // 2. 在连接对象上标记玩家和房间信息非常重要 conn.playerId playerId; conn.roomId roomId; this.conns.push(conn); // 3. 初始化新玩家状态例如出生在随机位置 const initX Math.random() * 10; const initY Math.random() * 10; this.rooms[roomId].players[playerId] { x: initX, y: initY, scale: 1, // 默认朝右 name: Player_${playerId.substr(0, 4)} // 简单生成个名字 }; // 4. 创建当前玩家信息对象用于响应客户端 const currentPlayer: CurrentPlayer { roomId, playerId, playerName: this.rooms[roomId].players[playerId].name }; // 5. 广播“有新人加入”的状态给房间内所有人包括自己 await this.broadcastGameState(roomId, RoomStateType.JOIN_STATE, currentPlayer); // 6. 监听这个连接的输入消息 this.onMessageInput(conn); // 7. 返回成功响应并附上当前房间内所有玩家信息 return { success: true, players: this.rooms[roomId], roomId, currentPlayerId: playerId, currentPlayerName: currentPlayer.playerName }; } }joinRoom方法做了几件关键事房间管理、连接绑定、状态初始化、广播通知。其中将playerId和roomId绑定到conn对象上是后续定向广播的关键。状态广播与输入处理private async broadcastGameState(roomId: string, type: RoomStateType, data: any) { const msg: MsgRoomState { type, data }; // 找到该房间内的所有连接 const roomConns this.conns.filter(conn conn.roomId roomId); // 并行发送提高效率 await Promise.all(roomConns.map(conn conn.sendMsg(room/RoomState, msg))); } private onMessageInput(conn: WsConnectionServiceType) { conn.listenMsg(player/Input, (msg: MsgInput) { const { roomId, playerId } conn; // 从连接中取出之前绑定的信息 if (!roomId || !playerId || !this.rooms[roomId]?.players[playerId]) { return; } // 1. 更新服务器权威状态 const playerState this.rooms[roomId].players[playerId]; playerState.x msg.input.pos.x; playerState.y msg.input.pos.y; playerState.scale msg.input.scale; // 2. 广播更新后的整个房间状态给所有人除了输入者自己这里广播给了所有人包括自己 // 广播给自己可以实现“客户端预测服务器回滚”中的权威状态校正但本项目简化处理统一广播。 this.broadcastGameState(roomId, RoomStateType.INPUT_STATE, this.rooms[roomId]); }); }broadcastGameState是同步的发动机。每当房间状态变化它就打包成MsgRoomState消息发送给房间内所有客户端。onMessageInput监听每个玩家的输入先更新服务器状态再触发广播。这里采用了一种简单的“状态同步”模式服务器定期或事件触发时将完整状态快照发送给所有客户端。客户端用这个快照来覆盖本地其他玩家的状态。实操心得连接管理this.conns数组存储了所有连接。在实际项目中一定要实现leaveRoom逻辑在连接关闭onClose时从conns中移除该连接并从对应的rooms状态中移除玩家并广播LEAVE_STATE消息。否则会导致内存泄漏和状态混乱。原示例代码中提到了离开房间的方法这是必须完善的。3.3 客户端网络状态接收与渲染客户端的关键在于如何将接收到的网络状态平滑、正确地渲染到屏幕上。GameController.ts- 游戏总控与状态分发客户端的GameController是大脑它负责连接服务器、加入房间并监听服务器广播的状态消息。// 加入房间 joinRoom() { this.playerId this.generatePlayerId(); // 生成一个唯一ID network.ws.callApi(room/Join, { roomId: 99999999, playerId: this.playerId }).then(res { if (!res.isSucc) return; // 1. 创建房间内已存在的其他玩家 const existingPlayers res.res.players.players; for (let id in existingPlayers) { if (id this.playerId) continue; // 跳过自己 this.createPlayerEntity(id, existingPlayers[id], false); // false表示是其他玩家 } // 2. 创建自己控制的玩家实体 this.createPlayerEntity(this.playerId, res.res.currentPlayerInfo, true); // true表示是自己 // 3. 开始监听房间状态变化 this.startListeningRoomState(); }); } // 监听房间状态消息 startListeningRoomState() { network.ws.listenMsg(room/RoomState, (msg: MsgRoomState) { switch (msg.type) { case RoomStateType.JOIN_STATE: this.onPlayerJoined(msg.data as CurrentPlayer); break; case RoomStateType.LEAVE_STATE: this.onPlayerLeft(msg.data as CurrentPlayer); break; case RoomStateType.INPUT_STATE: this.onRoomStateUpdated(msg.data as RoomState); // 处理移动同步 break; } }); }joinRoom成功后服务器会返回当前房间内所有玩家的信息。客户端需要据此实例化出其他玩家的角色。然后通过listenMsg持续监听三种状态消息加入、离开、状态更新。onRoomStateUpdated- 状态同步的核心这是实现平滑同步的关键函数。当收到服务器广播的完整RoomState后需要更新本地所有其他玩家实体的位置。onRoomStateUpdated(roomState: RoomState) { for (let playerId in roomState.players) { // 跳过自己自己的位置由本地输入和服务器校正决定本项目简化自己也会收到广播 if (playerId this.playerId) { // 这里可以添加客户端预测与服务器回滚的逻辑 continue; } const serverPlayerState roomState.players[playerId]; const playerNode this.findPlayerNode(playerId); if (playerNode) { const playerCtrl playerNode.getComponent(PlayerController); this.syncPlayerPosition(playerCtrl, serverPlayerState); } } this.updateRenderOrder(); // 根据Y轴更新渲染层级 }syncPlayerPosition- 平滑插值与动画处理直接设置node.position会显得很生硬。我们需要使用 Cocos Creator 的 Tween 系统进行插值实现平滑移动。syncPlayerPosition(playerCtrl: PlayerController, targetState: PlayerState) { const playerNode playerCtrl.node; const currentPos playerNode.position; const targetPos new Vec3(targetState.x, targetState.y, 0); // 1. 判断是否需要移动避免微小抖动 if (Vec3.distance(currentPos, targetPos) 0.01) { return; } // 2. 停止该角色可能正在进行的旧动画 this.stopPreviousTween(playerCtrl); // 3. 更新朝向通过scale.x的正负实现 playerCtrl.setScaleX(targetState.scale); // 4. 播放移动动画如果当前不是移动动画 if (!playerCtrl.isPlayingWalkAnimation()) { playerCtrl.playAnimation(PlayerAnimState.WALK); } // 5. 使用Tween进行位置插值 const tweenInstance tween(playerNode) .to(0.1, { position: targetPos }) // 0.1秒内移动到目标位置 .call(() { // 移动结束后播放待机动画 playerCtrl.playAnimation(PlayerAnimState.IDLE); // 清理tween引用 this.removeTween(playerCtrl); }) .start(); // 6. 记录tween实例以便后续中断 this.saveTween(playerCtrl, tweenInstance); }这里有几个关键点距离阈值避免因网络浮点数误差导致的微小抖动而频繁播放动画。动画状态管理移动时播放 Walk 动画到达后播放 Idle 动画。这需要与你的 Spine 或 Animation 组件状态机配合。Tween 管理必须保存 Tween 实例的引用。因为网络帧率比如每秒10-20次广播可能高于 Tween 动画的持续时间0.1秒。如果新的目标位置到来时旧的移动动画还没播完需要先停止旧的 Tween再开始新的否则会出现“抽搐”现象。渲染层级在 2D 游戏中通常需要根据角色的 Y 坐标动态调整渲染层级zIndex 或 siblingIndex让后面的角色被前面的角色遮挡。updateRenderOrder方法就是遍历所有玩家节点按 Y 坐标从大到小排序并设置setSiblingIndex。3.4 本地输入采集与发送本地玩家角色的移动由虚拟摇杆控制。输入需要立即在本地得到响应立即反馈同时发送给服务器。PlayerController.ts(本地玩家)update(deltaTime: number) { if (!this.isLocalPlayer) return; // 只有本地玩家才处理输入 // 1. 从虚拟摇杆获取输入向量 const inputVec this.virtualJoystick ? this.virtualJoystick.getDirection() : Vec2.ZERO; // 2. 本地预测立即更新位置和动画 if (!inputVec.equals(Vec2.ZERO)) { const moveSpeed 200; const deltaPos new Vec3(inputVec.x, inputVec.y, 0).multiplyScalar(moveSpeed * deltaTime); this.node.position this.node.position.add(deltaPos); // 更新朝向 this.spine.node.scaleX inputVec.x 0 ? 1 : -1; // 播放移动动画 this.playAnimation(PlayerAnimState.WALK); // 3. 发送输入给服务器 this.sendInputToServer(inputVec); } else { // 无输入播放待机动画 this.playAnimation(PlayerAnimState.IDLE); } } sendInputToServer(inputVec: Vec2) { const input: PlayerInput { playerId: this.playerId, scale: this.spine.node.scale.x, // 传递朝向 pos: { x: this.node.position.x, y: this.node.position.y } // 传递当前位置 }; network.ws.sendMsg(player/Input, { input }); }这里实现的是最简单的“客户端预测”形式本地先移动再把结果告诉服务器。服务器收到后会用自己的权威状态进行广播客户端在收到广播后会用服务器的权威位置来覆盖本地其他玩家的位置。对于本地玩家本项目简化处理也直接使用了服务器广播的位置这会导致轻微的延迟感。更高级的做法是采用客户端预测服务器权威回滚与调和但这套方案复杂度高得多本项目作为入门方案暂不涉及。注意事项发送频率优化在update中每帧发送输入消息是不可取的会造成巨大的网络流量。通常有两种优化方式1)节流每隔固定时间如50ms发送一次。2)状态变化才发送记录上一帧的输入向量只有发生变化时才发送。推荐使用节流方式代码更简单网络流量也可控。4. 项目部署与联调实战代码写完了如何让它跑起来并让多个客户端真正连在一起这里涉及到本地开发调试和简单的部署。4.1 环境准备与项目启动Node.js 环境确保安装了 Node.js (建议 LTS 版本) 和 npm。创建 Tsrpc 后端项目npx create-tsrpc-applatest my-game-server cd my-game-server npm install选择WebSocket和API功能。项目创建后将我们之前写的shared协议目录、Room.ts等业务代码放入src目录下相应的位置。Cocos Creator 项目创建一个新的 3.x 项目。将客户端脚本GameController,PlayerController等、UI 预制体摇杆准备好。同样需要将shared协议目录链接或复制到客户端项目中确保类型一致。共享协议的处理为了前后端共享类型最佳实践是将shared目录发布为一个私有 npm 包或者使用npm link在本地链接。对于快速原型也可以直接复制一份但务必保持同步。4.2 服务器启动与配置在后端项目根目录npm run devTsrpc 会启动开发服务器默认可能在http://localhost:3000。它会自动监听src目录下协议文件Ptl*.ts的变化并实时生成前端的Api*.ts调用代码。关键配置在src/index.ts或tsrpc.config.ts中// 创建WebSocket服务器 export const server new WsServer(serviceProto, { port: 3001, // WebSocket 端口避免与HTTP端口冲突 // ... 其他配置如JSON序列化、日志等级等 });确保 WebSocket 服务器的端口如 3001与客户端连接地址一致。4.3 客户端连接与测试在 Cocos Creator 的NetworkManager中配置服务器的 WebSocket 地址// NetworkManager.ts export class NetworkManager { public ws: HttpClient | null null; init() { const client new WsClient(serviceProto, { server: ws://localhost:3001, // 本地开发地址 // server: ws://你的服务器公网IP:3001, // 部署后地址 logger: console }); this.ws client; client.connect(); // 建立连接 } }本地多开测试Cocos Creator 编辑器运行一个客户端实例。然后使用浏览器的“无痕窗口”或不同的浏览器Chrome, Firefox, Edge再次打开游戏网页这样就可以模拟多个玩家同时在线。观察控制台日志和游戏画面检查角色是否都能正确创建、移动同步是否平滑。4.4 简单的公网部署用于朋友间测试要让外网的朋友也能连接你需要一台有公网 IP 的服务器云服务器如阿里云 ECS、腾讯云 CVM 等。服务器环境在服务器上安装 Node.js。上传代码将后端项目代码my-game-server上传到服务器。安装依赖并启动cd my-game-server npm install --production npm run build # 编译TypeScript npm start # 或使用 pm2 守护进程pm2 start npm --name game-server -- run start安全组/防火墙在云服务器控制台放行你配置的 WebSocket 端口如 3001 的 TCP 协议。客户端修改连接地址将客户端代码中的server地址改为你的服务器公网 IP 或域名例如ws://123.123.123.123:3001。构建与分发在 Cocos Creator 中构建 Web Mobile 或 Web Desktop 版本将构建出的build目录下的文件部署到任何一个静态网站托管服务如 GitHub Pages, Vercel, Netlify 或你自己的 Nginx 服务器。你的朋友通过访问这个网页就能连接到你的游戏服务器了。重要提示这种部署方式仅适用于原型测试或极小规模的熟人社交。对于正式上线的产品你需要考虑1)使用 HTTPS/WSS现代浏览器要求安全上下文下才能使用 WebSocket。你需要为域名配置 SSL 证书。2)使用反向代理通常用 Nginx 将wss://yourdomain.com/game代理到本地的ws://localhost:3001并处理 SSL。3)进程守护使用pm2或systemd确保 Node.js 进程崩溃后自动重启。4)负载均衡与水平扩展单个 Node.js 进程有连接数上限。当用户量增长时需要设计更复杂的架构如分房间到不同进程/机器并引入 Redis 等进行状态共享和消息广播。5. 常见问题、优化与扩展方向在实际开发和测试中你肯定会遇到各种问题。这里总结一些典型问题和我踩过的坑。5.1 网络延迟与同步抖动这是多人游戏永恒的主题。在本项目的简单状态同步模型下你会明显感觉到其他玩家的移动有延迟并且在停止时可能会“回弹”一下。问题根源网络传输需要时间RTT。从你发送输入到服务器处理并广播再到其他客户端接收至少有1.5个 RTT 的延迟。如果网络不稳定还会出现丢包和乱序。优化方案1插值与外推我们已经使用了 Tween 插值让移动平滑。还可以尝试“外推”Extrapolation即根据其他玩家上一帧的速度和方向预测他当前帧的位置等收到新的权威状态后再纠正。这能减少“停顿感”但预测错误时会产生更明显的“拉扯”。优化方案2提高广播频率在服务器端可以不用每次收到输入都广播。而是设置一个固定的“心跳”间隔如每秒15-20次定时广播房间状态。这能稳定网络流量但会增加同步延迟。优化方案3减少数据量广播完整的RoomState在玩家多时会很大。可以改为广播增量状态Delta Update只发送发生变化的部分。PlayerInput也可以只发送方向向量由服务器计算最终位置。5.2 断线重连与状态恢复玩家网络波动断开后如何重新加入并恢复到断线前的状态服务器端在Room类中不能只在joinRoom时创建新状态。需要实现一个reconnect协议。当客户端重连时发送旧的playerId和roomId服务器检查该玩家状态是否还存在可以设置一个“离线超时”时间比如30秒如果存在则将其绑定的conn更新为新的连接并下发当前完整的房间状态。客户端网络层NetworkManager需要监听连接断开事件并尝试自动重连。重连成功后调用reconnectAPI 而不是joinRoom。5.3 渲染层级Z-Order闪烁在updatePlayerLayers中我们根据 Y 坐标动态设置setSiblingIndex。如果多个玩家的 Y 坐标非常接近或者在同一帧内多个玩家的位置更新顺序导致排序结果波动就可能出现层级闪烁。解决方案可以引入一个“层级容差”或“稳定化”策略。例如只有当两个玩家的 Y 坐标差值大于某个阈值如0.5时才调整它们之间的顺序。或者可以每 N 帧而不是每帧更新一次层级减少变化频率。5.4 扩展方向这个基础框架可以像乐高一样扩展出很多功能房间列表与匹配实现一个大厅服务器管理所有房间信息房间号、人数、模式等。客户端先连接大厅获取房间列表或进行自动匹配再由大厅分配一个游戏服务器地址给客户端连接。帧同步 vs 状态同步本项目是典型的状态同步快照同步。对于需要高度确定性、操作严格的游戏如 RTS、MOBA可以研究帧同步Lockstep。Tsrpc 同样可以支持核心是服务器转发所有客户端的输入指令每个客户端根据相同的指令序列独立计算游戏逻辑。更复杂的游戏状态目前只同步了位置和朝向。你可以轻松扩展PlayerState和RoomState加入血量、分数、装备、技能冷却等属性并在广播逻辑中处理这些状态的同步。输入缓冲与指令排队为了应对网络抖动可以在客户端实现一个输入缓冲队列按服务器时间戳顺序处理移动指令使同步更平滑。使用 Protobuf 替代 JSONTsrpc 支持 Protobuf 作为二进制传输协议能显著减少数据包大小提升传输效率适合移动网络或更复杂的游戏状态。这套 Cocos Creator Tsrpc 的方案最大的优势就是透明和可控。你能清楚地知道每一个数据包从哪里来、到哪里去出了问题可以快速定位。它可能不是性能最高、功能最全的解决方案但绝对是理解多人游戏网络同步原理、并快速构建出可玩原型的绝佳路径。当你吃透了这套流程再去使用那些成熟的商业框架时你会更加得心应手因为你已经理解了它们背后在解决什么问题。