共计 8491 个字符,预计需要花费 22 分钟才能阅读完成。
这篇文章主要讲解通过 Swoole
和 webSocket
来实现斗地主的用户进入房间功能,这也是实现斗地主的第一步。实现整个斗地主分为三篇文章来讲解这是第一篇。
在技术栈方面主要使用php
, swoole, html5:webSocket
, pixi.js, 数据存储采用redis
。主要实现效果如下图:
开始
在开始之前我们默认已有 php
开始环境。我们只需要安装 swoole
扩展,redis
扩展 和redis
包。
- 安装
swoole
,redis
. - 下载
pixijs
库,当然你可以使用远程的. (一个绘制 2D 图形的引擎) composer.json
编写, 之后执行安装命令即可composer install
.
{
"require": {"meshell/surf": "^1.0.5" // 对 swoole 一个封装框架(https://github.com/TianLiangZhou/surf)
},
"autoload": {
"psr-4": {"Landowner\\": "./app" // 后台服务的文件目录}
}
}
- 建立入口 (
index.html
) 文件,资源目录,服务目录和文件。如图:
功能实现
我们需要实现 websocket
和后台服务的对接。制定协议格式,比如协议名,内容,返回值。
进入房间
一个斗地主流程比较完善应该是:
用户登录 -> 选择房间 -> 进入房间 -> 准备开始 -> 开始.
我们在这里的实现直接从进入房间开始。用户直接打开网页就是进入房间。
- 功能分析
当前用户进入房间之前,我们需要在页面显示当前房间有多少人等待,进入成功之后我们需要告知其它用户有人进来了。从上面得知我们需要一个获取当前房间有多少人的协议,
这些人的状态是什么(准备中
或未准备
), 还需要一个进入房间的协议,该协议要实现广播告知其它用户,而客户端也需要监听进入房间的广播消息。
- 服务端
我们先从服务端代码开始编写. 根据上面的分析我需要实现两个协议. surf
也是一个 MVC
形式的框架. 我们只需要实现自己的业务控制器就行了.
在这里我们使用框架自带的 json
数据传输类型来解析.
- 入口文件
landowner.php
php
require __DIR__ . '/vendor/autoload.php';
$config = [];
$config['setting'] = ['document_root' => __DIR__,
'task_worker_num' => 1,
];
$config['server'] = 'webSocket';
$app = new \Surf\Application(__DIR__, ['app.config' => $config
]);
$app->register(new \Surf\Provider\RedisServiceProvider());
include __DIR__ . '/protocol.php'; // 协议路由文件
try {$app->run();} catch (\Surf\Exception\ServerNotFoundException $e) {}
- 协议路由
protocol.php
<?php
use Landowner\Protocol\LandownerController;
$app->addProtocol( // 进入房间协议
\'enter.room\', // 协议名称
LandownerController::class . \':enterRoom\'
);
$app->addProtocol( // 获取当前房间人数列表协议
\'room.player\',
LandownerController::class . \':roomPlayer\'
);
- 控制器的实现
LandownerController.php
<?php
namespace Landowner\Protocol;
use Pimple\Psr11\Container;
use Surf\Mvc\Controller\WebSocketController;
use Surf\Server\RedisConstant;
use Surf\Task\PushTaskHandle;
class LandownerController extends WebSocketController
{
const READY_KEY = \'ready:action\';
/**
* @var null | \Redis
*/
protected $redis = null;
/**
* LandownerController constructor.
* @param Container $container
* @param int $workerId
*/
public function __construct(Container $container, $workerId = 0)
{parent::__construct($container, $workerId);
$this->redis = $this->container->get(\'redis\');
}
/**
* @param $body
* @return array
*/
public function enterRoom($body)
{
// 框架自带类常量,获取当前的总数
$count = $this->redis->sCard(RedisConstant::FULL_CONNECT_FD);
$flag = 0;
$players = [];
if ($count> 3) { // 限制 3
$flag = 500;
$this->setIsClose(true); // 主动断开
} else {$allPlayer = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);
print_r($allPlayer);
$otherPlayer = array_diff($allPlayer, [$this->frame->fd]);
if ($otherPlayer) { // 找出其它用户
foreach ($otherPlayer as $fd) {$readyState = $this->redis->hGet(self::READY_KEY, $fd);
$players[] = [\'ready\' => $readyState, // 获取其它用户的状态
\'playerId\' => $fd, // 用户 id
];
}
$this->task(["from" => $otherPlayer,
"content" => json_encode(["listen" => "enterRoom", // 客户监听的协议名称
"content" => $this->frame->fd,
])
], PushTaskHandle::class); // 通过任务给其它用户发送消息
}
}
return ["flag" => $flag,
"player" => $this->frame->fd,
"otherPlayer" => $players,
"requestId" => $body->requestId,
];
}
/**
* 获取正在房间的用户,以及状态,客户端要据状态显示不同的文字
*
* @param $body
* @return array
*/
public function roomPlayer($body)
{$count = $this->redis->sCard(RedisConstant::FULL_CONNECT_FD);
$player = $this->redis->sMembers(RedisConstant::FULL_CONNECT_FD);
$players = [];
foreach ($player as $fd) {if ($fd == $this->frame->fd) {continue;}
$readyState = $this->redis->hGet(self::READY_KEY, $fd);
$players[] =[\'ready\' => $readyState,
\'playerId\'=> $fd,
];
}
return ["flag" => 0,
"count" => $count,
"player" => $players,
"requestId" => $body->requestId,
];
}
...
}
- 客户端
在客户端我们这里使用原生的 webSocket
进行通信,pixijs
引擎进行渲染界面. 在这里封装了一个简单的 websocket
使用类. 整个 socket
类代码.
function Socket() {
var isConnection = false, _this = this;
var connection = function() {
var host = location.host;
if (host !== "example.loocode.com") {host = "192.168.56.101";}
return new WebSocket("ws://" host ":9527");
};
this.reconnect = function() {this.socket = connection();
this.socket.onopen = function () {
isConnection = true;
if (_this.openCallback) {_this.openCallback.call(_this);
}
};
this.socket.onmessage = function(event) {_this.response(event); // 监听消息
};
this.socket.onclose = function() {isConnection = false;};
this.socket.onerror = function(e) {alert("connection websocket error!");
};
};
this.response = function(event) {
var response = null;
try {response = JSON.parse(event.data);
} catch (e) {console.log(e);
}
console.log(response);
// 协议返回
if (typeof response.body === "object" && response.body !== undefined) {
var requestId = response.body.requestId;
var callback = this.getRequestCallback(requestId);
if (callback !== undefined) {callback.call(_this, response.body);
}
}
// 服务主动发送的监听
if (typeof response.listen === "string" && response.listen !== undefined) {if (_this.listeners.hasOwnProperty(response.listen)) {_this.listeners[response.listen].call(_this, response.content);
}
}
};
this.isConnected = function() {return isConnection;};
this.requestCallback = {};
this.listeners = {};
this.openCallback = null;
}
Socket.prototype = {addRequestCallback: function (id, callback) {this.requestCallback[id] = callback;
},
getRequestCallback: function(id) {if (this.requestCallback.hasOwnProperty(id)) {return this.requestCallback[id];
}
return undefined;
},
listen: function (name, callback) {this.listeners[name] = callback;
},
onOpenCallback: function(callback) {if (callback !== undefined && callback !== null) {this.openCallback = callback;}
},
send: function(protocol, body, callback) {var requestId = new Date().getTime();
if (body === undefined || body === null) {body = {requestId: requestId};
} else {body.requestId = requestId;}
var requestBody = {
"protocol": protocol,
"body": body
};
if (!this.isConnected()) {this.reconnect();
}
if (callback !== undefined) {this.addRequestCallback(requestId, callback);
}
this.socket.send(JSON.stringify(requestBody));
}
};
实现的客户业务代码
function Landowner() {
this.app = new PIXI.Application( // 初始化容器
window.innerWidth,
window.innerHeight,
{backgroundColor: 0x1099bb, // 设置容器颜色}
);
this.socket = new Socket();
this.playerCount = 0; // 用户总数
this.player = 0; // 用户 id
this.playerReadyButton = {};
this.playId = 0; // 当前牌局 id
}
Landowner.prototype = {start: function () {document.body.appendChild(this.app.view);
this.listen(); // 初始化监听},
initRender: function (landowner) {
/**
* @var landowner Landowner
*/
if (this.socket.isConnected()) {this.socket.send(\'room.player\', {}, function(body) {if (body.count> 3) {return ;}
landowner.playerCount = body.count;
for (var i = 0; i < body.player.length; i) {landowner.readyWorker(i 2, !!body.player[i].ready, body.player[i].playerId);
}
this.send("enter.room", {}, function (body) {if (body.flag === 0) {
landowner.player = body.player;
landowner.readyWorker(1, false, body.player);
}
});
});
}
},
listen: function() {
var _this = this;
this.socket.listen("enterRoom", function(content) {
_this.playerCount ;
_this.readyWorker(_this.playerCount, false, content);
});
this.socket.listen("readyStatus", function(content) {
/**
* @var button Graphics
*/
var button = _this.playerReadyButton[content.playerId];
var text = button.getChildByName(\'text\');
text.text = content.ready ? "准备中" : "准备";
});
this.socket.listen("assignPoker", function(content) {
_this.playId = content.playId;
_this.renderBottomCard(content.landowner);
_this.assignPoker(content.poker);
for (var key in _this.playerReadyButton) {_this.playerReadyButton[key].destroy();}
});
this.socket.onOpenCallback(function() {_this.initRender(_this);
});
this.socket.reconnect();},
readyWorker: function (offset, state, playId) {
var _this = this;
var button = new PIXI.Graphics()
.beginFill(0x2fb44a)
.drawRoundedRect(0, 0, 120, 60, 10)
.endFill();
var text = "准备";
if (state === true) {text = "准备中";}
var readyText = new PIXI.Text(text, new PIXI.TextStyle({
fontFamily: "Arial",
fontSize: 32,
fill: "white",
}));
readyText.x = 60 - 32;
readyText.y = 30 - 16;
readyText.name = "text";
button.addChild(readyText);
button.interactive = true;
button.buttonMode = true;
var clickCounter = 1;
if (offset === 1) {button.on(\'pointertap\', function () {
var ready = 0;
if (clickCounter % 2) {
readyText.text = "取消";
ready = 1;
} else {readyText.text = "准备";}
clickCounter ;
_this.socket.send(\'player.ready\', {\'ready\': ready}, function (body) {console.log(body);
});
});
}
var x = y = 0;
y = window.innerHeight / 2;
x = window.innerWidth / 2;
if (offset === 2) {x = x (x / 2);
y = y - (y / 2);
} else if (offset === 3) {x = x - (x / 2) - 60;
y = y - (y / 2);
} else {
x = x - 60;
y = y (y / 2);
}
button.x = x;
button.y = y;
this.playerReadyButton[playId] = button;
this.app.stage.addChild(button);
},
}
入口文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Landowner</title>
<script src="js/pixi.js"></script>
</head>
<body>
<script type="text/javascript" src="js/landowner.js"></script>
<script type="text/javascript">
new Landowner().start();
</script>
</body>
</html>
以上服务端和客户端代码就完成了用户进入房间功能。下一期我们讲下准备,发牌的实现.
源码地址
https://github.com/TianLiangZhou/loocode-example/tree/master/landowner
效果地址
https://example.loocode.com/landowner/index.html
推荐阅读
- https://wiki.swoole.com/
- https://github.com/TianLiangZhou/surf
- https://pixijs.io/examples/
- https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API