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