使用React,PHP和WebSockets程序生成的游戏地形

上次 ,我开始告诉您有关如何制作游戏的故事。 我描述了如何设置异步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方法,为网格中的每个xy创建了新的PatchModel对象。 然后,我开始了另一个循环,以在每个补丁程序上调用start方法。 我将在同一步骤中完成这些操作,但是我希望start方法能够检查周围的补丁。 这意味着我必须先创建所有这些文件,然后才能确定哪些补丁彼此相关。

我还更改了FarmModel以接受onGrowth闭包。 我的想法是,如果补丁发布(即使在引导阶段),我也可以称之为关闭。

每次补丁发布时,我都会重置$changes变量。 这确保了补丁将继续增长,直到整个农场都没有变化为止。 我还调用了onGrowth闭包。 我想让onGrowth成为普通的闭包,甚至返回Coroutine 。 这就是为什么我需要使createPatches成为async函数。

注意:诚然,允许onGrowth协程有点复杂,但是我认为在补丁增长时,允许其他异步动作是必不可少的。 也许以后我想发送套接字消息,并且只有在yieldonGrowth内部onGrowth ,我才能这样做。 如果createPatchesasync函数,则只能产生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()以便每个人都有机会进行随机化。 之后,我只需要将补丁的快照传递给套接字有效负载即可。

返回随机补丁

每个农场的随机补丁

“如果我以干燥污垢开始每个补丁怎么办? 然后我可以使一些补丁带有杂草,而另一些则带有树木……”

我着手定制补丁。 从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工作流程来展示服务器场了。 我已经知道了农场的widthheight ,因此我可以使每块土壤都变干(除非应该种植杂草)。 从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中。 我可以使用诸如componentWillMountcomponentWillUnmount类的方法作为钩接到其他数据点(如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;
    }
  }
}

现在,生成的农场具有一些颜色:

随机农场

你有一个农场,你有一个农场……

摘要

这绝不是一个完整的游戏。 它缺少诸如玩家输入和玩家角色之类的重要内容。 它不是多人游戏。 但是这次会议使人们对React组件,WebSocket通信和预处理器宏有了更深入的了解。

我很期待下一部分,在那里我可以开始接受玩家的投入,并改变农场。 也许我什至会开始使用播放器登录系统。 或许有一天!

From: https://www.sitepoint.com/procedurally-generated-game-terrain-reactjs-php-websockets/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值