使用 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.jsxfarm.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 每个对象 xy 在网格中。 然后我开始了另一个循环,调用 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 工作流。 我已经得到了 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...

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注