“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 React. The trouble is, I knew nothing about the dynamics of multiplayer games, or how to think about and implement player-based economies.
当我决定尝试使用PHP和React构建游戏时,我就开始考虑这一点。 问题是,我对多人游戏的动态性或如何考虑和实施基于玩家的经济一无所知。
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.
我什至不确定我是否对React足够了解,可以证明使用它是合理的。 我的意思是,初始界面(我主要关注服务器和游戏的经济方面)非常适合React。 但是,当我开始进行耕作/互动方面时呢? 我喜欢围绕经济系统建立等距界面的想法。
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.
我曾经看过dead_lugosi的演讲 ,她在那儿描述了用PHP构建中世纪的游戏。 玛格丽特激发了我的灵感,而那次演讲是促使我写一本关于JS游戏开发的书的原因之一 。 我决心写自己的经历。 在这种情况下,也许其他人也可以从我的错误中学到东西。
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.
该部分的代码可以在以下网址找到: github.com/assertchris-tutorials/sitepoint-making-games/tree/part-1 。 我已经在PHP 7.1和最新版本的Google Chrome中对其进行了测试。
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.
我搜索的第一件事是建立多人游戏经济的指南。 我发现了一个出色的Stack Overflow线程 ,人们在其中解释了要考虑的各种事情。 在意识到可能是从错误的地方开始之前,我已经走了一半。
“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.”
首先,我需要一台PHP服务器。 我将有大量的React客户端,所以我想要一些具有高并发性的东西(甚至WebSockets)。 它必须是持久的:即使玩家不在身边,事情也必须发生。”
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.
我开始工作以设置异步PHP服务器-处理高并发性并支持WebSockets。 我添加了最近与PHP预处理器一起使用的工作,以使事情更整洁,并创建了头两个端点。
From config.pre:
从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.
我决定将Aerys用于应用程序的HTTP和WebSocket部分。 这段代码看起来与Aerys文档大不相同,但这是因为我对所需内容有很好的了解。
The usual process for running an Aerys app was to use a command like this:
运行Aerys应用程序的通常过程是使用如下命令:
vendor/bin/aerys -d -c config.phpThat’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.
有很多代码需要重复,并且没有处理我想要使用PHP预处理的事实。 我创建了一个加载器文件。
From loader.php:
从loader.php :
return Pre\processAndRequire(__DIR__ . "/config.pre");I then installed my dependencies. This is from composer.json:
然后安装我的依赖项。 这是来自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.
我想使用amphp/parallel来将阻止代码移出异步服务器,但它不会与amphp/aerys的稳定标记amphp/aerys 。 这就是为什么我选择dev-amp_v2分支。
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…
我认为包括某种模板引擎和服务定位器是一个好主意。 我选择了每个PHP League版本。 最后,我添加了pre/short-closures ,既可以处理config.pre的自定义语法,又可以计划在after之后使用的简短闭包……
Then I set about creating routes files. From routes/web.pre:
然后我开始创建路由文件。 从routes/web.pre :
use Aerys\Router; use App\Action\HomeAction; return (Router $router) => { $router->route( "GET", "/", new HomeAction ); };And, from routes/api.pre:
并且,从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.
尽管路由很简单,但这些帮助我测试了config.pre的代码。 我决定使这些路由文件返回闭包,因此我可以将它们传递给类型化的$router ,它们可以在其中添加自己的路由。 最后,我创建了两个(类似)动作。
From app/Actions/HomeAction.pre:
从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.
最后一步是添加快捷方式脚本,以启动Aerys服务器的开发和生产版本。
From composer.json:
从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:
完成所有这些操作后,我可以启动新服务器,只需键入以下内容即可访问http://127.0.0.1:8080 。
composer dev“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…?”
“好吧,现在我对PHP的了解相对稳定了; 我将如何构建ReactJS文件? 也许我可以使用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.
我并不热衷于创建一个全新的构建链,并且Mix已被重建以在非Laravel项目中也能很好地工作。 尽管配置和扩展相对容易,但是默认情况下它偏爱VueJS。
The first thing I had to do was install a few NPM dependencies. From package.json:
我要做的第一件事是安装一些NPM依赖项。 从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.
混合使用Webpack预处理和捆绑JS和CSS文件。 我还需要安装React和相关的Babel库来构建jsx文件。 最后,我添加了Bootstrap文件,以获取一些默认样式。
Mix automatically loaded a custom configuration file, so I added the following. From webpack.mix.js:
Mix自动加载了自定义配置文件,因此我添加了以下内容。 从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.
我需要告诉Mix如何处理jsx文件,因此我添加了通常可能放在.babelrc的相同配置。 我计划将单个JS和CSS入口点插入到应用程序的各种细节中。
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.
注意:Mix的未来版本将附带对构建ReactJS资产的内置支持。 发生这种情况时,可以删除mix.webpackConfig代码。
Once again, I created a few shortcut scripts, to save on serious typing. From package.json:
再次,我创建了一些快捷方式脚本,以节省认真的打字时间。 从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.
所有这三个脚本都使用了Webpack variable命令,但是它们的不同之处在于它们。 dev构建了JS和CSS文件的调试版本。 -w开关启动了Webpack监视程序(以便可以部分重建捆绑软件)。 -p开关启用了捆绑软件的精益生产版本。
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:
由于我使用的是捆绑软件版本控制,因此我需要一种在不知道哈希值的情况下引用/js/app.60795d5b3951178abba1.js文件的方法。 我注意到Mix喜欢创建清单文件,因此我做了一个辅助函数来查询它。 来自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:
当Aerys以$val = yield $promise的形式出现时,Aerys知道如何处理它们,因此我使用了Amp的Promise实现。 读取和解码文件后,我可以寻找匹配的文件路径。 我调整了HomeAction 。 从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:
我意识到我可以继续创建返回promise的函数,并以这种方式使用它们使我的代码保持异步。 这是我的JS代码,来自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:
…并且,从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!
毕竟,我只是想看看Mix是否会编译我的jsx文件,以及是否可以使用异步mix函数再次找到它们。 原来它起作用了!
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.
注意:每次都使用mix函数非常昂贵,尤其是在加载相同文件的情况下。 相反,我们可以在服务器引导阶段加载所有模板,并在需要时从我们的操作内部引用它们。 我们启动Aerys时使用的配置文件可以返回一个承诺(例如Amp\all给我们的那种),因此我们可以在服务器启动之前解析所有模板。
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:
我快要准备好了。 最后要做的是通过WebSockets连接后端和前端。 我发现这相对简单,有了一个新类。 从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):
…并对网络路由进行了一些修改(来自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:
现在,我可以更改JS以连接到该WebSocket,并向与其连接的每个人发送一条消息。 从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 ComponentWhen 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.
创建新的Component对象时,它将连接到WebSocket服务器,并为新消息添加事件侦听器。 我添加了一些调试代码,以确保其正确连接并向后发送新消息。
We’ll get to the nitty-gritty of PHP and WebSockets later, don’t worry.
稍后,我们将介绍PHP和WebSocket的细节,请不要担心。
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.
在这一部分中,我们研究了如何设置简单的异步PHP Web服务器,如何在非Laravel项目中使用Laravel Mix,以及如何将后端和前端与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.
! 这是很多方面的知识,我们还没有编写任何游戏代码。 加入第二部分 ,当我们开始构建游戏逻辑和React接口时。
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!
本文由Niklas Keller同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
翻译自: https://www.sitepoint.com/game-development-with-reactjs-and-php-how-compatible-are-they/
相关资源:Amp是一个事件驱动的PHP框架与ReactPHP类似