上次 ,我开始告诉您有关如何制作游戏的故事。 我描述了如何设置异步PHP服务器,Laravel Mix构建链,React前端以及将所有这些连接在一起的WebSocket。 现在,让我告诉您有关当我开始使用React,PHP和WebSockets混合来构建游戏机制时发生了什么……
该部分的代码可以在github.com/assertchris-tutorials/sitepoint-making-games/tree/part-2中找到。 我已经在最新版本的Google Chrome中使用PHP 7.1
对其进行了测试。
做一个农场
“让我们从简单开始。 我们有一个10 x 10的瓷砖网格,里面充满了随机生成的东西。”
我决定将农场表示为Farm
,将每个图块表示为Patch
。 从app/Model/FarmModel.pre
:
namespace App\Model;
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;
}
}
我认为这是一个有趣的时间,可以通过使用公共获取器声明私有属性来尝试使用类访问器宏 。 为此,我必须安装pre/class-accessors
(通过composer require
)。
然后,我更改了套接字代码,以允许根据请求创建新的服务器场。 从app/Socket/GameSocket.pre
:
namespace App\Socket;
use Aerys\Request;
use Aerys\Response;
use Aerys\Websocket;
use Aerys\Websocket\Endpoint;
use Aerys\Websocket\Message;
use App\Model\FarmModel;
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
与我以前的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 App\Model;
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 App\Model;
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 App\Model;
use Amp;
use Amp\Coroutine;
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通过解决Promises来处理yield
关键字。 更重要的是:当Amp看到yield
关键字时,它假定产生的是协程(在大多数情况下)。
我本可以使createPatches
函数成为普通函数,并从中返回一个Coroutine,但这是如此常见的一段代码,我也可能为其创建了一个特殊的宏。 同时,我可以替换上一部分中编写的代码。 来自helpers.pre
:
async function mix($path) {
$manifest = yield Amp\File\get(
.."/public/mix-manifest.json"
);
$manifest = json_decode($manifest, true);
if (isset($manifest[$path])) {
return $manifest[$path];
}
throw new Exception("{$path} not found");
}
以前,我必须制造一个生成器,然后将其包装在新的Coroutine
:
use Amp\Coroutine;
function mix($path) {
$generator = () => {
$manifest = yield Amp\File\get(
.."/public/mix-manifest.json"
);
$manifest = json_decode($manifest, true);
if (isset($manifest[$path])) {
return $manifest[$path];
}
throw new Exception("{$path} not found");
};
return new Coroutine($generator());
}
我像以前一样开始了createPatches
方法,为网格中的每个x
和y
创建了新的PatchModel
对象。 然后,我开始了另一个循环,以在每个补丁程序上调用start
方法。 我将在同一步骤中完成这些操作,但是我希望start
方法能够检查周围的补丁。 这意味着我必须先创建所有这些文件,然后才能确定哪些补丁彼此相关。
我还更改了FarmModel
以接受onGrowth
闭包。 我的想法是,如果补丁发布(即使在引导阶段),我也可以称之为关闭。
每次补丁发布时,我都会重置$changes
变量。 这确保了补丁将继续增长,直到整个农场都没有变化为止。 我还调用了onGrowth
闭包。 我想让onGrowth
成为普通的闭包,甚至返回Coroutine
。 这就是为什么我需要使createPatches
成为async
函数。
注意:诚然,允许onGrowth
协程有点复杂,但是我认为在补丁增长时,允许其他异步动作是必不可少的。 也许以后我想发送套接字消息,并且只有在yield
在onGrowth
内部onGrowth
,我才能这样做。 如果createPatches
是async
函数,则只能产生onGrowth
。 而且因为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()
FarmModel
$farm->createPatches()
以便每个人都有机会进行随机化。 之后,我只需要将补丁的快照传递给套接字有效负载即可。
![返回随机补丁](https://i-blog.csdnimg.cn/blog_migrate/e7218296f300a2fb8d36e02871b5952d.gif)
每个农场的随机补丁
“如果我以干燥污垢开始每个补丁怎么办? 然后我可以使一些补丁带有杂草,而另一些则带有树木……”
我着手定制补丁。 从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 className="row" key={y}>
{patches}
</div>
)
}
return (
<div className="farm">{rows}</div>
)
}
}
export default Farm
我忘了解释以前的Farm
组件所做的很多事情。 React组件是思考如何构建接口的另一种方式。 他们将思考过程从“我想更改某些东西时如何与DOM交互?” 改为“在任何给定上下文中,DOM应该是什么样?”
我本来是想将render
方法执行一次,并且将它产生的所有内容都转储到DOM中。 我可以使用诸如componentWillMount
和componentWillUnmount
类的方法作为钩接到其他数据点(如WebSockets)的方法。 当我通过WebSocket收到更新时,只要在构造函数中设置了初始状态,我就可以更新组件的状态。
这导致了一个难看的,尽管功能齐全的div集。 我着手添加一些样式。 从app/Action/HomeAction.pre
:
namespace App\Action;
use Aerys\Request;
use Aerys\Response;
class HomeAction
{
public function __invoke(Request $request,
Response $response)
{
$js = yield mix("/js/app.js");
$css = yield mix("/css/app.css");
$response->end("
<link rel='stylesheet' href='{$css}' />
<div class='app'></div>
<script src='{$js}'></script>
");
}
}
从assets/scss/app.scss
:
.row {
width: 100%;
height: 50px;
.patch {
width: 50px;
height: 50px;
display: inline-block;
background-color: sandybrown;
&.weed {
background-color: green;
}
}
}
现在,生成的农场具有一些颜色:
![随机农场](https://i-blog.csdnimg.cn/blog_migrate/a4f08103f74b6bc57f3c8042d356f474.gif)
你有一个农场,你有一个农场……
摘要
这绝不是一个完整的游戏。 它缺少诸如玩家输入和玩家角色之类的重要内容。 它不是多人游戏。 但是这次会议使人们对React组件,WebSocket通信和预处理器宏有了更深入的了解。
我很期待下一部分,在那里我可以开始接受玩家的投入,并改变农场。 也许我什至会开始使用播放器登录系统。 或许有一天!
From: https://www.sitepoint.com/procedurally-generated-game-terrain-reactjs-php-websockets/