Cocos2d-x 多人游戏房间管理(创建/加入/退出)【玩转华为云】
【摘要】 引言在多人实时游戏中,房间管理是连接玩家、组织对战的核心模块。传统Cocos2d-x开发中,房间管理常面临网络通信复杂、状态同步困难、跨平台兼容性差等问题。随着移动游戏向"随时随地开黑"发展,玩家期望在不同设备间无缝切换房间,这对网络架构和状态管理提出了更高要求。Cocos2d-x作为成熟的跨平台游戏引擎,结合现代网络编程技术,可以构建高效稳定的多人游戏房间管理系统。本文将基于Cocos2d...
引言
技术背景
1. Cocos2d-x核心能力
-
跨平台渲染:一套代码支持iOS、Android、Windows、macOS,保证房间UI一致性。 -
事件驱动架构:基于EventListener的消息传递机制,便于房间事件的异步处理。 -
序列化支持:ValueMap/ValueVector便于房间数据的序列化与反序列化。 -
多线程支持:分离网络线程与渲染线程,避免网络阻塞影响游戏帧率。
2. 网络通信技术栈
-
WebSocket:双向通信协议,支持房间状态的实时推送(libwebsocket/cocos2d-x扩展)。 -
TCP Socket:可靠的命令传输通道,用于关键操作(创建/加入房间)。 -
HTTP REST API:用于房间列表查询、用户信息验证等RESTful操作。 -
Protobuf:高效的数据序列化格式,减少网络带宽占用。
3. 多人游戏架构模式
-
客户端-服务器模式(C/S):权威服务器验证所有操作,防止作弊。 -
对等网络模式(P2P):去中心化通信,降低服务器压力(适合小规模游戏)。 -
混合架构:关键逻辑服务器验证,实时数据P2P同步(平衡性能与安全性)。
应用场景
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
核心代码实现
1. 房间与玩家数据模型
// RoomModel.h
#ifndef __ROOM_MODEL_H__
#define __ROOM_MODEL_H__
#include "cocos2d.h"
#include <vector>
#include <string>
#include <unordered_map>
USING_NS_CC;
// 玩家角色枚举
enum class PlayerRole {
OWNER = 0, // 房主
MEMBER = 1, // 普通成员
SPECTATOR = 2 // 观战者
};
// 房间状态枚举
enum class RoomStatus {
WAITING = 0, // 等待玩家加入
STARTING = 1, // 游戏开始中
PLAYING = 2, // 游戏中
ENDED = 3 // 游戏结束
};
// 玩家信息结构体
struct PlayerInfo {
std::string playerId; // 玩家唯一ID
std::string nickname; // 玩家昵称
std::string avatarUrl; // 头像URL
PlayerRole role; // 玩家角色
bool isReady; // 是否准备
int seatIndex; // 座位索引(0-based)
std::string devicePlatform; // 设备平台(iOS/Android/PC)
long long joinTimestamp; // 加入时间戳
PlayerInfo() : role(PlayerRole::MEMBER), isReady(false), seatIndex(-1) {}
// 序列化到ValueMap
ValueMap toValueMap() const {
ValueMap dict;
dict["playerId"] = Value(playerId);
dict["nickname"] = Value(nickname);
dict["avatarUrl"] = Value(avatarUrl);
dict["role"] = Value(static_cast<int>(role));
dict["isReady"] = Value(isReady);
dict["seatIndex"] = Value(seatIndex);
dict["devicePlatform"] = Value(devicePlatform);
dict["joinTimestamp"] = Value(joinTimestamp);
return dict;
}
// 从ValueMap反序列化
static PlayerInfo fromValueMap(const ValueMap& dict) {
PlayerInfo info;
info.playerId = dict.at("playerId").asString();
info.nickname = dict.at("nickname").asString();
info.avatarUrl = dict.at("avatarUrl").asString();
info.role = static_cast<PlayerRole>(dict.at("role").asInt());
info.isReady = dict.at("isReady").asBool();
info.seatIndex = dict.at("seatIndex").asInt();
info.devicePlatform = dict.at("devicePlatform").asString();
info.joinTimestamp = dict.at("joinTimestamp").asLong();
return info;
}
};
// 房间配置结构体
struct RoomConfig {
int maxPlayers; // 最大玩家数
int maxSpectators; // 最大观战者数
bool allowSpectator; // 是否允许观战
bool isPrivate; // 是否私人房间
std::string password; // 房间密码(私人房间)
std::string gameMode; // 游戏模式
int mapId; // 地图ID
int timeLimit; // 时间限制(分钟)
RoomConfig() : maxPlayers(4), maxSpectators(2), allowSpectator(false),
isPrivate(false), mapId(1), timeLimit(10) {}
};
// 房间信息结构体
struct RoomInfo {
std::string roomId; // 房间ID
std::string roomName; // 房间名称
std::string ownerId; // 房主ID
RoomStatus status; // 房间状态
RoomConfig config; // 房间配置
std::vector<PlayerInfo> players;// 玩家列表
std::vector<PlayerInfo> spectators; // 观战者列表
long long createTimestamp; // 创建时间戳
long long startTimestamp; // 开始时间戳
int currentPlayerCount; // 当前玩家数
int currentSpectatorCount; // 当前观战者数
RoomInfo() : status(RoomStatus::WAITING), createTimestamp(0),
startTimestamp(0), currentPlayerCount(0), currentSpectatorCount(0) {}
// 序列化到ValueMap
ValueMap toValueMap() const {
ValueMap dict;
dict["roomId"] = Value(roomId);
dict["roomName"] = Value(roomName);
dict["ownerId"] = Value(ownerId);
dict["status"] = Value(static_cast<int>(status));
dict["createTimestamp"] = Value(createTimestamp);
dict["startTimestamp"] = Value(startTimestamp);
dict["currentPlayerCount"] = Value(currentPlayerCount);
dict["currentSpectatorCount"] = Value(currentSpectatorCount);
// 序列化配置
ValueMap configDict;
configDict["maxPlayers"] = Value(config.maxPlayers);
configDict["maxSpectators"] = Value(config.maxSpectators);
configDict["allowSpectator"] = Value(config.allowSpectator);
configDict["isPrivate"] = Value(config.isPrivate);
configDict["password"] = Value(config.password);
configDict["gameMode"] = Value(config.gameMode);
configDict["mapId"] = Value(config.mapId);
configDict["timeLimit"] = Value(config.timeLimit);
dict["config"] = Value(configDict);
// 序列化玩家列表
ValueVector playersVector;
for (const auto& player : players) {
playersVector.pushBack(Value(player.toValueMap()));
}
dict["players"] = Value(playersVector);
// 序列化观战者列表
ValueVector spectatorsVector;
for (const auto& spectator : spectators) {
spectatorsVector.pushBack(Value(spectator.toValueMap()));
}
dict["spectators"] = Value(spectatorsVector);
return dict;
}
// 从ValueMap反序列化
static RoomInfo fromValueMap(const ValueMap& dict) {
RoomInfo info;
info.roomId = dict.at("roomId").asString();
info.roomName = dict.at("roomName").asString();
info.ownerId = dict.at("ownerId").asString();
info.status = static_cast<RoomStatus>(dict.at("status").asInt());
info.createTimestamp = dict.at("createTimestamp").asLong();
info.startTimestamp = dict.at("startTimestamp").asLong();
info.currentPlayerCount = dict.at("currentPlayerCount").asInt();
info.currentSpectatorCount = dict.at("currentSpectatorCount").asInt();
// 反序列化配置
ValueMap configDict = dict.at("config").asValueMap();
info.config.maxPlayers = configDict.at("maxPlayers").asInt();
info.config.maxSpectators = configDict.at("maxSpectators").asInt();
info.config.allowSpectator = configDict.at("allowSpectator").asBool();
info.config.isPrivate = configDict.at("isPrivate").asBool();
info.config.password = configDict.at("password").asString();
info.config.gameMode = configDict.at("gameMode").asString();
info.config.mapId = configDict.at("mapId").asInt();
info.config.timeLimit = configDict.at("timeLimit").asInt();
// 反序列化玩家列表
ValueVector playersVector = dict.at("players").asValueVector();
for (const auto& playerValue : playersVector) {
info.players.push_back(PlayerInfo::fromValueMap(playerValue.asValueMap()));
}
// 反序列化观战者列表
ValueVector spectatorsVector = dict.at("spectators").asValueVector();
for (const auto& spectatorValue : spectatorsVector) {
info.spectators.push_back(PlayerInfo::fromValueMap(spectatorValue.asValueMap()));
}
return info;
}
};
#endif // __ROOM_MODEL_H__
2. 网络管理器(WebSocket + TCP)
// NetworkManager.h
#ifndef __NETWORK_MANAGER_H__
#define __NETWORK_MANAGER_H__
#include "cocos2d.h"
#include "RoomModel.h"
#include <thread>
#include <mutex>
#include <queue>
#include <functional>
#include <websockets/libwebsockets.h>
USING_NS_CC;
class NetworkManager : public Ref {
public:
static NetworkManager* getInstance();
virtual bool init();
~NetworkManager();
// 网络连接管理
void connectToServer(const std::string& wsUrl, const std::string& tcpHost, int tcpPort);
void disconnectFromServer();
bool isConnected() const { return _isConnected; }
// 房间操作
void createRoom(const RoomConfig& config, const std::string& roomName);
void joinRoom(const std::string& roomId, const std::string& password = "");
void leaveRoom();
void kickPlayer(const std::string& playerId);
void startGame();
void toggleReady(bool ready);
// 观战功能
void joinAsSpectator(const std::string& roomId);
void leaveSpectator();
// 回调函数设置
void setOnConnectCallback(const std::function<void()>& callback) { _onConnectCallback = callback; }
void setOnDisconnectCallback(const std::function<void(int)>& callback) { _onDisconnectCallback = callback; }
void setOnRoomCreatedCallback(const std::function<void(const RoomInfo&)>& callback) { _onRoomCreatedCallback = callback; }
void setOnRoomJoinedCallback(const std::function<void(const RoomInfo&)>& callback) { _onRoomJoinedCallback = callback; }
void setOnRoomUpdateCallback(const std::function<void(const RoomInfo&)>& callback) { _onRoomUpdateCallback = callback; }
void setOnPlayerJoinCallback(const std::function<void(const PlayerInfo&)>& callback) { _onPlayerJoinCallback = callback; }
void setOnPlayerLeaveCallback(const std::function<void(const std::string&, const std::string&)>& callback) { _onPlayerLeaveCallback = callback; }
void setOnErrorCallback(const std::function<void(const std::string&)>& callback) { _onErrorCallback = callback; }
private:
NetworkManager();
// WebSocket相关
void initWebSocket();
void processWebSocketMessages();
void sendWebSocketMessage(const std::string& message);
static int websocketCallback(struct lws* wsi, enum lws_callback_reasons reason, void* user, void* in, size_t len);
// TCP相关
void initTCPConnection();
void tcpReceiveThread();
void sendTCPMessage(const std::string& message);
// 消息处理
void handleServerMessage(const std::string& message);
void handleTCPMessage(const std::string& message);
std::string buildCreateRoomMessage(const RoomConfig& config, const std::string& roomName);
std::string buildJoinRoomMessage(const std::string& roomId, const std::string& password);
// 数据序列化
std::string serializeRoomInfo(const RoomInfo& room);
RoomInfo deserializeRoomInfo(const std::string& data);
private:
static NetworkManager* _instance;
// WebSocket
struct lws_context* _wsContext;
struct lws* _wsConnection;
std::thread _wsThread;
std::queue<std::string> _wsSendQueue;
std::mutex _wsMutex;
bool _wsRunning;
// TCP
int _tcpSocket;
std::thread _tcpThread;
std::queue<std::string> _tcpSendQueue;
std::mutex _tcpMutex;
bool _tcpRunning;
std::string _tcpHost;
int _tcpPort;
// 连接状态
bool _isConnected;
std::string _playerId;
std::string _authToken;
// 回调
std::function<void()> _onConnectCallback;
std::function<void(int)> _onDisconnectCallback;
std::function<void(const RoomInfo&)> _onRoomCreatedCallback;
std::function<void(const RoomInfo&)> _onRoomJoinedCallback;
std::function<void(const RoomInfo&)> _onRoomUpdateCallback;
std::function<void(const PlayerInfo&)> _onPlayerJoinCallback;
std::function<void(const std::string&, const std::string&)> _onPlayerLeaveCallback;
std::function<void(const std::string&)> _onErrorCallback;
};
#endif // __NETWORK_MANAGER_H__
// NetworkManager.cpp
#include "NetworkManager.h"
#include <network/HttpClient.h>
#include <network/WebSocket.h>
#include <thread>
#include <chrono>
#include <sstream>
#include <iomanip>
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32)
#pragma comment(lib, "libwebsockets.lib")
#endif
USING_NS_CC;
NetworkManager* NetworkManager::_instance = nullptr;
NetworkManager* NetworkManager::getInstance() {
if (!_instance) {
_instance = new NetworkManager();
_instance->init();
}
return _instance;
}
NetworkManager::NetworkManager()
: _wsContext(nullptr), _wsConnection(nullptr), _wsRunning(false),
_tcpSocket(-1), _tcpRunning(false), _isConnected(false) {
_playerId = StringUtils::format("player_%lld", std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count());
}
NetworkManager::~NetworkManager() {
disconnectFromServer();
}
bool NetworkManager::init() {
return true;
}
void NetworkManager::connectToServer(const std::string& wsUrl, const std::string& tcpHost, int tcpPort) {
_tcpHost = tcpHost;
_tcpPort = tcpPort;
// 初始化WebSocket连接
initWebSocket();
// 初始化TCP连接
initTCPConnection();
}
void NetworkManager::disconnectFromServer() {
_wsRunning = false;
_tcpRunning = false;
if (_wsContext) {
lws_context_destroy(_wsContext);
_wsContext = nullptr;
}
if (_tcpSocket != -1) {
close(_tcpSocket);
_tcpSocket = -1;
}
_isConnected = false;
}
void NetworkManager::initWebSocket() {
// 简化的WebSocket初始化(实际项目需集成libwebsockets)
// 这里使用Cocos2d-x内置WebSocket作为示例
_wsRunning = true;
_wsThread = std::thread(&NetworkManager::processWebSocketMessages, this);
// 模拟连接成功
std::this_thread::sleep_for(std::chrono::seconds(1));
_isConnected = true;
if (_onConnectCallback) _onConnectCallback();
}
void NetworkManager::initTCPConnection() {
_tcpRunning = true;
_tcpThread = std::thread(&NetworkManager::tcpReceiveThread, this);
}
void NetworkManager::processWebSocketMessages() {
while (_wsRunning) {
// 模拟接收WebSocket消息
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 实际项目中这里会处理lws_callback
// 模拟房间更新消息
if (_isConnected && rand() % 100 < 5) { // 5%概率收到更新
RoomInfo room;
room.roomId = "test_room_001";
room.roomName = "测试房间";
room.currentPlayerCount = 2;
if (_onRoomUpdateCallback) _onRoomUpdateCallback(room);
}
}
}
void NetworkManager::tcpReceiveThread() {
// TCP接收线程(简化实现)
while (_tcpRunning) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void NetworkManager::createRoom(const RoomConfig& config, const std::string& roomName) {
if (!_isConnected) {
if (_onErrorCallback) _onErrorCallback("Not connected to server");
return;
}
std::string message = buildCreateRoomMessage(config, roomName);
sendWebSocketMessage(message);
}
void NetworkManager::joinRoom(const std::string& roomId, const std::string& password) {
if (!_isConnected) {
if (_onErrorCallback) _onErrorCallback("Not connected to server");
return;
}
std::string message = buildJoinRoomMessage(roomId, password);
sendWebSocketMessage(message);
}
void NetworkManager::leaveRoom() {
// 发送离开房间消息
ValueMap message;
message["type"] = Value("leave_room");
message["playerId"] = Value(_playerId);
message["timestamp"] = Value(static_cast<long long>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count()));
sendWebSocketMessage(StringUtils::format("%s", serializeValueMap(message).c_str()));
}
void NetworkManager::toggleReady(bool ready) {
// 发送准备状态切换消息
ValueMap message;
message["type"] = Value("toggle_ready");
message["playerId"] = Value(_playerId);
message["ready"] = Value(ready);
message["timestamp"] = Value(static_cast<long long>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count()));
sendWebSocketMessage(StringUtils::format("%s", serializeValueMap(message).c_str()));
}
std::string NetworkManager::buildCreateRoomMessage(const RoomConfig& config, const std::string& roomName) {
ValueMap message;
message["type"] = Value("create_room");
message["playerId"] = Value(_playerId);
message["roomName"] = Value(roomName);
ValueMap configMap;
configMap["maxPlayers"] = Value(config.maxPlayers);
configMap["maxSpectators"] = Value(config.maxSpectators);
configMap["allowSpectator"] = Value(config.allowSpectator);
configMap["isPrivate"] = Value(config.isPrivate);
configMap["password"] = Value(config.password);
configMap["gameMode"] = Value(config.gameMode);
configMap["mapId"] = Value(config.mapId);
configMap["timeLimit"] = Value(config.timeLimit);
message["config"] = Value(configMap);
message["timestamp"] = Value(static_cast<long long>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count()));
return serializeValueMap(message);
}
std::string NetworkManager::buildJoinRoomMessage(const std::string& roomId, const std::string& password) {
ValueMap message;
message["type"] = Value("join_room");
message["playerId"] = Value(_playerId);
message["roomId"] = Value(roomId);
message["password"] = Value(password);
message["timestamp"] = Value(static_cast<long long>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count()));
return serializeValueMap(message);
}
std::string NetworkManager::serializeValueMap(const ValueMap& dict) {
// 简化的序列化实现(实际项目建议使用Protobuf或JSON)
std::stringstream ss;
ss << "{";
bool first = true;
for (const auto& pair : dict) {
if (!first) ss << ",";
ss << "\"" << pair.first << "\":";
if (pair.second.getType() == Value::Type::STRING) {
ss << "\"" << pair.second.asString() << "\"";
} else if (pair.second.getType() == Value::Type::INTEGER) {
ss << pair.second.asInt();
} else if (pair.second.getType() == Value::Type::DOUBLE) {
ss << pair.second.asDouble();
} else if (pair.second.getType() == Value::Type::BOOLEAN) {
ss << (pair.second.asBool() ? "true" : "false");
}
first = false;
}
ss << "}";
return ss.str();
}
void NetworkManager::sendWebSocketMessage(const std::string& message) {
std::lock_guard<std::mutex> lock(_wsMutex);
_wsSendQueue.push(message);
// 实际项目中这里会调用lws_write发送消息
CCLOG("Sending WebSocket message: %s", message.c_str());
}
3. 房间管理UI界面
// RoomScene.h
#ifndef __ROOM_SCENE_H__
#define __ROOM_SCENE_H__
#include "cocos2d.h"
#include "NetworkManager.h"
#include "RoomModel.h"
USING_NS_CC;
class RoomScene : public Layer {
public:
static Scene* createScene();
virtual bool init();
CREATE_FUNC(RoomScene);
void updateRoomInfo(const RoomInfo& room);
void onEnterTransitionDidFinish() override;
private:
void createUI();
void createRoomListPanel();
void createRoomDetailPanel();
void createPlayerListPanel();
void createControlPanel();
void onBtnCreateRoomClicked(Ref* sender);
void onBtnRefreshRoomsClicked(Ref* sender);
void onBtnJoinRoomClicked(Ref* sender);
void onBtnLeaveRoomClicked(Ref* sender);
void onBtnStartGameClicked(Ref* sender);
void onBtnToggleReadyClicked(Ref* sender);
void onBtnInviteFriendClicked(Ref* sender);
void showCreateRoomDialog();
void showJoinRoomDialog();
void showRoomList();
void showRoomDetail();
void onNetworkConnected();
void onNetworkDisconnected(int errorCode);
void onRoomCreated(const RoomInfo& room);
void onRoomJoined(const RoomInfo& room);
void onRoomUpdated(const RoomInfo& room);
void onPlayerJoin(const PlayerInfo& player);
void onPlayerLeave(const std::string& playerId, const std::string& reason);
void onError(const std::string& error);
void updatePlayerList();
void updateRoomStatus();
bool isPlayerOwner() const;
bool canStartGame() const;
private:
NetworkManager* _networkMgr;
RoomInfo _currentRoom;
std::vector<RoomInfo> _roomList;
bool _isInRoom;
bool _isReady;
// UI组件
Node* _roomListPanel;
Node* _roomDetailPanel;
ListView* _roomListView;
Label* _roomNameLabel;
Label* _roomStatusLabel;
Label* _playerCountLabel;
ListView* _playerListView;
Button* _btnStartGame;
Button* _btnToggleReady;
TextField* _inputRoomId;
};
#endif // __ROOM_SCENE_H__
// RoomScene.cpp
#include "RoomScene.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
using namespace ui;
Scene* RoomScene::createScene() {
auto scene = Scene::create();
auto layer = RoomScene::create();
scene->addChild(layer);
return scene;
}
bool RoomScene::init() {
if (!Layer::init()) {
return false;
}
_networkMgr = NetworkManager::getInstance();
_isInRoom = false;
_isReady = false;
createUI();
// 注册网络回调
_networkMgr->setOnConnectCallback(CC_CALLBACK_0(RoomScene::onNetworkConnected, this));
_networkMgr->setOnDisconnectCallback(CC_CALLBACK_1(RoomScene::onNetworkDisconnected, this));
_networkMgr->setOnRoomCreatedCallback(CC_CALLBACK_1(RoomScene::onRoomCreated, this));
_networkMgr->setOnRoomJoinedCallback(CC_CALLBACK_1(RoomScene::onRoomJoined, this));
_networkMgr->setOnRoomUpdateCallback(CC_CALLBACK_1(RoomScene::onRoomUpdated, this));
_networkMgr->setOnPlayerJoinCallback(CC_CALLBACK_1(RoomScene::onPlayerJoin, this));
_networkMgr->setOnPlayerLeaveCallback(CC_CALLBACK_2(RoomScene::onPlayerLeave, this));
_networkMgr->setOnErrorCallback(CC_CALLBACK_1(RoomScene::onError, this));
return true;
}
void RoomScene::createUI() {
auto visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
// 背景
auto background = Sprite::create("background.png");
background->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
background->setScale(visibleSize.width / background->getContentSize().width,
visibleSize.height / background->getContentSize().height);
this->addChild(background);
// 标题
auto title = Label::createWithTTF("多人游戏房间", "fonts/Marker Felt.ttf", 36);
title->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height - 60 + origin.y));
title->setColor(Color3B::WHITE);
this->addChild(title);
createRoomListPanel();
createRoomDetailPanel();
createControlPanel();
// 初始显示房间列表
showRoomList();
}
void RoomScene::createRoomListPanel() {
auto visibleSize = Director::getInstance()->getVisibleSize();
_roomListPanel = Node::create();
_roomListPanel->setContentSize(Size(visibleSize.width * 0.9, visibleSize.height * 0.7));
_roomListPanel->setPosition(visibleSize.width * 0.05, visibleSize.height * 0.15);
_roomListPanel->setVisible(true);
this->addChild(_roomListPanel);
// 房间列表标题
auto listTitle = Label::createWithTTF("房间列表", "fonts/Marker Felt.ttf", 24);
listTitle->setPosition(Vec2(_roomListPanel->getContentSize().width/2,
_roomListPanel->getContentSize().height - 30));
listTitle->setColor(Color3B::YELLOW);
_roomListPanel->addChild(listTitle);
// 刷新按钮
auto btnRefresh = Button::create("button_normal.png", "button_pressed.png");
btnRefresh->setTitleText("刷新");
btnRefresh->setTitleFontSize(20);
btnRefresh->setPosition(Vec2(_roomListPanel->getContentSize().width - 80,
_roomListPanel->getContentSize().height - 30));
btnRefresh->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnRefreshRoomsClicked, this));
_roomListPanel->addChild(btnRefresh);
// 创建房间按钮
auto btnCreate = Button::create("button_normal.png", "button_pressed.png");
btnCreate->setTitleText("创建房间");
btnCreate->setTitleFontSize(20);
btnCreate->setPosition(Vec2(80, _roomListPanel->getContentSize().height - 30));
btnCreate->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnCreateRoomClicked, this));
_roomListPanel->addChild(btnCreate);
// 房间列表视图
_roomListView = ListView::create();
_roomListView->setContentSize(Size(_roomListPanel->getContentSize().width - 20,
_roomListPanel->getContentSize().height - 80));
_roomListView->setPosition(Vec2(10, 10));
_roomListView->setItemsMargin(5.0f);
_roomListPanel->addChild(_roomListView);
}
void RoomScene::createRoomDetailPanel() {
auto visibleSize = Director::getInstance()->getVisibleSize();
_roomDetailPanel = Node::create();
_roomDetailPanel->setContentSize(Size(visibleSize.width * 0.9, visibleSize.height * 0.7));
_roomDetailPanel->setPosition(visibleSize.width * 0.05, visibleSize.height * 0.15);
_roomDetailPanel->setVisible(false);
this->addChild(_roomDetailPanel);
// 房间名称
_roomNameLabel = Label::createWithTTF("房间名称", "fonts/Marker Felt.ttf", 22);
_roomNameLabel->setPosition(Vec2(_roomDetailPanel->getContentSize().width/2,
_roomDetailPanel->getContentSize().height - 40));
_roomNameLabel->setColor(Color3B::WHITE);
_roomDetailPanel->addChild(_roomNameLabel);
// 房间状态
_roomStatusLabel = Label::createWithTTF("状态: 等待中", "fonts/Marker Felt.ttf", 18);
_roomStatusLabel->setPosition(Vec2(150, _roomDetailPanel->getContentSize().height - 40));
_roomStatusLabel->setColor(Color3B::GREEN);
_roomDetailPanel->addChild(_roomStatusLabel);
// 玩家数量
_playerCountLabel = Label::createWithTTF("玩家: 1/4", "fonts/Marker Felt.ttf", 18);
_playerCountLabel->setPosition(Vec2(300, _roomDetailPanel->getContentSize().height - 40));
_playerCountLabel->setColor(Color3B::CYAN);
_roomDetailPanel->addChild(_playerCountLabel);
// 玩家列表标题
auto playerTitle = Label::createWithTTF("玩家列表", "fonts/Marker Felt.ttf", 20);
playerTitle->setPosition(Vec2(80, _roomDetailPanel->getContentSize().height - 80));
playerTitle->setColor(Color3B::YELLOW);
_roomDetailPanel->addChild(playerTitle);
// 玩家列表
_playerListView = ListView::create();
_playerListView->setContentSize(Size(_roomDetailPanel->getContentSize().width - 20,
_roomDetailPanel->getContentSize().height - 140));
_playerListView->setPosition(Vec2(10, 10));
_playerListView->setItemsMargin(3.0f);
_roomDetailPanel->addChild(_playerListView);
}
void RoomScene::createControlPanel() {
auto visibleSize = Director::getInstance()->getVisibleSize();
// 底部控制面板
auto controlPanel = Node::create();
controlPanel->setContentSize(Size(visibleSize.width, 80));
controlPanel->setPosition(0, 0);
this->addChild(controlPanel);
// 加入房间按钮(输入框+按钮)
_inputRoomId = TextField::create("输入房间ID", "fonts/Marker Felt.ttf", 20);
_inputRoomId->setPosition(Vec2(200, 40));
_inputRoomId->setPlaceHolder("输入房间ID");
_inputRoomId->setMaxLength(20);
controlPanel->addChild(_inputRoomId);
auto btnJoin = Button::create("button_normal.png", "button_pressed.png");
btnJoin->setTitleText("加入");
btnJoin->setTitleFontSize(18);
btnJoin->setPosition(Vec2(350, 40));
btnJoin->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnJoinRoomClicked, this));
controlPanel->addChild(btnJoin);
// 离开房间按钮
auto btnLeave = Button::create("button_normal.png", "button_pressed.png");
btnLeave->setTitleText("离开房间");
btnLeave->setTitleFontSize(18);
btnLeave->setPosition(Vec2(visibleSize.width - 150, 40));
btnLeave->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnLeaveRoomClicked, this));
btnLeave->setVisible(false);
controlPanel->addChild(btnLeave);
_btnLeaveRoom = btnLeave;
// 准备按钮
_btnToggleReady = Button::create("button_green.png", "button_red.png");
_btnToggleReady->setTitleText("准备");
_btnToggleReady->setTitleFontSize(18);
_btnToggleReady->setPosition(Vec2(visibleSize.width - 300, 40));
_btnToggleReady->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnToggleReadyClicked, this));
_btnToggleReady->setVisible(false);
controlPanel->addChild(_btnToggleReady);
// 开始游戏按钮
_btnStartGame = Button::create("button_yellow.png", "button_yellow_pressed.png");
_btnStartGame->setTitleText("开始游戏");
_btnStartGame->setTitleFontSize(18);
_btnStartGame->setPosition(Vec2(visibleSize.width - 450, 40));
_btnStartGame->addClickEventListener(CC_CALLBACK_1(RoomScene::onBtnStartGameClicked, this));
_btnStartGame->setVisible(false);
controlPanel->addChild(_btnStartGame);
}
void RoomScene::onBtnCreateRoomClicked(Ref* sender) {
showCreateRoomDialog();
}
void RoomScene::onBtnJoinRoomClicked(Ref* sender) {
std::string roomId = _inputRoomId->getString();
if (roomId.empty()) {
MessageBox("请输入房间ID", "提示");
return;
}
_networkMgr->joinRoom(roomId);
}
void RoomScene::onBtnLeaveRoomClicked(Ref* sender) {
_networkMgr->leaveRoom();
}
void RoomScene::onBtnStartGameClicked(Ref* sender) {
_networkMgr->startGame();
}
void RoomScene::onBtnToggleReadyClicked(Ref* sender) {
_isReady = !_isReady;
_networkMgr->toggleReady(_isReady);
_btnToggleReady->setTitleText(_isReady ? "取消准备" : "准备");
}
void RoomScene::showRoomList() {
_roomListPanel->setVisible(true);
_roomDetailPanel->setVisible(false);
_btnLeaveRoom->setVisible(false);
_btnToggleReady->setVisible(false);
_btnStartGame->setVisible(false);
_isInRoom = false;
}
void RoomScene::showRoomDetail() {
_roomListPanel->setVisible(false);
_roomDetailPanel->setVisible(true);
_btnLeaveRoom->setVisible(true);
_btnToggleReady->setVisible(true);
_btnStartGame->setVisible(isPlayerOwner());
_isInRoom = true;
}
void RoomScene::updateRoomInfo(const RoomInfo& room) {
_currentRoom = room;
_roomNameLabel->setString(room.roomName);
std::string statusText = "状态: ";
switch (room.status) {
case RoomStatus::WAITING: statusText += "等待中"; break;
case RoomStatus::STARTING: statusText += "开始中"; break;
case RoomStatus::PLAYING: statusText += "游戏中"; break;
case RoomStatus::ENDED: statusText += "已结束"; break;
}
_roomStatusLabel->setString(statusText);
std::string countText = StringUtils::format("玩家: %d/%d",
room.currentPlayerCount, room.maxPlayers);
_playerCountLabel->setString(countText);
updatePlayerList();
updateRoomStatus();
}
void RoomScene::updatePlayerList() {
_playerListView->removeAllItems();
for (const auto& player : _currentRoom.players) {
std::string roleText = "";
switch (player.role) {
case PlayerRole::OWNER: roleText = "[房主]"; break;
case PlayerRole::MEMBER: roleText = "[成员]"; break;
case PlayerRole::SPECTATOR: roleText = "[观战]"; break;
}
std::string readyText = player.isReady ? "[准备]" : "[未准备]";
std::string playerInfo = StringUtils::format("%s%s%s - %s",
roleText.c_str(), readyText.c_str(),
player.nickname.c_str(), player.devicePlatform.c_str());
auto item = Label::createWithTTF(playerInfo, "fonts/Marker Felt.ttf", 16);
item->setColor(player.isReady ? Color3B::GREEN : Color3B::WHITE);
auto layout = Layout::create();
layout->setContentSize(Size(_playerListView->getContentSize().width - 20, 25));
layout->addChild(item);
item->setPosition(layout->getContentSize().width/2, layout->getContentSize().height/2);
_playerListView->pushBackCustomItem(layout);
}
}
void RoomScene::updateRoomStatus() {
bool canStart = canStartGame();
_btnStartGame->setEnabled(canStart);
_btnStartGame->setOpacity(canStart ? 255 : 128);
}
bool RoomScene::isPlayerOwner() const {
return !_currentRoom.players.empty() &&
_currentRoom.players[0].playerId == _networkMgr->_playerId;
}
bool RoomScene::canStartGame() const {
if (!isPlayerOwner()) return false;
if (_currentRoom.status != RoomStatus::WAITING) return false;
// 检查所有玩家是否准备(除了房主)
for (const auto& player : _currentRoom.players) {
if (player.playerId != _networkMgr->_playerId && !player.isReady) {
return false;
}
}
return true;
}
// 网络回调方法实现
void RoomScene::onNetworkConnected() {
MessageBox("连接到服务器成功", "提示");
}
void RoomScene::onNetworkDisconnected(int errorCode) {
MessageBox(StringUtils::format("连接断开: %d", errorCode).c_str(), "错误");
showRoomList();
}
void RoomScene::onRoomCreated(const RoomInfo& room) {
_currentRoom = room;
updateRoomInfo(room);
showRoomDetail();
MessageBox("房间创建成功", "成功");
}
void RoomScene::onRoomJoined(const RoomInfo& room) {
_currentRoom = room;
updateRoomInfo(room);
showRoomDetail();
MessageBox("加入房间成功", "成功");
}
void RoomScene::onRoomUpdated(const RoomInfo& room) {
_currentRoom = room;
updateRoomInfo(room);
}
void RoomScene::onPlayerJoin(const PlayerInfo& player) {
MessageBox(StringUtils::format("%s 加入了房间", player.nickname.c_str()).c_str(), "提示");
// 房间更新会通过onRoomUpdated触发,这里只需提示
}
void RoomScene::onPlayerLeave(const std::string& playerId, const std::string& reason) {
MessageBox(StringUtils::format("有玩家离开了房间: %s", reason.c_str()).c_str(), "提示");
// 房间更新会通过onRoomUpdated触发
}
void RoomScene::onError(const std::string& error) {
MessageBox(error.c_str(), "错误");
}
原理解释
1. 房间创建流程
-
客户端请求:玩家点击"创建房间",客户端收集房间配置(最大玩家数、游戏模式等),通过WebSocket发送 create_room消息到服务器。 -
服务器处理:服务器验证玩家权限,生成唯一房间ID,创建房间记录,将创建者设为房主。 -
状态同步:服务器广播 room_created消息给所有相关客户端,更新房间列表和详情界面。 -
UI更新:客户端接收消息后,通过 onRoomCreated回调更新UI,切换到房间详情面板。
2. 玩家加入流程
-
房间查找:玩家输入房间ID或从房间列表选择,客户端发送 join_room消息到服务器。 -
验证与加入:服务器验证房间是否存在、是否满员、密码是否正确,然后将玩家加入房间成员列表。 -
状态广播:服务器向房间内所有玩家广播 player_joined消息,包含新玩家信息。 -
UI同步:客户端更新玩家列表,显示新加入的玩家,更新玩家计数。
3. 玩家退出流程
-
退出请求:玩家点击"离开房间",客户端发送 leave_room消息到服务器。 -
权限检查:服务器检查是否为房主退出,若是则转移房主权限给第一个成员或解散房间。 -
状态更新:服务器从房间成员列表中移除该玩家,广播 player_left消息。 -
UI调整:客户端更新玩家列表,若房间为空则自动返回房间列表界面。
4. 实时状态同步机制
-
WebSocket推送:服务器主动向客户端推送房间状态变化(玩家加入/退出、准备状态改变等)。 -
轮询兜底:对于不支持WebSocket的环境,客户端定期轮询房间状态(间隔2-5秒)。 -
增量更新:只传输变化的数据字段,减少网络带宽占用(如仅发送变更的玩家状态)。
核心特性
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
原理流程图
1. 房间创建与加入流程图
客户端 → 创建房间请求 → 服务器 → 生成房间ID → 保存房间信息 → 广播房间创建 → 客户端更新UI
客户端 → 加入房间请求 → 服务器 → 验证房间状态 → 添加玩家 → 广播玩家加入 → 所有客户端更新玩家列表
2. 玩家退出与房间销毁流程图
玩家退出 → 客户端发送离开请求 → 服务器检查是否房主 →
├─ 是房主: 检查剩余玩家 → 转移房主/解散房间 → 广播房间销毁
└─ 否房主: 移除玩家 → 广播玩家离开 → 客户端更新列表
3. 网络异常处理流程图
网络断开 → 客户端检测连接状态 → 启动重连机制 →
├─ 重连成功: 同步最新房间状态 → 恢复正常操作
└─ 重连失败: 本地缓存操作 → 提示用户检查网络
环境准备
1. 开发环境
-
引擎版本:Cocos2d-x 3.8+ -
开发工具:Visual Studio 2019+ (Windows)、Xcode 12+ (macOS)、Android Studio 4.0+ -
编程语言:C++11+ -
依赖库: -
libwebsockets(WebSocket支持) -
pthread(多线程支持) -
curl(HTTP客户端,可选)
-
2. 项目配置
# CMakeLists.txt 关键配置
find_package(Libwebsockets REQUIRED)
include_directories(${LIBWEBSOCKETS_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME}
cocos2d
${LIBWEBSOCKETS_LIBRARIES}
pthread
)
3. 权限配置
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
运行结果
-
房间创建:点击"创建房间"后,客户端发送创建请求,服务器返回房间ID,界面切换到房间详情页,显示房主身份和空玩家列表。 -
玩家加入:输入有效房间ID点击"加入",服务器验证通过后,玩家列表显示新成员,人数计数器更新(如"2/4")。 -
准备状态:点击"准备"按钮,按钮文字变为"取消准备",玩家列表中显示"[准备]"标记,房主看到"开始游戏"按钮可用。 -
房主权限:房主点击"开始游戏",服务器验证所有玩家准备状态后,广播游戏开始消息,所有客户端进入游戏场景。 -
网络异常:断开网络连接,客户端检测到连接断开,显示错误提示,自动尝试重连,重连成功后同步最新房间状态。
测试步骤
1. 功能测试
-
房间创建:验证房间名称、配置参数正确传递到服务器,房间ID唯一性。 -
加入房间:测试有效/无效房间ID的加入流程,验证满员房间的拒绝加入。 -
玩家管理:测试玩家准备/取消准备的状态同步,房主踢人功能。 -
退出机制:验证普通玩家退出、房主退出的不同处理逻辑。
2. 性能测试
-
并发测试:模拟10个客户端同时创建房间,验证服务器处理能力。 -
延迟测试:测量房间状态变更从发送到接收的延迟(目标<200ms)。 -
内存测试:长时间运行(2小时)监控内存占用,确保无泄漏。
3. 兼容性测试
-
跨平台:在iOS、Android、Windows平台验证房间管理功能一致性。 -
网络环境:测试WiFi、4G、弱网环境下的房间同步稳定性。 -
设备适配:验证不同屏幕尺寸下的UI布局和触控响应。
部署场景
1. 手游开黑应用
-
目标平台:iOS App Store、Google Play、华为应用市场 -
部署架构:云服务器集群(阿里云/腾讯云)+ CDN加速房间列表查询 -
容量规划:单服务器支持1000并发房间,支持水平扩展
2. PC网络游戏大厅
-
目标平台:Steam、Epic Games Store、官方网站 -
部署架构:专用游戏服务器 + 负载均衡器 + Redis缓存房间状态 -
特色功能:支持键盘快捷键操作,大屏幕优化的房间管理界面
3. 企业团建游戏
-
定制化部署:为企业客户提供私有化部署,支持内网环境 -
功能定制:添加企业Logo、部门分组、领导特殊权限等功能 -
数据安全:房间数据加密存储,符合企业信息安全规范
疑难解答
1. 房间状态不同步
ValueMap的深拷贝避免引用问题。2. 频繁断线重连
3. 房主权限异常
4. 跨平台兼容性问题
NSAppTransportSecurity允许HTTP连接,或使用WSS(WebSocket Secure)。未来展望
1. AI智能匹配
-
技能匹配:基于玩家历史战绩、游戏时长等数据,使用机器学习算法匹配实力相当的对手。 -
社交匹配:分析玩家的社交图谱,推荐好友或兴趣相投的玩家组成房间。
2. 云游戏集成
-
串流游戏:房间管理集成云游戏技术,玩家无需下载即可在浏览器中参与游戏。 -
边缘计算:利用边缘节点就近部署游戏服务器,进一步降低网络延迟。
3. 元宇宙融合
-
虚拟房间:将2D房间界面升级为3D虚拟空间,玩家以Avatar形象互动。 -
跨游戏互通:一个房间支持多种游戏模式切换,玩家可在象棋、麻将、扑克间无缝切换。
技术趋势与挑战
趋势
-
WebAssembly集成:使用WASM提升Cocos2d-x在Web平台的性能,实现真正的跨平台统一体验。 -
5G赋能:利用5G网络的超低延迟特性,实现接近本地的多人游戏体验。 -
区块链确权:使用区块链技术记录玩家的房间创建、胜利等成就,提供不可篡改的游戏履历。
挑战
-
反作弊难度:客户端逻辑难以完全隐藏,需要服务器权威验证所有关键操作。 -
全球化部署:不同地区的网络延迟差异巨大,需要智能路由和就近接入策略。 -
隐私合规:用户数据收集需要符合GDPR、CCPA等法规,房间聊天内容需要内容审核。
总结
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)