使用 React、PHP 和 WebSockets 程序生成的游戏地形
上次,我开始告诉你我想如何制作游戏的故事。 我描述了如何设置异步 PHP 服务器、Laravel Mix 构建链、React 前端以及将所有这些连接在一起的 WebSockets。 现在,让我告诉你当我开始使用 React、PHP 和 WebSockets 的组合构建游戏机制时发生了什么……
这部分的代码可以在 github.com/assertchris-tutorials/sitepoint-making-games/tree/part-2 找到。 我已经用 PHP 测试过了 7.1
,在最新版本的谷歌浏览器中。
制作农场
“让我们从简单的开始。 我们有一个 10 x 10 的瓷砖网格,里面装满了随机生成的东西。”
我决定将农场表示为 Farm
每个瓦片作为 Patch
. 从 app/Model/FarmModel.pre
:
namespace AppModel;
class Farm
{
private $width
{
get { return $this->width; }
}
private $height
{
get { return $this->height; }
}
public function __construct(int $width = 10,
int $height = 10)
{
$this->width = $width;
$this->height = $height;
}
}
我认为通过使用公共 getter 声明私有属性来尝试类访问器宏将是一个有趣的时刻。 为此,我必须安装 pre/class-accessors
(通过 composer require
).
然后我更改了套接字代码以允许根据请求创建新的场。 从 app/Socket/GameSocket.pre
:
namespace AppSocket;
use AerysRequest;
use AerysResponse;
use AerysWebsocket;
use AerysWebsocketEndpoint;
use AerysWebsocketMessage;
use AppModelFarmModel;
class GameSocket implements Websocket
{
private $farms = [];
public function onData(int $clientId,
Message $message)
{
$body = yield $message;
if ($body === "new-farm") {
$farm = new FarmModel();
$payload = json_encode([
"farm" => [
"width" => $farm->width,
"height" => $farm->height,
],
]);
yield $this->endpoint->send(
$payload, $clientId
);
$this->farms[$clientId] = $farm;
}
}
public function onClose(int $clientId,
int $code, string $reason)
{
unset($this->connections[$clientId]);
unset($this->farms[$clientId]);
}
// …
}
我注意到这有多相似 GameSocket
是我之前的一个——除了,我没有广播回声,而是检查 new-farm
并只向提出请求的客户发回消息。
“也许现在是减少 React 代码通用性的好时机。 我要重命名 component.jsx
到 farm.jsx
”
从 assets/js/farm.jsx
:
import React from "react"
class Farm extends React.Component
{
componentWillMount()
{
this.socket = new WebSocket(
"ws://127.0.0.1:8080/ws"
)
this.socket.addEventListener(
"message", this.onMessage
)
// DEBUG
this.socket.addEventListener("open", () => {
this.socket.send("new-farm")
})
}
}
export default Farm
事实上,我唯一改变的是发送 new-farm
代替 hello world
. 其他一切都一样。 我确实必须改变 app.jsx
虽然代码。 从 assets/js/app.jsx
:
import React from "react"
import ReactDOM from "react-dom"
import Farm from "./farm"
ReactDOM.render(
<Farm />,
document.querySelector(".app")
)
它离我需要的地方还很远,但是使用这些更改我可以看到正在运行的类访问器,以及原型一种用于未来 WebSocket 交互的请求/响应模式。 我打开控制台,看到 {"farm":{"width":10,"height":10}}
.
“伟大的!”
然后我创建了一个 Patch
类来表示每个图块。 我认为这是很多游戏逻辑会发生的地方。 从 app/Model/PatchModel.pre
:
namespace AppModel;
class PatchModel
{
private $x
{
get { return $this->x; }
}
private $y
{
get { return $this->y; }
}
public function __construct(int $x, int $y)
{
$this->x = $x;
$this->y = $y;
}
}
我需要创建与新文件中的空格一样多的补丁 Farm
. 我可以这样做 FarmModel
建造。 从 app/Model/FarmModel.pre
:
namespace AppModel;
class FarmModel
{
private $width
{
get { return $this->width; }
}
private $height
{
get { return $this->height; }
}
private $patches
{
get { return $this->patches; }
}
public function __construct($width = 10, $height = 10)
{
$this->width = $width;
$this->height = $height;
$this->createPatches();
}
private function createPatches()
{
for ($i = 0; $i < $this->width; $i++) {
$this->patches[$i] = [];
for ($j = 0; $j < $this->height; $j++) {
$this->patches[$i][$j] =
new PatchModel($i, $j);
}
}
}
}
对于每个单元格,我创建了一个新的 PatchModel
目的。 这些开始时非常简单,但它们需要随机性元素——一种种植树木、杂草、花卉的方法……至少在开始时是这样。 从 app/Model/PatchModel.pre
:
public function start(int $width, int $height,
array $patches)
{
if (!$this->started && random_int(0, 10) > 7) {
$this->started = true;
return true;
}
return false;
}
我以为我会从随机种植一个补丁开始。 这并没有改变补丁的外部状态,但它确实为我提供了一种方法来测试它们是如何由农场启动的。 从 app/Model/FarmModel.pre
:
namespace AppModel;
use Amp;
use AmpCoroutine;
use Closure;
class FarmModel
{
private $onGrowth
{
get { return $this->onGrowth; }
}
private $patches
{
get { return $this->patches; }
}
public function __construct(int $width = 10,
int $height = 10, Closure $onGrowth)
{
$this->width = $width;
$this->height = $height;
$this->onGrowth = $onGrowth;
}
public async function createPatches()
{
$patches = [];
for ($i = 0; $i < $this->width; $i++) {
$this->patches[$i] = [];
for ($j = 0; $j < $this->height; $j++) {
$this->patches[$i][$j] = $patches[] =
new PatchModel($i, $j);
}
}
foreach ($patches as $patch) {
$growth = $patch->start(
$this->width,
$this->height,
$this->patches
);
if ($growth) {
$closure = $this->onGrowth;
$result = $closure($patch);
if ($result instanceof Coroutine) {
yield $result;
}
}
}
}
// …
}
这里发生了很多事情。 对于初学者,我介绍了一个 async
使用宏的函数关键字。 你看,Amp 处理 yield
通过解析 Promises 关键字。 更重要的是:当 Amp 看到 yield
关键字,它假定产生的是协程(在大多数情况下)。
我本可以让 createPatches
运行一个普通函数,并从中返回一个协程,但那是一段很常见的代码,我还不如为它创建一个特殊的宏。 同时,我可以替换我在前一部分中编写的代码。 从 helpers.pre
:
async function mix($path) {
$manifest = yield AmpFileget(
.."/public/mix-manifest.json"
);
$manifest = json_decode($manifest, true);
if (isset($manifest[$path])) {
return $manifest[$path];
}
throw new Exception("https://www.sitepoint.com/procedurally-generated-game-terrain-reactjs-php-websockets/{$path} not found");
}
以前,我必须制作一个生成器,然后将其包装在一个新的 Coroutine
:
use AmpCoroutine;
function mix($path) {
$generator = () => {
$manifest = yield AmpFileget(
.."/public/mix-manifest.json"
);
$manifest = json_decode($manifest, true);
if (isset($manifest[$path])) {
return $manifest[$path];
}
throw new Exception("https://www.sitepoint.com/procedurally-generated-game-terrain-reactjs-php-websockets/{$path} not found");
};
return new Coroutine($generator());
}
我开始了 createPatches
方法和以前一样,创建新的 PatchModel
每个对象 x
和 y
在网格中。 然后我开始了另一个循环,调用 start
每个补丁上的方法。 我会在同一步骤中完成这些,但我想要我的 start
能够检查周围补丁的方法。 这意味着我必须先创建所有这些,然后才能找出彼此周围的补丁。
我也变了 FarmModel
接受一个 onGrowth
关闭。 我的想法是,如果补丁增长(即使在引导阶段),我可以调用该关闭。
每次补丁增长时,我都会重置 $changes
多变的。 这确保了补丁将继续增长,直到农场的整个通道没有产生任何变化。 我还调用了 onGrowth
关闭。 我想允许 onGrowth
成为一个正常的闭包,甚至返回一个 Coroutine
. 这就是为什么我需要做 createPatches
一个 async
功能。
注意:诚然,允许 onGrowth
协程让事情变得有点复杂,但我认为它对于在补丁增长时允许其他异步操作至关重要。 也许以后我想发送一个套接字消息,我只能这样做如果 yield
在里面工作 onGrowth
. 我只能屈服 onGrowth
如果 createPatches
曾是一个 async
功能。 而且因为 createPatches
曾是一个 async
功能,我需要在里面产生它 GameSocket
.
“在制作第一个异步 PHP 应用程序时,很容易对所有需要学习的东西感到厌烦。 不要太早放弃!”
我需要编写的最后一段代码来检查这一切是否正常工作 GameSocket
. 从 app/Socket/GameSocket.pre
:
if ($body === "new-farm") {
$patches = [];
$farm = new FarmModel(10, 10,
function (PatchModel $patch) use (&$patches) {
array_push($patches, [
"x" => $patch->x,
"y" => $patch->y,
]);
}
);
yield $farm->createPatches();
$payload = json_encode([
"farm" => [
"width" => $farm->width,
"height" => $farm->height,
],
"patches" => $patches,
]);
yield $this->endpoint->send(
$payload, $clientId
);
$this->farms[$clientId] = $farm;
}
这只比我以前的代码稍微复杂一点。 我需要向 FarmModel
构造函数,并产生 $farm->createPatches()
这样每个人都有机会随机化。 之后,我只需要将补丁的快照传递给套接字负载。
每个农场的随机补丁
“如果我开始每个补丁都是干土怎么办? 然后我可以让一些补丁有杂草,其他补丁有树……”
我着手定制补丁。 从 app/Model/PatchModel.pre
:
private $started = false;
private $wet {
get { return $this->wet ?: false; }
};
private $type {
get { return $this->type ?: "dirt"; }
};
public function start(int $width, int $height,
array $patches)
{
if ($this->started) {
return false;
}
if (random_int(0, 100) < 90) {
return false;
}
$this->started = true;
$this->type = "weed";
return true;
}
我稍微改变了逻辑顺序,如果补丁已经开始就提前退出。 我也减少了成长的机会。 如果这些早期退出都没有发生,补丁类型将更改为杂草。
然后我可以将此类型用作套接字消息负载的一部分。 从 app/Socket/GameSocket.pre
:
$farm = new FarmModel(10, 10,
function (PatchModel $patch) use (&$patches) {
array_push($patches, [
"x" => $patch->x,
"y" => $patch->y,
"wet" => $patch->wet,
"type" => $patch->type,
]);
}
);
渲染农场
是时候展示农场了,使用我之前设置的 React 工作流。 我已经得到了 width
和 height
农场,这样我就可以让每个方块都变干(除非它应该长出杂草)。 从 assets/js/app.jsx
:
import React from "react"
class Farm extends React.Component
{
constructor()
{
super()
this.onMessage = this.onMessage.bind(this)
this.state = {
"farm": {
"width": 0,
"height": 0,
},
"patches": [],
};
}
componentWillMount()
{
this.socket = new WebSocket(
"ws://127.0.0.1:8080/ws"
)
this.socket.addEventListener(
"message", this.onMessage
)
// DEBUG
this.socket.addEventListener("open", () => {
this.socket.send("new-farm")
})
}
onMessage(e)
{
let data = JSON.parse(e.data);
if (data.farm) {
this.setState({"farm": data.farm})
}
if (data.patches) {
this.setState({"patches": data.patches})
}
}
componentWillUnmount()
{
this.socket.removeEventListener(this.onMessage)
this.socket = null
}
render() {
let rows = []
let farm = this.state.farm
let statePatches = this.state.patches
for (let y = 0; y < farm.height; y++) {
let patches = []
for (let x = 0; x < farm.width; x++) {
let className = "patch"
statePatches.forEach((patch) => {
if (patch.x === x && patch.y === y) {
className += " " + patch.type
if (patch.wet) {
className += " " + wet
}
}
})
patches.push(
<div className={className}
key={x + "x" + y} />
)
}
rows.push(
<div...