PHP
Article
By Christopher Pitt

Procedurally Generated Game Terrain with ReactJS, PHP, and Websockets

By Christopher Pitt

Game Development with PHP and ReactJS

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: github.com/assertchris-tutorials/sitepoint-making-games/tree/part-2. 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).

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:

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

This is from helpers.pre

Previously, I had to make a generator, and then wrap it in a new 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.

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

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.

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.

“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!”

The last bit of code I needed to write to check that this was all working was in GameSocket:

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 is from app/Socket/GameSocket.pre

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.

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:

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

This is from app/Model/PatchModel.pre

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:

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

This is from app/Socket/GameSocket.pre

Rendering The Farm

I was time to show the farm, using the ReactJS 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):

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

This is from assets/js/app.jsx

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

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 Web Sockets). And as I received updates through the Web Socket, I could update the component’s state, so long as I had set the initial state in the constructor.

This resulted in an ugly, albeit functional set of divs. I set about adding some styling:

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

This is from app/Action/HomeAction.pre

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

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

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

This is from assets/scss/app.scss

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

A random farm rendered

“You get a farm, you get a farm…”

Summary

This is 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 ReactJS components, Web Socket communication, and preprocessor macros.

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.

Stay tuned!

  • Daniel Piechnick

    This is much better when done iteratively. Daniel Piechnick.

Recommended
Sponsors
Get the latest in PHP, once a week, for free.