Back to Question Center
0

Procedureel gegenereerde gameterrein met React, PHP en WebSockets            Procedureel gegenereerde gameterrein met React, PHP en WebSockets-gerelateerde onderwerpen: FrameworksAPIsSecurityPatterns & Praktijken Ontwerpen & Semalt

1 answers:
Procedureel gegenereerde gameterrein met React, PHP en WebSockets

Ontwikkeling van spellen met PHP en ReactJS

  • Ontwikkeling van het spel met React en PHP: hoe compatibel zijn ze?
  • Procedureel gegenereerde gameterrein met React, PHP en WebSockets

Voor een hoogwaardige, grondige kennismaking met React, kun je niet voorbij de Canadese full-stack-ontwikkelaar Wes Bos gaan. Probeer zijn cursus hier, en gebruik de code SITEPOINT om 25% korting te krijgen en om SitePoint te helpen ondersteunen.

De vorige keer begon ik je het verhaal te vertellen over hoe ik een spel wilde maken. Ik heb beschreven hoe ik de asynchrone PHP-server, de Laravel Mix-buildketen, de React-frontend en WebSockets heb ingesteld om dit allemaal met elkaar te verbinden. Laat me je vertellen wat er gebeurde toen ik begon met het bouwen van de game mechanics met deze mix van React, PHP en WebSockets - cuales son las tecnicas de fotografia.


De code voor dit deel is te vinden op github. com / assertchris-tutorials / sitepoint-making-games / tree / part-2. Ik heb het getest met PHP 7. 1 , in een recente versie van Google Chrome.


Procedureel gegenereerde gameterrein met React, PHP en WebSocketsProcedureel gegenereerde gameterrein met React, PHP en WebSockets-gerelateerde onderwerpen:
FrameworksAPIsSecurityPatterns & PracticesDebugging & Semalt

Een boerderij bouwen

"Semalt begint eenvoudig. We hebben een raster van 10 bij 10 rasters, gevuld met willekeurig gegenereerde spullen. "

Ik besloot om het bedrijf te vertegenwoordigen als een boerderij en elke tegel als een pleister . Van app / Model / FarmModel. pre :

  naamruimte App \ Model;klasse Farm{privé $ breedte{krijg {return $ this-> width; }}privé $ hoogte{krijg {return $ this-> height; }}openbare functie __construct (int $ width = 10,int $ height = 10){$ this-> width = $ width;$ this-> height = $ height;}}   

Ik dacht dat het een leuk moment zou zijn om de klasse accessors macro uit te proberen door particuliere eigendommen met openbare getters te verklaren. Hiervoor moest ik pre / class-accessors installeren (via composer require ).

Vervolgens heb ik de socketcode gewijzigd om nieuwe bedrijven op verzoek te kunnen creëren. Vanaf app / socket / gamepad. pre :

  naamruimte App \ Socket;gebruik Aerys \ Request;gebruik Aerys \ Response;gebruik Aerys \ Websocket;gebruik Aerys \ Websocket \ Endpoint;gebruik Aerys \ Websocket \ Message;gebruik App \ Model \ FarmModel;klasse GameSocket implementeert Websocket{privé $ boerderijen = [];openbare functie onData (int $ clientId,Bericht $ bericht){$ body = opbrengst $ bericht;if ($ body === "nieuwe boerderij") {$ farm = new FarmModel   ;$ payload = json_encode (["boerderij" => ["width" => $ farm-> width,"hoogte" => $ boerderij-> hoogte,],]);opbrengst $ this-> endpoint-> send ($ payload, $ clientId);$ this-> farms [$ clientId] = $ farm;}}openbare functie onClose (int $ clientId,int $ code, string $ reden){unset ($ this-> aansluitingen [$ ClientID]);unset ($ this-> boerderijen [$ ClientID]);}// .}   

Ik heb gemerkt hoe vergelijkbaar deze GameSocket was met de vorige die ik had - behalve dat ik in plaats van een echo uit te zenden, ik zocht naar new-farm en alleen een bericht terugstuurde naar de klant die had gevraagd.

"Misschien is het een goed moment om minder algemeen te worden met de React-code. Ik ga het onderdeel hernoemen. jsx tot boerderij. jsx . "

Van activa / js / boerderij. jsx :

  invoer Reageren van "reageren"klasse Farm breidt React uit. socket = nieuwe WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")deze. socket. addEventListener ("bericht", dit. OnMessage)// DEBUGdeze. socket. addEventListener ("open",    => {deze. socket. stuur ( "new-farm")})}}export standaard Boerderij   

In feite was het enige andere dat ik veranderde, het sturen van nieuwe bedrijven in plaats van hallo-wereld . Al het andere was hetzelfde. Ik moest de app veranderen. jsx code wel. Van assets / js / app. jsx :

  invoer Reageren van "reageren"import ReactDOM from "react-dom"import Farm from ". / farm"ReactDOM. maken (,document. querySelector (". app"))   

Het was nog ver verwijderd van waar ik moest zijn, maar met behulp van deze veranderingen zag ik de klasse-accessors in actie, evenals een prototype van een soort verzoek / reactiepatroon voor toekomstige WebSocket-interacties. Ik opende de console en zag {"boerderij": {"width": 10, "height": 10}} .

"Geweldig!"

Vervolgens heb ik een Patch -klasse gemaakt om elke tegel weer te geven. Ik dacht dat dit was waar veel van de logica van het spel zou gebeuren. Van app / Model / PatchModel. pre :

  naamruimte App \ Model;klasse PatchModel{privé $ x{krijg {return $ this-> x; }}privé $ y{krijg {return $ this-> y; }}publieke functie __construct (int $ x, int $ y){$ this-> x = $ x;$ dit-> y = $ y;}}   

Ik zou zoveel patches moeten maken als er spaties zijn in een nieuwe Farm . Ik zou dit kunnen doen als onderdeel van FarmModel constructie. Van app / Model / FarmModel. pre :

  naamruimte App \ Model;klasse FarmModel{privé $ breedte{krijg {return $ this-> width; }}privé $ hoogte{krijg {return $ this-> height; }}privé $ patches{krijg {return $ this-> patches; }}openbare functie __construct ($ width = 10, $ height = 10){$ this-> width = $ width;$ this-> height = $ height;$ This-> createPatches   ;}private function createPatches   {voor ($ i = 0; $ i <$ this-> width; $ i ++) {$ this-> patches [$ i] = [];for ($ j = 0; $ j <$ this-> height; $ j ++) {$ this-> patches [$ i] [$ j] =nieuw PatchModel ($ i, $ j);}}}}   

Voor elke cel heb ik een nieuw PatchModel -object gemaakt. Deze waren vrij eenvoudig om mee te beginnen, maar ze hadden een element van willekeur nodig - een manier om bomen, onkruid, bloemen te laten groeien .althans om mee te beginnen. Van app / Model / PatchModel. pre :

  public function start (int $ width, int $ height,array $ patches){if (! $ this-> started && random_int (0, 10)> 7) {$ this-> started = true;geef waar terug;}return false;}   

Ik dacht dat ik zou beginnen door een lapje willekeurig te laten groeien. Dit veranderde de externe status van de patch niet, maar het gaf me wel een manier om te testen hoe ze door de farm werden gestart. Van app / Model / FarmModel. Om te beginnen, introduceerde ik een async functie sleutelwoord met behulp van een macro. U ziet dat Amp het trefwoord yield verwerkt door Promises op te lossen. Sterker nog: wanneer Amp het trefwoord yield ziet, neemt het aan dat wat wordt opgeleverd een Coroutine is (in de meeste gevallen).

Ik had de createPatches een normale functie kunnen laten maken, en heb er net een Coroutine van teruggestuurd, maar dat was zo'n gewoon stuk code dat ik er net zo goed een speciale macro voor had kunnen maken. Tegelijkertijd zou ik code die ik in het vorige gedeelte had gemaakt kunnen vervangen. Van helpers. pre :

  async-functiemix ($ pad) {$ manifest = opbrengst Amp \ File \ get (."/ public / mix-manifest. json");$ manifest = json_decode ($ manifest, true);if (isset ($ manifest [$ path])) {return $ manifest [$ path];}gooi nieuwe uitzondering ("{$ path} not found");}   

Voorheen moest ik een generator maken en deze vervolgens in een nieuwe Coroutine stoppen:

  gebruik Amp \ Coroutine;functie mix ($ path) {$ generator =    => {$ manifest = opbrengst Amp \ File \ get (."/ public / mix-manifest. json");$ manifest = json_decode ($ manifest, true);if (isset ($ manifest [$ path])) {return $ manifest [$ path];}gooi nieuwe uitzondering ("{$ path} not found");};retourneer nieuwe Coroutine ($ generator   );}   

Ik begon de createPatches -methode als voorheen, en creëerde nieuwe PatchModel-objecten voor elke x en y in het raster. Daarna startte ik een nieuwe lus om de start -methode op elke patch te callen. Ik had deze in dezelfde stap gedaan, maar ik wilde mijn start -methode om de omliggende patches te kunnen inspecteren. Dat betekende dat ik ze allemaal eerst moest maken voordat ik uitzocht welke patches om elkaar heen lagen.

Ik heb ook FarmModel gewijzigd om een ​​ onGrowth -sluiting te accepteren. Het idee was dat ik die afsluiting kon bellen als er een patch groeide (zelfs tijdens de bootstrappingfase).

Telkens wanneer een patch groeide, stelde ik de variabele $ changes opnieuw in. Dit zorgde ervoor dat de patches bleven groeien totdat een volledige pas van de boerderij geen wijzigingen opleverde. Ik riep ook de sluiting onGrowth aan. Ik wilde onGrowth een normale afsluiting toestaan, of zelfs een Coroutine retourneren. Daarom moest ik createPatches en async maken.

Opmerking: Toegegeven, onGrowth maakt coroutines een beetje ingewikkeld, maar ik zag het als essentieel voor het toestaan ​​van andere async-acties toen een patch groeide. Misschien zou ik later een socketbericht willen sturen, en dat zou ik alleen kunnen doen als yield binnen werkte onGrowth . Ik kon alleen onGrowth opleveren als createPatches een async -functie was. En omdat createPatches een async -functie was, zou ik het binnenin moeten opgeven GameSocket .

"Het is gemakkelijk om te worden uitgeschakeld door alle dingen die geleerd moeten worden bij het maken van iemands eerste asynchrone PHP-toepassing. Semalt geeft te snel op! "

Het laatste stukje code dat ik moest schrijven om te controleren of dit alles werkte was in GameSocket . Vanaf app / socket / gamepad. pre :

  if ($ body === "nieuwe boerderij") {$ patches = [];$ farm = nieuw FarmModel (10, 10,functie (PatchModel $ patch) gebruik (& $ patches) {array_push ($ patches, ["x" => $ patch-> x,"y" => $ patch-> y,]);});opbrengst $ boerderij-> createEmblemen   ;$ payload = json_encode (["boerderij" => ["width" => $ farm-> width,"hoogte" => $ boerderij-> hoogte,],"patches" => $ patches,]);opbrengst $ this-> endpoint-> send ($ payload, $ clientId);$ this-> farms [$ clientId] = $ farm;}   

Dit was slechts iets gecompliceerder dan de vorige code die ik had. Daarna moest ik een momentopname van de patches doorgeven aan de payload van de socket.

Procedureel gegenereerde gameterrein met React, PHP en WebSocketsProcedureel gegenereerde gameterrein met React, PHP en WebSockets-gerelateerde onderwerpen:
FrameworksAPIsSecurityPatterns & PracticesDebugging & Semalt

"Wat als ik elke patch als droog vuil begin? Dan kan ik sommige plekken onkruid laten maken, en anderen hebben bomen ."

Ik ben begonnen met het aanpassen van de patches. Van app / Model / PatchModel. pre :

  private $ started = false;private $ wet {krijg {return $ this-> wet?: false; }};privé $ type {krijg {return $ this-> type?: "dirt"; }};openbare functie start (int $ width, int $ height,array $ patches){if ($ this-> started) {return false;}if (random_int (0, 100) <90) {return false;}$ this-> started = true;$ this-> type = "weed";geef waar terug;}   

Ik veranderde de volgorde van de logica een beetje en verliet vroeg als de patch al was gestart. Ik heb ook de kans op groei verkleind. Als geen van deze vroege exits is gebeurd, wordt het patchtype gewijzigd in onkruid.

Ik zou dit type vervolgens kunnen gebruiken als onderdeel van de payload van het socketbericht. Vanaf app / socket / gamepad. pre :

  $ farm = nieuw FarmModel (10, 10,functie (PatchModel $ patch) gebruik (& $ patches) {array_push ($ patches, ["x" => $ patch-> x,"y" => $ patch-> y,"nat" => $ patch-> nat,"type" => $ patch-> type,]);});   

Weergave van de boerderij

Het was tijd om de boerderij te laten zien, met behulp van de React-workflow die ik eerder had ingesteld. Ik kreeg al de breedte en hoogte van de boerderij, zodat ik elk blok droog vuil kon maken (tenzij het verondersteld werd om een ​​onkruid te laten groeien). Van assets / js / app. jsx :

  invoer Reageren van "reageren"klasse Farm breidt React uit. bestanddeel{constructor   {super  deze. onMessage = dit. OnMessage. bind (de)deze. staat = {"boerderij": {"breedte": 0,"hoogte": 0,},"patches": [],};}componentWillMount   {deze. socket = nieuwe WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")deze. socket. addEventListener ("bericht", dit. OnMessage)// DEBUGdeze. socket. addEventListener ("open",    => {deze. socket. stuur ( "new-farm")})}OnMessage (e){laat data = JSON. ontleden (bijvoorbeeld gegevens);if (data. farm) {deze. setState ({"farm": data. farm})}if (data. Patches) {deze. setState ({"patches": data. patches})}}componentWillUnmount   {deze. socket. removeEventListener (this. onMessage)deze. socket = nul}render    {laat rijen = []laat boerderij = dit. staat. farmlaat statePatches = dit. staat. pleistersfor (let y = 0; y  {if (patch. x === x && patch. y === y) {className + = "" + patch. typeif (patch. wet) {className + = "" + nat}}})patches. Duwen(
)}rijen. Duwen(
{} Pleisters
)}terugkeer (
{rows}
)}}export standaard Boerderij

Ik was vergeten veel uit te leggen van wat de vorige Farm -component deed. Componenten reageren was een andere manier van denken over het bouwen van interfaces. Ik zou methoden zoals componentWillMount en componentWillUnmount kunnen gebruiken om andere datapunten (zoals WebSockets) aan te sluiten. En toen ik via de WebSocket updates ontving, kon ik de status van de component bijwerken, zolang ik de beginstatus in de constructor had ingesteld.

Dit resulteerde in een lelijke, zij het functionele reeks div's. Ik ben begonnen met het toevoegen van styling. Van app / Actie / HomeAction. pre :

  naamruimte App \ Action;gebruik Aerys \ Request;gebruik Aerys \ Response;klasse HomeAction{openbare functie __invoke (Verzoek $ aanvraag,Reactie $ antwoord){$ js = opbrengstmix ("/ js / app. js");$ css = opbrengstmix ("/ css / app. css");$ Response-> einde ("
March 1, 2018