Back to Question Center
0

Terreno di gioco proceduralmente generato con React, PHP e WebSockets            Argomenti correlati: Terreno di gioco proceduralmente generato con React, PHP e WebSockets Argomenti correlati: FrameworksAPIsSecurityPatterns & PracticesDebugging &

1 answers:
Terreno di gioco proceduralmente generato con React, PHP e WebSockets

Sviluppo di giochi con PHP e ReactJS

  • Sviluppo di giochi con React e PHP: quanto sono compatibili?
  • Terreno di gioco proceduralmente generato con React, PHP e WebSockets

Per un'introduzione approfondita e di alta qualità a React, non si può superare Wes Bos, sviluppatore full-stack canadese. Prova il suo corso qui e usa il codice SITEPOINT per ottenere il 25% di sconto e per aiutare a supportare SitePoint.

L'ultima volta, ho iniziato a raccontarti la storia di come volevo fare un gioco. Ho descritto come ho configurato il server PHP asincrono, la catena di build Laravel Mix, il front-end React e WebSockets che collegano tutto questo insieme. Ora, lascia che ti dica cosa è successo quando ho iniziato a costruire le meccaniche di gioco con questo mix di React, PHP e WebSockets - employee time clock software solutions.


Il codice per questa parte può essere trovato su github. com / assertchris-tutorial / SitePoint-making-giochi / albero / parte-2. L'ho provato con PHP 7. 1 , in una versione recente di Google Chrome.


Terreno di gioco proceduralmente generato con React, PHP e WebSocketsArgomenti correlati: Terreno di gioco proceduralmente generato con React, PHP e WebSockets Argomenti correlati:
FrameworksAPIsSecurityPatterns & PracticesDebugging & Semalt

Fare una fattoria

"Semalt start simple. Abbiamo una griglia di piastrelle 10 per 10, piena di materiale generato casualmente. "

Ho deciso di rappresentare la fattoria come fattoria e ogni tessera come cerotto . Da app / Modello / FarmModel. pre :

  namespace App \ Model;fattoria di classe{$ larghezza privata{ottieni {return $ this-> width; }}altezza $ privata{ottieni {return $ this-> height; }}funzione pubblica __construct (int $ width = 10,int $ height = 10){$ this-> width = $ width;$ this-> height = $ height;}}   

Ho pensato che sarebbe stato un momento divertente provare la macro degli accessors di classe dichiarando proprietà private con getter pubblici. Per questo ho dovuto installare pre / class-accessors (via compositore require ).

Ho quindi modificato il codice socket per consentire la creazione di nuove farm su richiesta. Da app / Socket / GameSocket. pre :

  namespace App \ Socket;usa Aerys \ Request;utilizzare Aerys \ Response;usa Aerys \ Websocket;utilizzare Aerys \ Websocket \ Endpoint;usa Aerys \ Websocket \ Message;utilizzare App \ Model \ FarmModel;la classe GameSocket implementa Websocket{private $ farms = [];funzione pubblica onData (int $ clientId,Messaggio $ messaggio){$ body = yield $ message;if ($ body === "new-farm") {$ farm = new FarmModel   ;$ payload = json_encode (["farm" => ["width" => $ farm-> width,"height" => $ farm-> height,],]);rendi $ this-> endpoint-> send ($ payload, $ clientId);$ this-> farms [$ clientId] = $ farm;}}funzione pubblica onClose (int $ clientId,int $ code, stringa $ ragione){unset ($ this-> connessioni [$ clientid]);unset ($ this-> aziende agricole [$ clientid]);}// .}   

Ho notato quanto questo GameSocket fosse simile a quello precedente che avevo - tranne, invece di trasmettere un'eco, stavo controllando per new-farm e inviando un messaggio solo indietro al cliente che aveva chiesto.

"Forse è un buon momento per diventare meno generici con il codice React. Ho intenzione di rinominare componente. jsx a fattoria. jsx . "

Da attività / js / azienda agricola. jsx :

  importa Reagire da "reagire"la classe Farm estende React. socket = new WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")Questo. zoccolo. addEventListener ("messaggio", questo. onMessage)// DEBUGQuesto. zoccolo. addEventListener ("open",    => {Questo. zoccolo. inviare ( "new-farm")})}}esporta fattoria predefinita   

In effetti, l'unica altra cosa che ho cambiato è stata l'invio new-farm anziché Hello World . Tutto il resto era lo stesso. Ho dovuto cambiare l'app . jsx codice però. Da assets / js / app. jsx :

  importa Reagire da "reagire"importare ReactDOM da "react-dom"import Farm da ". / farm"ReactDOM. render (,documento. querySelector (". app"))   

Era lontano da dove dovevo essere, ma usando queste modifiche ho potuto vedere gli accessor di classe in azione, oltre a prototipare una sorta di modello di richiesta / risposta per le future interazioni WebSocket. Ho aperto la console e ho visto {"farm": {"width": 10, "height": 10}} .

"Grande!"

Quindi ho creato una classe Patch per rappresentare ogni piastrella. Ho pensato che questo fosse il luogo dove accadrebbe un sacco di logica del gioco. Da app / Model / PatchModel. pre :

  namespace App \ Model;classe PatchModel{$ privato privato{ottieni {return $ this-> x; }}privato $ y{ottenere {return $ this-> y; }}funzione pubblica __construct (int $ x, int $ y){$ this-> x = $ x;$ this-> y = $ y;}}   

Avrei bisogno di creare tante patch quante sono gli spazi in una nuova Farm . Potrei farlo come parte della costruzione di FarmModel . Da app / Modello / FarmModel. pre :

  namespace App \ Model;classe FarmModel{$ larghezza privata{ottieni {return $ this-> width; }}altezza $ privata{ottieni {return $ this-> height; }}patch $ private{ottenere {return $ this-> patches; }}funzione pubblica __construct ($ width = 10, $ height = 10){$ this-> width = $ width;$ this-> height = $ height;$ This-> createPatches   ;}funzione privata createPatches   {for ($ i = 0; $ i <$ this-> width; $ i ++) {$ this-> patches [$ i] = [];for ($ j = 0; $ j <$ this-> height; $ j ++) {$ this-> patches [$ i] [$ j] =nuovo PatchModel ($ i, $ j);}}}}   

Per ogni cella, ho creato un nuovo oggetto PatchModel . All'inizio erano abbastanza semplici, ma avevano bisogno di un elemento di casualità - un modo per far crescere alberi, erbe infestanti, fiori .almeno per cominciare. Da app / Model / PatchModel. pre :

  funzione pubblica start (int $ width, int $ height,array $ patch){if (! $ this-> started && random_int (0, 10)> 7) {$ this-> started = true;ritorna vero;}restituisce falso;}   

Pensavo che avrei iniziato coltivando in modo casuale una patch. Questo non ha cambiato lo stato esterno della patch, ma mi ha dato un modo per testare come sono stati avviati dalla farm. Da app / Modello / FarmModel. Per i principianti, ho introdotto una funzione chiave async utilizzando una macro. Vedete, Amp gestisce la parola chiave yield risolvendo Promises. Più precisamente: quando Amp vede la parola chiave yield , assume che cosa viene restituito è una Coroutine (nella maggior parte dei casi).

Avrei potuto fare in modo che il createPatches funzionasse con una funzione normale, e gli abbiamo appena restituito una Coroutine, ma era un pezzo di codice così comune che avrei potuto creare una macro speciale per questo. Allo stesso tempo, potrei sostituire il codice che avevo creato nella parte precedente. Da aiutanti. pre :

  mix di funzioni asincrone (percorso $) {$ manifest = yield Amp \ File \ get (."/ pubblico / mix-manifest. json");$ manifest = json_decode ($ manifest, true);if (isset ($ manifest [$ path])) {return $ manifest [$ path];}lanciare una nuova eccezione ("{$ path} non trovato");}   

Precedentemente, dovevo creare un generatore e poi avvolgerlo in un nuovo Coroutine :

  usa Amp \ Coroutine;function mix ($ path) {$ generator =    => {$ manifest = yield Amp \ File \ get (."/ pubblico / mix-manifest. json");$ manifest = json_decode ($ manifest, true);if (isset ($ manifest [$ path])) {return $ manifest [$ path];}lanciare una nuova eccezione ("{$ path} non trovato");};restituisce nuova Coroutine ($ generator   );}   

Ho iniziato il metodo createPatches come prima, creando nuovi oggetti PatchModel per ogni x e y nella griglia. Quindi ho avviato un altro ciclo, per chiamare il metodo start su ogni patch. Avrei fatto questi nello stesso passo, ma volevo che il mio metodo start fosse in grado di ispezionare le patch circostanti. Ciò significava che avrei dovuto crearli tutti per primi, prima di capire quali patch erano l'una intorno all'altra.

Ho anche modificato FarmModel per accettare una chiusura onGrowth . L'idea era che potevo chiamare quella chiusura se una patch cresceva (anche durante la fase di bootstrap).

Ogni volta che una patch è cresciuta, ho ripristinato la variabile $ changes . Ciò ha garantito che le patch continuassero a crescere fino a quando un intero passaggio della farm non ha apportato modifiche. Ho anche invocato la chiusura onGrowth . Volevo permettere a onGrowth di essere una chiusura normale, o anche di restituire una Coroutine . Ecco perché ho dovuto creare createPatches una funzione asincrona .

Nota: ammettendo, con onGrowth le coroutine hanno complicato un po 'le cose, ma l'ho visto essenziale per consentire altre azioni asincrone quando una patch è cresciuta. Forse più tardi vorrei inviare un messaggio socket, e potrei farlo solo se yield ha funzionato all'interno onGrowth . Potevo solo ottenere onGrowth se createPatches era una funzione asincrona . E poiché createPatches era una funzione asincrono , avrei dovuto renderlo all'interno di GameSocket .

"È facile essere disattivati ​​da tutte le cose che devono essere apprese quando si esegue la prima applicazione PHP asincrona. Semalt rinunciare troppo presto! "

L'ultimo bit di codice che avevo bisogno di scrivere per verificare che tutto questo funzionasse era in GameSocket . Da 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,]);});resa $ farm-> createPatches   ;$ payload = json_encode (["farm" => ["width" => $ farm-> width,"height" => $ farm-> height,],"patches" => $ patch,]);rendi $ this-> endpoint-> send ($ payload, $ clientId);$ this-> farms [$ clientId] = $ farm;}   

Questo era solo leggermente più complesso del codice precedente che avevo. Successivamente, ho solo dovuto passare un'istantanea delle patch al payload del socket.

Terreno di gioco proceduralmente generato con React, PHP e WebSocketsArgomenti correlati: Terreno di gioco proceduralmente generato con React, PHP e WebSockets Argomenti correlati:
FrameworksAPIsSecurityPatterns & PracticesDebugging & Semalt

"Cosa succede se inizio ogni patch come sporco secco? Quindi potrei fare delle patch con delle erbacce e altre avere degli alberi ."

Ho impostato la personalizzazione delle patch. Da app / Model / PatchModel. pre :

  private $ started = false;$ privato privato {ottenere {return $ this-> wet?: false; }};private $ tipo {ottenere {return $ this-> type?: "dirt"; }};funzione pubblica start (int $ width, int $ height,array $ patch){se ($ this-> started) {restituisce falso;}if (random_int (0, 100) <90) {restituisce falso;}$ this-> started = true;$ this-> type = "weed";ritorna vero;}   

Ho cambiato l'ordine della logica un po ', uscendo presto se la patch era già stata avviata. Ho anche ridotto le possibilità di crescita. Se nessuna di queste prime uscite si verificava, il tipo di patch sarebbe cambiato in erba.

Potrei quindi usare questo tipo come parte del payload del messaggio socket. Da app / Socket / GameSocket. pre :

  $ farm = new FarmModel (10, 10,function (PatchModel $ patch) use (& $ patches) {array_push ($ patches, ["x" => $ patch-> x,"y" => $ patch-> y,"wet" => $ patch-> bagnato,"type" => $ patch-> type,]);});   

Rendering della fattoria

Era tempo di mostrare la fattoria, usando il flusso di lavoro React che avevo impostato in precedenza. Stavo già ottenendo la larghezza e altezza della fattoria, così potevo fare ogni blocco di sporco secco (a meno che non si supponesse che coltivasse un'erbaccia). Da assets / js / app. jsx :

  importa Reagire da "reagire"la classe Farm estende React. Componente{costruttore  {super  Questo. onMessage = questo. onMessage. bind (questo)Questo. stato = {"azienda agricola": {"larghezza": 0,"altezza": 0,},"cerotti": [],};}componentWillMount   {Questo. socket = new WebSocket ("ws: // 127. 0. 0. 1: 8080 / ws")Questo. zoccolo. addEventListener ("messaggio", questo. onMessage)// DEBUGQuesto. zoccolo. addEventListener ("open",    => {Questo. zoccolo. inviare ( "new-farm")})}onMessage (e){let data = JSON. parse (e. dati);if (data. farm) {Questo. setState ({"farm": data. farm})}if (data. patches) {Questo. setState ({"patches: data. patches})}}componentWillUnmount   {Questo. zoccolo. removeEventListener (this. onMessage)Questo. socket = null}render    {let rows = []lascia farm = this. stato. azienda agricolalet statePatches = this. stato. cerottifor (let y = 0; y  {if (patch x === x && patch. y === y) {className + = "" + patch. generese (patch bagnato) {className + = "" + bagnato}}})cerotti. spingere(
)}filari. spingere(
{cerotti}
)}ritorno (
{rows}
)}}esporta fattoria predefinita

Mi ero dimenticato di spiegare molto di ciò che stava facendo la precedente Farm . I componenti di React erano un modo diverso di pensare a come costruire interfacce. Potrei usare metodi come componentWillMount e componentWillUnmount come modi per collegarsi ad altri punti di dati (come WebSockets). E come ho ricevuto gli aggiornamenti tramite WebSocket, ho potuto aggiornare lo stato del componente, a patto che avessi impostato lo stato iniziale nel costruttore.

Ciò ha provocato un brutto, sebbene funzionale set di div. Ho deciso di aggiungere un po 'di stile. Da app / Azione / HomeAzione. pre :

  namespace App \ Action;usa Aerys \ Request;utilizzare Aerys \ Response;classe HomeAzione{funzione pubblica __invoke (richiesta $ richiesta,Risposta $ risposta){$ js = yield mix ("/ js / app. js");$ css = yield mix ("/ css / app. css");$ Response-> end ("
March 1, 2018