Friday, March 31, 2017

Procedurally Generated Game Terrain with ReactJS, PHP, and Websockets

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 ReactJS front-end, and the Web Sockets connecting all this together. Now, let me tell you about what happened when I starting building the game mechanics with this mix of ReactJS, PHP, and Websockets...

The code for this part can be found at: http://ift.tt/2mVNk1y. I've tested it with PHP 7.1 and in a recent version of Google Chrome.

Final image

Making A Farm

"Let's start simple. We have a 10 by 10 grid of tiles, filled with randomly generated stuff."

I decided to represent the farm as a Farm, and each tile as a Patch:

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;
    }
}

This is from app/Model/FarmModel.pre

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).

I then changed the socket code to allow for new farms to be created on request:

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]);
    }

    // ...
}

This is from app/Socket/GameSocket.pre

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.

"Perhaps it's a good time to get less generic with the ReactJS code. I'm going to rename component.jsx to 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

This is from assets/js/farm.jsx

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:

import React from "react"
import ReactDOM from "react-dom"
import Farm from "./farm"

ReactDOM.render(
    <Farm />,
    document.querySelector(".app")
)

This is from assets/js/app.jsx

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 Web Socket interactions. I opened the console, and saw {"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:

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;
    }
}

This is from app/Model/PatchModel.pre

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:

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);
            }
        }
    }
}

This is from app/Model/FarmModel.pre

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:

public function start(int $width, int $height,
    array $patches)
{
    if (!$this->started && random_int(0, 10) > 7) {
        $this->started = true;
        return true;
    }

    return false;
}

This is from app/Model/PatchModel.pre

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:

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;
                }
            }
        }
    }

    // ...
}

This is from app/Model/FarmModel.pre

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).

Continue reading %Procedurally Generated Game Terrain with ReactJS, PHP, and Websockets%


by Christopher Pitt via SitePoint

No comments:

Post a Comment