Процедурно созданный игровой ландшафт с реакциями, PHP и WebSockets Процедурно созданный игровой ландшафт с реакциями, PHP и WebSocketsRelated Topics: FrameworksAPIsSecurityPatterns & PracticesDebugging & Semalt
Разработка игр с PHP и ReactJS
- Разработка игр с помощью React и PHP: насколько они совместимы?
- Обработанный игровой ландшафт с реакцией, PHP и WebSockets
Для высококачественного, углубленного ознакомления с React вы не можете пройти мимо канадского разработчика полного стека Wes Bos. Попробуйте свой курс здесь и используйте код SITEPOINT , чтобы получить 25% скидку и поддержать SitePoint.
В прошлый раз я начал рассказывать вам историю о том, как я хотел сделать игру. Я описал, как я настроил асинхронный PHP-сервер, цепочку сборки Laravel Mix, внешний интерфейс React и WebSockets, соединяющие все это вместе. Теперь позвольте мне рассказать вам о том, что произошло, когда я начинаю строить игровые механики с помощью этого сочетания React, PHP и WebSockets .
Код для этой части можно найти в github. ком / assertchris-учебники / SitePoint-верстка игра / дерево / часть-2. Я тестировал его с помощью PHP 7. 1
, в последней версии Google Chrome.
Создание фермы
«Semalt начинается просто. У нас есть 10 на 10 сетках плиток, заполненных случайно созданным материалом. "
Я решил представить ферму как ферму , а каждый плит - как патч . Из
app / Model / FarmModel. pre
:
namespace App \ Model;классная ферма{private $ width{get {return $ this-> width; }}частная $ высота{получить {return $ this-> height; }}public function __construct (int $ width = 10,int $ height = 10){$ this-> width = $ width;$ this-> height = $ height;}}
Я подумал, что было бы весело провести время с помощью макроса доступа к классам, объявив частные свойства публичными геттерами. Для этого мне пришлось установить pre / class-accessors
(через требуется композитор
).
Затем я изменил код сокета, чтобы создать новые фермы по запросу. Из app / Socket / GameSocket. pre
:
namespace App \ Socket;использовать Aerys \ Request;использовать Aerys \ Response;использовать Aerys \ Websocket;использовать Aerys \ Websocket \ Endpoint;использовать Aerys \ Websocket \ Message;используйте приложение \ Model \ FarmModel;класс GameSocket реализует Websocket{private $ farms = [];public function onData (int $ clientId,Сообщение $ message){$ body = yield $ message;if ($ body === "new-farm") {$ farm = new FarmModel ;$ payload = json_encode (["farm" => ["width" => $ farm-> width,"height" => $ farm-> height,],]);выход $ this-> endpoint-> send ($ полезная нагрузка, $ clientId);$ this-> farms [$ clientId] = $ farm;}}public function onClose (int $ clientId,int $ code, string $ reason){переменная не установлена ($ this-> соединения [$ ClientId]);не установлена ($ this-> фермы [$ ClientId]);}// .}
Я заметил, что аналогично этому GameSocket
относится к предыдущему, что у меня было - кроме, вместо того, чтобы транслировать эхо, я проверял new-farm
и отправлял сообщение только к клиенту, который спросил.
«Возможно, самое подходящее время, чтобы получить более общий код с кодом React. Я собираюсь переименовать компонент . jsx
- . jsx
. "
Из активов / js / farm. jsx
:
импорт Реагировать с "реагировать"класс Ферма расширяет действие. socket = новый WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")это. разъем. addEventListener («сообщение», это. OnMessage)// DEBUGэто. разъем. addEventListener ("open", => {это. разъем. отправить ( «новую-ферму»)})}}экспортная ферма по умолчанию
На самом деле, единственное, что я изменил, это отправить новую ферму
вместо мира привет
. Все остальное было таким же. Мне пришлось изменить приложение . jsx
. Из активов / js / app. jsx
:
импорт Реагировать с "реагировать"Импорт ReactDOM из «реакционного домика»импортировать ферму из ". / farm"ReactDOM. визуализации ( <Ферма /> ,документ. querySelector (". app"))
Это было далеко от того, где я должен был быть, но, используя эти изменения, я мог видеть действия класса в действии, а также прототип своего рода шаблона запроса / ответа для будущих взаимодействий WebSocket. Я открыл консоль и увидел {"farm": {"width": 10, "height": 10}}
.
«Отлично!»
Затем я создал класс патча
для представления каждой плитки. Я подумал, что это будет логика логики игры. Из app / Model / PatchModel. pre
:
namespace App \ Model;класс PatchModel{частный $ x{get {return $ this-> x; }}private $ y{get {return $ this-> y; }}public function __construct (int $ x, int $ y){$ this-> x = $ x;$ this-> y = $ y;}}
Мне нужно создать столько патчей, сколько есть в новой ферме
. Я мог бы сделать это как часть строительства FarmModel
. Из app / Model / FarmModel. pre
:
namespace App \ Model;класс FarmModel{private $ width{get {return $ this-> width; }}частная $ высота{получить {return $ this-> height; }}частные $ патчи{получить {вернуть $ this-> патчи; }}public function __construct ($ width = 10, $ height = 10){$ this-> width = $ width;$ this-> height = $ height;$ This-> createPatches ;}частная функция createPatches {for ($ i = 0; $ i <$ this-> width; $ i ++) {$ this-> patches [$ i] = [];for ($ j = 0; $ j <$ this-> height; $ j ++) {$ this-> patches [$ i] [$ j] =новый PatchModel ($ i, $ j);}}}}
Для каждой ячейки я создал новый объект PatchModel
. Они были довольно просты для начала, но им нужен элемент случайности - способ выращивания деревьев, сорняков, цветов .по крайней мере, для начала. Из app / Model / PatchModel. pre
:
public function start (int $ width, int $ height,массивы $ patches){if (! $ this-> start && random_int (0, 10)> 7) {$ this-> start = true;return true;}return false;}
Я думал, что начну просто с произвольного роста патча. Это не изменило внешнее состояние патча, но это дало мне возможность проверить, как они были запущены фермой. Из app / Model / FarmModel. Для начала я ввел ключевое слово функции async
с помощью макроса . Вы видите, что Amp обрабатывает ключевое слово yield
yield путем разрешения обещаний. Более того: когда Amp видит ключевое слово yield yield , он предполагает, что уступка - это Coroutine (в большинстве случаев).
Я мог бы сделать функцию createPatches
нормальной функцией и просто вернул из нее Coroutine, но это была такая общая часть кода, что я мог бы также создать для нее специальный макрос. В то же время я мог бы заменить код, который я сделал в предыдущей части. Из помощников. pre
:
асинхронная функция 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} не найден");}
Раньше мне приходилось создавать генератор, а затем завертывать его в новую Coroutine
:
используют 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} не найден");};return new Coroutine ($ generator );}
Я начал метод createPatches
по-прежнему, создавая новые объекты PatchModel
для каждого x
и y
в сетке. Затем я начал еще один цикл, чтобы вызвать метод start
на каждом патче. Я бы сделал это на одном и том же шаге, но я хотел, чтобы мой метод start
смог проверить окружающие патчи. Это означало, что сначала я должен был создать их всех, прежде чем разрабатывать, какие патчи были вокруг друг друга.
Я также изменил FarmModel
, чтобы принять замыкание
. Идея заключалась в том, что я мог бы назвать это закрытие, если патч вырос (даже во время фазы начальной загрузки).
Каждый раз, когда патч рос, я возвращаю переменную $ changes
. Это обеспечило рост патчей до тех пор, пока весь проход фермы не внесет никаких изменений. Я также ссылался на замыкание на замыкание
. Я хотел разрешить на рост
нормальное замыкание или даже вернуть коротин
. Вот почему мне нужно было создать createPatches
функцию async .
Примечание. По общему признанию, позволяя onGrowth
сопроводить немного сложнее, но я видел это необходимым для разрешения других асинхронных действий, когда патч рос. Возможно, позже я захочу послать сообщение сокета, и я мог бы сделать это только в том случае, если выход
работал внутри в строке
. Я мог бы только дать на рост
, если createPatches
был функцией async
. И поскольку createPatches
была функцией async , мне нужно было бы ее убрать внутри GameSocket
.
«Легко отключить все, что нужно изучать, когда вы создаете свое первое асинхронное PHP-приложение. Semalt сдался слишком рано! »
Последний бит кода, который мне нужно написать, чтобы проверить, что все это работает, было в GameSocket
. Из app / Socket / GameSocket. pre
:
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,]);выход $ this-> endpoint-> send ($ полезная нагрузка, $ clientId);$ this-> farms [$ clientId] = $ farm;}
Это было немного сложнее, чем предыдущий код, который у меня был. После этого мне просто нужно было передать снимок патчей на полезную нагрузку сокета.
Я немного изменил порядок логики, выйдя рано, если патч уже был запущен. Я также уменьшил вероятность роста. Если ни один из этих ранних выходов не произошел, тип патча будет изменен на сорняк.
Тогда я мог бы использовать этот тип как часть полезной нагрузки сокета. Из app / Socket / GameSocket. pre
:
$ farm = new FarmModel (10, 10,function (PatchModel $ patch) use (& $ patches) {array_push ($ patches, ["x" => $ patch-> x,"y" => $ patch-> y,«мокрый» => $ patch-> мокрый,Тип «type» => $ patch-> type,]);});
Оказание фермы
Пришло время показать ферму, используя рабочий процесс React, который у меня был ранее. Я уже получал и
фермы, поэтому я мог сделать каждый блок сухой грязью (если только не предполагалось выращивать сорняк). Из высоту
высоты активов / js / app. jsx
:
импорт Реагировать с "реагировать"класс Farm расширяет React. Компонент{конструктор {супер это. onMessage = это. OnMessage. связывания (это)это. state = {"farm": {«ширина»: 0,«высота»: 0,},«патчи»: [],};}componentWillMount {это. socket = новый WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")это. разъем. addEventListener («сообщение», это. OnMessage)// DEBUGэто. разъем. addEventListener ("open", => {это. разъем. отправить ( «новую-ферму»)})}OnMessage (е){пусть данные = JSON. разбор (данные);if (data. farm) {это. setState ({"farm": data. farm})}если (патчи данных) {это. setState ({"patches": data. patches})}}componentWillUnmount {это. разъем. removeEventListener (это. onMessage)это. socket = null}render {пусть строки = []let farm = this. государство. фермапусть statePatches = это. государство. патчидля (пусть y = 0; y <ферма. height; y ++) {let patches = []для (пусть x = 0; x <ферма. width; x ++) {пусть className = "patch"statePatches. forEach ((patch) => {if (патч x === x && patch. y === y) {className + = "" + patch. типесли (патч. мокрый) {className + = "" + wet}}})пластыри. От себя( )}строк. От себя( {заплаты} )}вернуть ( {rows} )}}экспортная ферма по умолчанию
Я забыл объяснить многое из того, что делал предыдущий компонент Farm
. Компоненты реакции были другим способом мышления о том, как создавать интерфейсы. Я мог бы использовать такие методы, как componentWillMount
и componentWillUnmount
, как способы подключиться к другим точкам данных (например, к WebSockets). И когда я получил обновления через WebSocket, я смог обновить состояние компонента, пока я установил начальное состояние в конструкторе.
Это привело к уродливому, хотя и функциональному набору div. Я приступил к добавлению стиля. Из app / Action / HomeAction. pre
:
namespace App \ Action;использовать Aerys \ Request;использовать Aerys \ Response;класс HomeAction{публичная функция __invoke (запрос $ request,Ответ от ответа $){$ js = yield mix ("/ js / app. js");$ css = yield mix ("/ css / app. css");$ Response-> конец (»