react端生成小程序码_使用React,PHP和WebSockets程序生成的游戏地形

react端生成小程序码

Last time, I began telling you the story of how I wanted to make a game. I described how I set up the async PHP server, the Laravel Mix build chain, the React front end, and WebSockets connecting all this together. Now, let me tell you about what happened when I starting building the game mechanics with this mix of React, PHP, and WebSockets…

上次 ,我开始告诉您有关如何制作游戏的故事。 我描述了如何设置异步PHP服务器,Laravel Mix构建链,React前端以及将所有这些连接在一起的WebSocket。 现在,让我告诉您有关当我开始使用React,PHP和WebSockets的混合来构建游戏机制时发生了什么……



The code for this part can be found at github.com/assertchris-tutorials/sitepoint-making-games/tree/part-2. I’ve tested it with PHP 7.1, in a recent version of Google Chrome.

该部分的代码可以在github.com/assertchris-tutorials/sitepoint-making-games/tree/part-2中找到。 我已经在最新版本的Google Chrome中使用PHP 7.1对其进行了测试。



Final image

做一个农场 (Making a Farm)

“Let’s start simple. We have a 10 by 10 grid of tiles, filled with randomly generated stuff.”

“让我们从简单开始。 我们有一个10乘10格的瓷砖,里面充满了随机生成的东西。”

I decided to represent the farm as a Farm, and each tile as a Patch. From app/Model/FarmModel.pre:

我决定将农场表示为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;
  }
}

I thought it would be a fun time to try out the class accessors macro by declaring private properties with public getters. For this I had to install pre/class-accessors (via composer require).

我认为这是一个有趣的时间,可以通过使用公共获取器声明私有属性来尝试使用类访问器宏 。 为此,我必须安装pre/class-accessors (通过composer require )。

I then changed the socket code to allow for new farms to be created on request. From app/Socket/GameSocket.pre:

然后,我更改了套接字代码,以允许根据请求创建新的服务器场。 从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]);
  }

  // …
}

I noticed how similar this GameSocket was to the previous one I had — except, instead of broadcasting an echo, I was checking for new-farm and sending a message back only to the client that had asked.

我注意到这个GameSocket与我以前的GameSocket有多么相似-只是我没有广播回声,而是在检查new-farm并仅将消息发送回要求的客户端。

“Perhaps it’s a good time to get less generic with the React code. I’m going to rename component.jsx to farm.jsx.”

“也许是时候让React代码的通用性降低。 我将把component.jsx重命名为farm.jsx 。”

From assets/js/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

In fact, the only other thing I changed was sending new-farm instead of hello world. Everything else was the same. I did have to change the app.jsx code though. From assets/js/app.jsx:

实际上,我唯一改变的另一件事是发送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")
)

It was far from where I needed to be, but using these changes I could see the class accessors in action, as well as prototype a kind of request/response pattern for future WebSocket interactions. I opened the console, and saw {"farm":{"width":10,"height":10}}.

距离我需要的地方还很远,但是使用这些更改,我可以看到类访问器正在运行,并且为将来的WebSocket交互提供了一种请求/响应模式的原型。 我打开控制台,看到{"farm":{"width":10,"height":10}}

“Great!”

“大!”

Then I created a Patch class to represent each tile. I figured this was where a lot of the game’s logic would happen. From app/Model/PatchModel.pre:

然后,我创建了一个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;
  }
}

I’d need to create as many patches as there are spaces in a new Farm. I could do this as part of FarmModel construction. From app/Model/FarmModel.pre:

我需要创建的补丁数量与新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);
      }
    }
  }
}

For each cell, I created a new PatchModel object. These were pretty simple to begin with, but they needed an element of randomness — a way to grow trees, weeds, flowers … at least to begin with. From app/Model/PatchModel.pre:

对于每个单元格,我创建了一个新的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;
}

I thought I’d begin just by randomly growing a patch. This didn’t change the external state of the patch, but it did give me a way to test how they were started by the farm. From app/Model/FarmModel.pre:

我以为我会先随机种植一个补丁。 这并没有改变补丁的外部状态,但确实为我提供了一种方法来测试服务器场如何启动它们。 从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;
        }
      }
    }
  }

  // …
}

There was a lot going on here. For starters, I introduced an async function keyword using a macro. You see, Amp handles the yield keyword by resolving Promises. More to the point: when Amp sees the yield keyword, it assumes what is being yielded is a Coroutine (in most cases).

这里发生了很多事情。 首先,我介绍了使用宏的async函数关键字。 您会看到,Amp通过解决Promises来处理yield关键字。 更重要的是:当Amp看到yield关键字时,它假定产生的是协程(在大多数情况下)。

I could have made the createPatches function a normal function, and just returned a Coroutine from it, but that was such a common piece of code that I might as well have created a special macro for it. At the same time, I could replace code I had made in the previous part. From helpers.pre:

我本可以使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");
}

Previously, I had to make a generator, and then wrap it in a new Coroutine:

以前,我必须制作一个生成器,然后将其包装在新的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());
}

I began the createPatches method as before, creating new PatchModel objects for each x and y in the grid. Then I started another loop, to call the start method on each patch. I would have done these in the same step, but I wanted my start method to be able to inspect the surrounding patches. That meant I would have to create all of them first, before working out which patches were around each other.

我像以前一样开始了createPatches方法,为网格中的每个xy创建了新的PatchModel对象。 然后,我启动了另一个循环,以在每个补丁程序上调用start方法。 我将在同一步骤中完成这些操作,但是我希望start方法能够检查周围的补丁。 这意味着我必须先创建所有这些文件,然后才能确定哪些补丁相互围绕。

I also changed FarmModel to accept an onGrowth closure. The idea was that I could call that closure if a patch grew (even during the bootstrapping phase).

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

Each time a patch grew, I reset the $changes variable. This ensured the patches would keep growing until an entire pass of the farm yielded no changes. I also invoked the onGrowth closure. I wanted to allow onGrowth to be a normal closure, or even to return a Coroutine. That’s why I needed to make createPatches an async function.

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

Note: Admittedly, allowing onGrowth coroutines complicated things a bit, but I saw it as essential for allowing other async actions when a patch grew. Perhaps later I’d want to send a socket message, and I could only do that if yield worked inside onGrowth. I could only yield onGrowth if createPatches was an async function. And because createPatches was an async function, I would need to yield it inside GameSocket.

注意:诚然,允许onGrowth协程onGrowth复杂一些,但是我认为在补丁增长时,允许其他异步操作是必不可少的。 也许以后我想发送套接字消息,并且只有在yieldonGrowthonGrowth ,我才能这样做。 如果createPatchesasync函数,则只能产生onGrowth 而且因为createPatches是一个async函数,所以我需要在GameSocket产生它。

“It’s easy to get turned off by all the things that need learning when making one’s first async PHP application. Don’t give up too soon!”

“在制作第一个异步PHP应用程序时,很容易被所有需要学习的东西所关闭。 不要太早放弃!”

The last bit of code I needed to write to check that this was all working was in GameSocket. From app/Socket/GameSocket.pre:

我需要编写的最后一部分代码来检查所有工作是否在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;
}

This was only slightly more complex than the previous code I had. I needed to provide a third parameter to the FarmModel constructor, and yield $farm->createPatches() so that each could have a chance to randomize. After that, I just needed to pass a snapshot of the patches to the socket payload.

这仅比我以前的代码稍微复杂一点。 我需要为FarmModel构造函数提供第三个参数,并产生$farm->createPatches() FarmModel $farm->createPatches()以便每个人都有机会随机化。 之后,我只需要将补丁的快照传递给套接字有效负载即可。

Random Patches being returned

Random patches for each farm

每个农场的随机补丁

“What if I start each patch as dry dirt? Then I could make some patches have weeds, and others have trees …”

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

I set about customizing the patches. From app/Model/PatchModel.pre:

我着手定制补丁。 从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;
}

I changed the order of logic around a bit, exiting early if the patch had already been started. I also reduced the chance of growth. If neither of these early exits happened, the patch type would be changed to weed.

我大约更改了逻辑顺序,如果补丁已启动,则提早退出。 我也减少了成长的机会。 如果这些早期退出均未发生,则补丁类型将更改为杂草。

I could then use this type as part of the socket message payload. From app/Socket/GameSocket.pre:

然后,我可以将这种类型用作套接字消息有效负载的一部分。 从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,
  ]);
}
);

渲染农场 (Rendering the Farm)

It was time to show the farm, using the React workflow I had setup previously. I was already getting the width and height of the farm, so I could make every block dry dirt (unless it was supposed to grow a weed). From assets/js/app.jsx:

是时候使用我之前设置的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

I had forgotten to explain much of what the previous Farm component was doing. React components were a different way of thinking about how to build interfaces. They changed one’s thought process from “How do I interact with the DOM when I want to change something?” to “What should the DOM look like with any given context?”

我忘了解释以前的Farm组件所做的很多事情。 React组件是思考如何构建接口的另一种方式。 他们将思考过程从“我想更改某些东西时如何与DOM交互?” 改为“在任何给定上下文中,DOM应该是什么样?”

I was meant to think about the render method as only executing once, and that everything it produced would be dumped into the DOM. I could use methods like componentWillMount and componentWillUnmount as ways to hook into other data points (like WebSockets). And as I received updates through the WebSocket, I could update the component’s state, so long as I had set the initial state in the constructor.

我本来是想将render方法执行一次,并且将它产生的所有内容都转储到DOM中。 我可以使用诸如componentWillMountcomponentWillUnmount类的方法作为钩接到其他数据点(如WebSockets)的方法。 当我通过WebSocket收到更新时,只要我在构造函数中设置了初始状态,就可以更新组件的状态。

This resulted in an ugly, albeit functional set of divs. I set about adding some styling. From app/Action/HomeAction.pre:

这导致了一个难看的,尽管功能齐全的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>
    ");
  }
}

From assets/scss/app.scss:

assets/scss/app.scss

.row {
  width: 100%;
  height: 50px;

  .patch {
    width: 50px;
    height: 50px;
    display: inline-block;
    background-color: sandybrown;

    &.weed {
      background-color: green;
    }
  }
}

The generated farms now had a bit of color to them:

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

A random farm rendered

You get a farm, you get a farm …

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

摘要 (Summary)

This was by no means a complete game. It lacked vital things like player input and player characters. It wasn’t very multiplayer. But this session resulted in a deeper understanding of React components, WebSocket communication, and preprocessor macros.

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

I was looking forward to the next part, wherein I could start taking player input, and changing the farm. Perhaps I’d even start on the player login system. Maybe one day!

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

翻译自: https://www.sitepoint.com/procedurally-generated-game-terrain-reactjs-php-websockets/

react端生成小程序码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值