PHP - - By Christopher Pitt

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

Game Development with PHP and ReactJS

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

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

Stardew valley

I wasn’t even sure I knew enough about React 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 React. 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.

Recommended Courses

Wes Bos
A step-by-step training course to get you building real world React.js + Firebase apps and website components in a couple of afternoons. Use coupon code 'SITEPOINT' at checkout to get 25% off.

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 led 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 Stack Overflow thread in which folks explained various things to think about. I got about halfway through it before realizing I may have been starting from the wrong place.

“First things first: I need a PHP server. I’m going to have a bunch of React clients, so I want something capable of high-concurrency (perhaps even WebSockets). 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 WebSockets. I added my recent work with PHP preprocessors to make things cleaner, and made the first couple of endpoints.

From config.pre:

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

I decided to use Aerys for the HTTP and WebSocket portions of the application. This code looked very different from 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 this:

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

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

From loader.php:

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

I then installed my dependencies. This is from composer.json:

"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"
},

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. From routes/web.pre:

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

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

And, from routes/api.pre:

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

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

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.

From app/Actions/HomeAction.pre:

namespace App\Action;

use Aerys\Request;
use Aerys\Response;

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

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

From composer.json:

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

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. From package.json:

"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"
},

Mix used Webpack to preprocess and bundle JS and CSS files. I also needed to install the React 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 the following. From webpack.mix.js:

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

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. From package.json:

"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"
},

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. From helpers.pre:

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

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. From app/Actions/HomeAction.pre:

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

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

I realized I could keep creating functions that returned promises, and use them in this way to keep my code asynchronous. Here’s my JS code, from assets/js/component.jsx:

import React from "react"

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

export default Component

… and, from assets/js/app.jsx:

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

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

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 WebSockets

I was almost set up. The last thing to do was to connect the back end and the front end, via WebSockets. I found this relatively straightforward, with a new class. From app/Socket/GameSocket.pre:

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

… and a slight modification to the web routes (from routes/web.pre):

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

Now, I could alter the JS to connect to this WebSocket, and send a message to everyone connected to it. From assets/js/component.jsx:

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

When I created a new Component object, it would connect to the WebSocket 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 WebSockets 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 WebSockets.

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 React interface.

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!