PHP
Article
By Christopher Pitt

Game Development with ReactJS and PHP: How Compatible Are They?

By Christopher Pitt

Game Development with PHP and ReactJS

This article was peer reviewed by Niklas Keller. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


“I’d like to make a multiplayer, economy-based game. Something like Stardew Valley, but with none of the befriending aspects and a player-based economy.”

Stardew valley

I started thinking about this the moment I decided to try and build a game using PHP and ReactJS. The trouble is that I knew nothing about the dynamics of multiplayer games, or how to think about and implement player-based economies.

I wasn’t even sure that I knew enough about ReactJS to justify using it. I mean, the initial interface – where I focus heavily on the server and economic aspects of the game – is perfectly suited for ReactJS. But what about when I start to make the farming /interaction aspects? I love the idea of building an isometric interface around the economic system.

I once watched a talk, by dead_lugosi, where she described building a medieval game in PHP. Margaret inspired me, and that talk was one of the things that lead to me writing a book about JS game development. I became determined to write about my experience. Perhaps others could learn from my mistakes in this case, too.

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

Setting Up The Back-end

The first thing I searched for was guidance on building multiplayer economies. I found an excellent StackOverflow thread in which folks explained various things to think about. I got about half-way through it before realizing I may be starting from the wrong place.

“First things first; I need a PHP server. I’m going to have a bunch of ReactJS clients, so I want something capable of high-concurrency (perhaps even Web Sockets). And it needs to be persistent: things must happen even when players aren’t around.”

I went to work setting up an async PHP server – to handle high concurrency and support web sockets. I added my recent work with PHP preprocessors to make things cleaner, and made the first couple of endpoints:

$host = new Aerys\Host();
$host->expose("*", 8080);

$host->use($router = Aerys\router());
$host->use($root = Aerys\root(.."/public"));

$web = process .."/routes/web.pre";
$web($router);

$api = process .."/routes/api.pre";
$api($router);

This is from config.pre

I decided to use Aerys for the HTTP and Web Socket portions of the application. This code looked very different compared to the Aerys docs, but that’s because I had a good idea about what I needed.

The usual process for running an Aerys app was to use a command like:

vendor/bin/aerys -d -c config.php

That’s a lot of code to keep repeating, and didn’t handle the fact that I wanted to use PHP preprocessing. I created a loader file:

return Pre\processAndRequire(__DIR__ . "/config.pre");

This is from loader.php

…and installed my dependencies:

"require": {
    "amphp/aerys": "dev-amp_v2",
    "amphp/parallel": "dev-master",
    "league/container": "^2.2",
    "league/plates": "^3.3",
    "pre/short-closures": "^0.4.0"
},
"require-dev": {
    "phpunit/phpunit": "^6.0"
},

This is from composer.json

I wanted to use amphp/parallel, to move blocking code out of the async server, but it wouldn’t install with a stable tag of amphp/aerys. That’s why I went with the dev-amp_v2 branch.

I thought it would be a good idea to include some sort of template engine and service locator. I opted for PHP League versions of each. Finally I added pre/short-closures, both to handle the custom syntax in config.pre and the short closures I planned on using after…

Then I set about creating routes files:

use Aerys\Router;
use App\Action\HomeAction;

return (Router $router) => {
    $router->route(
        "GET", "/", new HomeAction
    );
};

This is from routes/web.pre

…and:

use Aerys\Router;
use App\Action\Api\HomeAction;

return (Router $router) => {
    $router->route(
        "GET", "/api", new HomeAction
    );
};

This is from routes/api.pre

Though simple routes, these helped me to test the code in config.pre. I decided to make these routes files return closures, so I could pass them a typed $router, to which they could add their own routes. Finally, I created two (similar) actions:

namespace App\Action;

use Aerys\Request;
use Aerys\Response;

class HomeAction
{
    public function __invoke(Request $request,
        Response $response)
    {
        $response->end("hello world");
    }
}

This is from app/Actions/HomeAction.pre

One final touch was to add shortcut scripts, to launch dev and prod versions of the Aerys server:

"scripts": {
    "dev": "vendor/bin/aerys -d -c loader.php",
    "prod": "vendor/bin/aerys -c loader.php"
},
"config": {
    "process-timeout": 0
},

This is from composer.json

With all of this done, I could spin up a new server, and visit http://127.0.0.1:8080 just by typing:

composer dev

Setting Up The Front-end

“Ok, now that I’ve got the PHP side of things relatively stable; how am I going to build the ReactJS files? Perhaps I can use Laravel Mix…?”

I wasn’t keen on creating a whole new build chain, and Mix had been rebuilt to work well on non-Laravel projects too. Although it was relatively easy to configure and extend, it favored VueJS by default.

The first thing I had to do was install a few NPM dependencies:

"devDependencies": {
    "babel-preset-react": "^6.23.0",
    "bootstrap-sass": "^3.3.7",
    "jquery": "^3.1.1",
    "laravel-mix": "^0.7.5",
    "react": "^15.4.2",
    "react-dom": "^15.4.2",
    "webpack": "^2.2.1"
},

This is from package.json

Mix used Webpack to preprocess and bundle JS and CSS files. I also needed to install the ReactJS (and related Babel) libraries, to build jsx files. Finally, I added the Bootstrap files, for a bit of default styling.

Mix automatically loaded a custom configuration file, so I added:

let mix = require("laravel-mix")

// load babel presets for jsx files

mix.webpackConfig({
    "module": {
        "rules": [
            {
                "test": /jsx$/,
                "exclude": /(node_modules)/,
                "loader": "babel-loader" + mix.config.babelConfig(),
                "query": {
                    "presets": [
                        "react",
                        "es2015",
                    ],
                },
            },
        ],
    },
})

// set up front-end assets

mix.setPublicPath("public")

mix.js("assets/js/app.jsx", "public/js/app.js")
mix.sass("assets/scss/app.scss", "public/css/app.css")
mix.version()

This is from webpack.mix.js

I needed to tell Mix what to do with jsx files, so I added the same kind of configuration one might normally put in .babelrc. I planned to have single JS and CSS entry-points into the application’s various bits and bobs.

Note: Future versions of Mix will ship with built-in support for building ReactJS assets. When that happens, the mix.webpackConfig code can be removed.

Once again I created a few shortcut scripts, to save on serious typing:

"scripts": {
    "dev": "$npm_package_config_webpack",
    "watch": "$npm_package_config_webpack -w",
    "prod": "$npm_package_config_webpack -p"
},
"config": {
    "webpack": "webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},

This is from package.json

All three scripts used the Webpack variable command, but they differed in what they did beyond that. dev built a debug version of the JS and CSS files. The -w switch started the Webpack watcher (so that bundles could be partially rebuilt). The -p switch enabled a lean production version of the bundles.

Since I was using bundle versioning, I needed a way to reference files like /js/app.60795d5b3951178abba1.js without knowing the hash. I noticed Mix liked to create a manifest file, so I made a helper function to query it:

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

This is from helpers.pre

Aerys knew how to handle promises when they came in the form of $val = yield $promise, so I used Amp’s Promise implementation. When the file was read and decoded, I could look for the matching file path. I adjusted HomeAction:

public function __invoke(Request $request,
    Response $response)
{
    $path = yield mix("/js/app.js");

    $response->end("
        <div class='app'></div>
        <script src='{$path}'></script>
    ");
}

This is from app/Actions/HomeAction.pre

I realised I could keep creating functions that returned promises, and use them in this way to keep my code asynchronous. My JS code looked like this:

import React from "react"

class Component extends React.Component
{
    render() {
        return <div>hello world</div>
    }
}

export default Component

This is from assets/js/component.jsx

… and:

import React from "react"
import ReactDOM from "react-dom"
import Component from "./component"

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

This is from assets/js/app.jsx

After all, I just wanted to see whether Mix would compile my jsx files, and if I could find them again using the async mix function. Turns out it worked!

Note: Using the mix function every time is expensive, especially if we’re loading the same files. Instead, we could load all the templates in the server bootstrapping phase, and reference them from inside our actions when needed. The configuration file we start Aerys with can return a promise (like the kind Amp\all gives us), so we could resolve all the templates before the server starts up.

Connecting With Web Sockets

I was almost set up. The last thing to do was to connect the back-end and the front-end, via Web Sockets. I found this relatively straightforward, with a new class:

namespace App\Socket;

use Aerys\Request;
use Aerys\Response;
use Aerys\Websocket;
use Aerys\Websocket\Endpoint;
use Aerys\Websocket\Message;

class GameSocket implements Websocket
{
    private $endpoint;
    private $connections = [];

    public function onStart(Endpoint $endpoint)
    {
        $this->endpoint = $endpoint;
    }

    public function onHandshake(Request $request,
        Response $response)
    {
        $origin = $request->getHeader("origin");

        if ($origin !== "http://127.0.0.1:8080") {
            $response->setStatus(403);
            $response->end("<h1>origin not allowed</h1>");
            return null;
        }

        $info = $request->getConnectionInfo();

        return $info["client_addr"];
    }

    public function onOpen(int $clientId, $address)
    {
        $this->connections[$clientId] = $address;
    }

    public function onData(int $clientId,
        Message $message)
    {
        $body = yield $message;

        yield $this->endpoint->broadcast($body);
    }

    public function onClose(int $clientId,
        int $code, string $reason)
    {
        unset($this->connections[$clientId]);
    }

    public function onStop()
    {
        // nothing to see here...
    }
}

This is from app/Socket/GameSocket.pre

…and a slight modification to the web routes:

use Aerys\Router;
use App\Action\HomeAction;
use App\Socket\GameSocket;

return (Router $router) => {
    $router->route(
        "GET", "/", new HomeAction
    );

    $router->route(
        "GET", "/ws", Aerys\websocket(new GameSocket)
    );
};

This is from routes/web.pre

Now, I could alter the JS to connect to this web socket, and send a message to everyone connected to it:

import React from "react"

class Component extends React.Component
{
    constructor()
    {
        super()
        this.onMessage = this.onMessage.bind(this)
    }

    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("hello world")
        })
    }

    onMessage(e)
    {
        console.log("message: " + e.data)
    }

    componentWillUnmount()
    {
        this.socket.removeEventListener(this.onMessage)
        this.socket = null
    }

    render() {
        return <div>hello world</div>
    }
}

export default Component

This is from assets/js/component.jsx

When I created a new Component object, it would connect to the web socket server, and add an event listener for new messages. I added a bit of debugging code – to make sure it was connecting properly, and sending new messages back.

We’ll get to the nitty-gritty of PHP and web sockets later, don’t worry.

Summary

In this part, we looked at how to set up a simple async PHP web server, how to use Laravel Mix in a non-Laravel project, and even how to connect the back-end and front-end together with Web Sockets.

Phew! That’s a lot of ground covered, and we haven’t written a single line of game code. Join me in part two, when we start to build game logic and a ReactJS interface.

  • I never thought of developing a game with PHP, And after reading this article i am surely going to develop something. Great Article.

    • Chris

      So glad to hear that, and thanks for your kind words.

  • NoobSaiboT TheFreakative

    This is interesting . I have been very interested in your work ( reactPHP, etc ) recently and its why I subscribed to updates of your async PHP book . One day I would love to build something like a game with PHP , for now I just gotta level up my JS skills :D

  • Chris

    Thanks for the comment. I do note the “future” support, and when I wrote these it was still very much that. In person projects I still like to use the configuration, but it’s definitely easier just to use mix.react (if one does not require any “interesting” customisations). :)

    • William Cantin

      well unless im wrong, your custom configs are nothing that special haha :P. Should be covered with mix.react()

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