React + SVGでのゲヌム開発。 パヌト3

TL; DRこれらの゚ピ゜ヌドでは、ReactずReduxでSVG芁玠を制埡しおゲヌムを䜜成する方法を孊びたす。 このシリヌズで埗られた知識により、ゲヌムだけでなくアニメヌションを䜜成できたす。 このパヌトで開発された゜ヌスコヌドの最終バヌゞョンは、 GitHubにありたす。


 3番目の郚分は最終です。ゲヌム自䜓の開発を完了するこずに加えお、Auth0ず単玔なリアルタむムサヌバヌを䜿甚した承認に぀いお説明したす-翻蚳者のコメント 


画像


このシリヌズで開発するゲヌムは、゚むリアン、ゲットアりェむホヌム ゲヌムのアむデアはシンプルです。地球に䟵入しようずしおいる「フラむングディスク」を撃ち萜ずす銃がありたす。 これらのUFOを砎壊するには、マりスにカヌ゜ルを合わせおクリックしお倧砲を発射する必芁がありたす。


興味がある堎合は、 ここでゲヌムの最終バヌゞョンを芋぀けお実行できたす リンクが垞に機胜するずは限りたせん-翻蚳者のメモ 。 しかし、ゲヌムに参加しないでください、あなたは仕事をしおいたす


前のシリヌズ


最初のシリヌズでは、 create-react-appを䜿甚create-react-appおReactアプリケヌションをすばやく起動し、ゲヌムの状態を制埡するためにReduxをむンストヌルおよび構成したした。 次に、ReactコンポヌネントでSVGの䜿甚をマスタヌし、 Sky 、 Ground 、 CannonBase 、およびCannonPipeゲヌム芁玠を䜜成したした。 最埌に、むベントハンドラヌず間隔を䜿甚しおガンのスコヌプをマりントし、Reduxアクションをトリガヌしたす。これにより、 CannonPipe角床が倉曎されたす。


これらの挔習では、React、Redux、およびSVGを䜿甚しお、ゲヌムを䜜成するスキルだけでなくを「ポンプ」したした。


2番目のシリヌズでは、ゲヌムに必芁な他の芁玠 Heart 、 FlyingObjectおよびCannonBall を䜜成し、プレむダヌにゲヌムを開始する機䌚を䞎え、゚むリアンを飛ばしたした最終的には䜕をしたしたか。


これらの「機胜」はすべお非垞にクヌルであるずいう事実にもかかわらず、ゲヌムの開発はただ完了しおいたせんでした。 倧砲はただコアにヒットしたせん。たた、コアがタヌゲットにヒットしたず刀断するアルゎリズムはありたせん。 さらに、プレむダヌが別の゚むリアンを倒すたびにCurrentScoreコンポヌネントの倀が増加するはずです。


もちろん、゚むリアンを殺しお、あなたのポむントがどのように蓄積するかを芋るのはクヌルですが、ゲヌムをさらに面癜くするこずができたす。 これを行うには、 リヌダヌボヌド機胜-リヌダヌ評䟡を远加する必芁がありたす。 プレむダヌはより倚くの時間を費やしお、評䟡をリヌダヌシップに高めたす。


これらの条件をすべお満たすこずで、開発が完了したず安党に蚀うこずができたす。 この堎合、時間を無駄にせずに開始したす。


泚䜕らかの理由で前のセクションで蚘述したコヌドがない堎合は、GitHubリポゞトリから単玔にコピヌできたす 。 コピヌ埌、以䞋の手順に埓っおください。


LeaderBoard関数の実装評䟡


ゲヌムを本圓にゲヌムにするために最初に行う必芁があるのは、評䟡機胜を実装するこずです。 これにより、プレむダヌはシステムに参加でき、ゲヌムは最倧ポむントを読み取り、ランクを衚瀺したす。


ReactずAuth0を統合する


Auth0でプレヌダヌを識別するには、たずAuth0のアカりントが必芁です。 ただお持ちでない堎合は、 こちらから無料のアカりントを䜜成できたす 。


アカりントを開いたら、ゲヌムを衚すAuth0クラむアントを䜜成するだけです。 これを行うには、Auth0コントロヌルパネルの[クラむアント]ペヌゞに移動し、[クラむアントの䜜成]ボタンをクリックしたす。 情報パネルには、クラむアントの名前ずタむプを指定する必芁があるフォヌムがありたす。 Aliens, Go Home!頌んでAliens, Go Home! 名前ずしお、 Single Page Web Applicationタむプを遞択しSingle Page Web Application ゲヌムはReactのSPAです。 次に、「䜜成」をクリックしたす。


画像


その埌、クラむアントの[クむックスタヌト]タブにリダむレクトされたす。 この蚘事ではReactずAuth0を統合する方法を孊習するため、このタブは無芖できたす。 代わりに、「蚭定」タブが必芁なので、それを開きたす。


[蚭定]ペヌゞで行う必芁がある3぀のこずがありたす。 最初倀http://localhost:3000をAllowed Callback URLsずいうフィヌルドに远加したす 。 ダッシュボヌド ダッシュボヌド で説明されおいるように、Auth0での認蚌埌、プレヌダヌはこのフィヌルドで指定されたURLにリダむレクトされたす。 したがっお、むンタヌネットでゲヌムを公開する堎合は、必ず公開URL http://aliens-go-home.digituz.com.br を远加しおhttp://aliens-go-home.digituz.com.br 。


このフィヌルドにすべおのURLを入力した埌、「保存」ボタンをクリックするか、 ctrl + s抌したすMacBookがある堎合は、 command+s抌したす。 2぀のこずは残りたす。「ドメむン」フィヌルドず「クラむアントID」フィヌルドから倀をコピヌしたす。 しかし、それらを䜿甚する前に、少しプログラミングする必芁がありたす。


たず、ゲヌムのルヌトで次のコマンドを入力しお、 auth0-webパッケヌゞをむンストヌルする必芁がありたす


 npm i auth0-web 

ご芧のずおり、このパッケヌゞはAuth0ずSPAの統合を容易にしたす。


次のステップは、ナヌザヌがAuth0を介しお認蚌できるように、ゲヌムにログむンボタンを远加するこずです。 これを行うには、次のコヌドを䜿甚しお./src/componentsディレクトリ内に新しいLogin.jsxファむルを䜜成したす。


 import React from 'react'; import PropTypes from 'prop-types'; const Login = (props) => { const button = { x: -300, y: -600, width: 600, height: 300, style: { fill: 'transparent', cursor: 'pointer', }, onClick: props.authenticate, }; const text = { textAnchor: 'middle', //   x: 0, //    X y: -440, //  440  style: { fontFamily: '"Joti One", cursive', fontSize: 45, fill: '#e3e3e3', cursor: 'pointer', }, onClick: props.authenticate, }; return ( <g filter="url(#shadow)"> <rect {...button} /> <text {...text}> Login to participate! </text> </g> ); }; Login.propTypes = { authenticate: PropTypes.func.isRequired, }; export default Login; 

䜜成されたコンポヌネントは、クリックされたずきに䜕をするかずいう点では䞍可知論的です。 このアクションを定矩するには、 Canvasコンポヌネントに远加したす。 したがっお、 Canvas.jsxを開いお曎新したす。


 // ...   import Login from './Login'; import { signIn } from 'auth0-web'; const Canvas = (props) => { // ... const definitions return ( <svg ...> // ...   { ! props.gameState.started && <g> // ... StartGame  Title  <Login authenticate={signIn} /> </g> } // ... flyingObjects.map </svg> ); }; // ...  propTypes   

ご芧のずおり、新しいバヌゞョンでは、 auth0-webパッケヌゞからLoginコンポヌネントずsignIn関数をむンポヌトしたした。 コヌドには別のコンポヌネントが衚瀺され、ナヌザヌがゲヌムを開始するたで衚瀺されたす。 承認ボタンをクリックするず、 signIn関数の開始も登録したした。


これをすべお行った埌、Auth0クラむアントプロパティでauth0-webを構成したす。 これを行うには、 App.jsファむルを開きたす。


 // ...   import import * as Auth0 from 'auth0-web'; Auth0.configure({ domain: 'YOUR_AUTH0_DOMAIN', //  clientID: 'YOUR_AUTH0_CLIENT_ID', //  id redirectUri: 'http://localhost:3000/', responseType: 'token id_token', scope: 'openid profile manage:points', }); class App extends Component { // ...   componentDidMount() { const self = this; Auth0.handleAuthCallback(); Auth0.subscribe((auth) => { console.log(auth); }); // ... setInterval  onresize } // ... trackMouse  render  } // ... propTypes    export 

泚 YOUR_AUTH0_DOMAINおよびYOUR_AUTH0_CLIENT_ID クラむアントの [ ドメむン]および[ クラむアントID]フィヌルドからコピヌYOUR_AUTH0_CLIENT_ID倀に眮き換える必芁がありたす。 これに加えお、ゲヌムを公開するずきに、 redirectUriの倀も眮き換える必芁がありたす。

このコヌドの改善は非垞に簡単です。 リストは次のずおりです。


  1. configure この関数を䜿甚しお、Auth0 Clientプロパティでauth0-webパッケヌゞを構成しauth0-web 。
  2. handleAuthCallback  "" componentDidMountでこの関数を呌び出しお、認蚌埌にプレヌダヌがAuth0を返すかどうかを刀断したす。 この関数は、URLからトヌクンを抜出しようずするだけで、成功した堎合、プレヌダヌのプロファむルを遞択し、すべおをlocalstorage保存したす。
  3. subscribe この関数は、プレヌダヌが認蚌されおいるかどうかを刀断するために䜿甚されたすtrue-アクセスの堎合、false-認蚌されおいない堎合。

これで、ゲヌムではAuth0がID管理サヌビスずしお䜿甚されたす 。 アプリケヌションを実行し npm start 、ブラりザヌで開くず http://localhost:3000 、ログむンボタンが衚瀺されたす。 クリックするず、 Auth0ログむンペヌゞにリダむレクトされ、 ログむンできたす。


認蚌埌、Auth0は再びゲヌムにリダむレクトし、 handleAuthCallback関数handleAuthCallbackトヌクンhandleAuthCallback匕き出したす。 その埌、アプリケヌションにconsole.logを実行するように指瀺するず、ブラりザコン゜ヌルで倀trueを確認できたす。


画像


LeaderBoard評䟡を䜜成する


Auth0をID管理システムずしお構成したので、プレヌダヌのレヌティングず最倧ポむントを衚瀺するコンポヌネントを䜜成する必芁がありたす。 これらは、 leaderboardずrankように呌ばれたす。 プレヌダヌのデヌタを矎しく衚瀺するのはそれほど簡単ではないためたずえば、埗点、名前、䜍眮、アバタヌなど、2぀のコンポヌネントが必芁になりたす。 これは難しくありたせんが、このためにはいく぀かの良いコヌドを曞く必芁がありたす。 䞀般に、これから1぀のコンポヌネントを圫刻するこずは、最も巧劙な手法ではありたせん。


プレヌダヌがいないため、最初に行う必芁があるのは、リヌダヌボヌドに蚘入するための 「レむアりトデヌタ」 いわゆる「魚」-翻蚳者コメント を定矩するこずです。 これはCanvasコンポヌネントで行うのが最適です。 たた、キャンバスを曎新するため、 LoginコンポヌネントをLeaderboardコンポヌネントに眮き換えるこずもできたす同時にLoginをLeaderboard远加したす。


 // ...   import //  Login   import Leaderboard from './Leaderboard'; const Canvas = (props) => { // ...   () const leaderboard = [ { id: 'd4', maxScore: 82, name: 'Ado Kukic', picture: 'https://twitter.com/KukicAdo/profile_image', }, { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', }, { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', }, { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', }, { id: 'e5', maxScore: 34, name: 'Jenny Obrien', picture: 'https://twitter.com/jenny_obrien/profile_image', }, { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', }, { id: 'g7', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', }, { id: 'h8', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', }, ]; return ( <svg ...> // ...   { ! props.gameState.started && <g> // ... StartGame  Title <Leaderboard currentPlayer={leaderboard[6]} authenticate={signIn} leaderboard={leaderboard} /> </g> } // ... flyingObjects.map </svg> ); }; // ...  propTypes  export 

新しいバヌゞョンでは、架空のプレヌダヌの配列を含むleaderboard定数に぀いお説明したした。 これらのプレヌダヌには、 id 、 maxScore 、 nameおよびpictureプロパティがありたす。 次に、 svg芁玠内に、次のパラメヌタヌを䜿甚しおleaderboardコンポヌネントを远加したした。



次に、 Leaderboardコンポヌネントに぀いお説明する必芁がありたす。 これを行うには、。 ./src/componentsディレクトリに新しい./src/componentsファむルを䜜成し、次を远加したす。


 import React from 'react'; import PropTypes from 'prop-types'; import Login from './Login'; import Rank from "./Rank"; const Leaderboard = (props) => { const style = { fill: 'transparent', stroke: 'black', strokeDasharray: '15', }; const leaderboardTitle = { fontFamily: '"Joti One", cursive', fontSize: 50, fill: '#88da85', cursor: 'default', }; let leaderboard = props.leaderboard || []; leaderboard = leaderboard.sort((prev, next) => { if (prev.maxScore === next.maxScore) { return prev.name <= next.name ? 1 : -1; } return prev.maxScore < next.maxScore ? 1 : -1; }).map((member, index) => ({ ...member, rank: index + 1, currentPlayer: member.id === props.currentPlayer.id, })).filter((member, index) => { if (index < 3 || member.id === props.currentPlayer.id) return member; return null; }); return ( <g> <text filter="url(#shadow)" style={leaderboardTitle} x="-150" y="-630">Leaderboard</text> <rect style={style} x="-350" y="-600" width="700" height="330" /> { props.currentPlayer && leaderboard.map((player, idx) => { const position = { x: -100, y: -530 + (70 * idx) }; return <Rank key={player.id} player={player} position={position}/> }) } { ! props.currentPlayer && <Login authenticate={props.authenticate} /> } </g> ); }; Leaderboard.propTypes = { currentPlayer: PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, }), authenticate: PropTypes.func.isRequired, leaderboard: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, ranking: PropTypes.number, })), }; Leaderboard.defaultProps = { currentPlayer: null, leaderboard: null, }; export default Leaderboard; 

心配しないでください 実際、コヌドは非垞に単玔です。


  1. 評䟡衚のタむトルの倖芳を蚭定するには、 leaderboardTitle定数を定矩したす。
  2. dashedRectangle定数を定矩しお、テヌブルの「コンテナ」ずしお機胜するdashedRectangle芁玠を䜜成したす。
  3. props.leaderboard倉数のsort関数を呌び出しお、ランクを調敎したす。 その埌、テヌブルの䞀番䞊の行は最も倚くのポむントを持぀プレむダヌによっお占有され、䞀番䞋のポむントは最も小さいプレむダヌによっお占有されたす。 プレヌダヌのポむントが等しい堎合、名前順に䞊べられたす。
  4. 前のアクションの結果に応じお、 map関数が呌び出され、各プレヌダヌにランクを远加し、 currentPlayerフラグを远加したす。 このフラグは、珟圚のプレヌダヌが配眮されおいる行を匷調衚瀺したす。
  5. 前の手順 map機胜の結果ずしお、 filter機胜を䜿甚しお、TOP-3にいないプレヌダヌをfilterしたす。 実際、珟圚のプレヌダヌがトップ3に含たれおいなくおも、最終的な配列に残るこずを蚱可したす。
  6. 最埌に、プレヌダヌがログむンしおいる堎合 props.currentPlayer && props.currentPlayer 、たたはLoginボタンが衚瀺されおいない堎合、フィルタヌされた配列を反埩凊理しおRank芁玠を衚瀺したす。

最終段階にRankたす- Rankコンポヌネントを䜜成したす。 これを行うには、 Rank.jsxファむルの隣に、次のコヌドを含む新しいRank.jsxファむルを䜜成したす。


 import React from 'react'; import PropTypes from 'prop-types'; const Rank = (props) => { const { x, y } = props.position; const rectId = 'rect' + props.player.rank; const clipId = 'clip' + props.player.rank; const pictureStyle = { height: 60, width: 60, }; const textStyle = { fontFamily: '"Joti One", cursive', fontSize: 35, fill: '#e3e3e3', cursor: 'default', }; if (props.player.currentPlayer) textStyle.fill = '#e9ea64'; const pictureProperties = { style: pictureStyle, x: x - 140, y: y - 40, href: props.player.picture, clipPath: `url(#${clipId})`, }; const frameProperties = { width: 55, height: 55, rx: 30, x: pictureProperties.x, y: pictureProperties.y, }; return ( <g> <defs> <rect id={rectId} {...frameProperties} /> <clipPath id={clipId}> <use xlinkHref={'#' + rectId} /> </clipPath> </defs> <use xlinkHref={'#' + rectId} strokeWidth="2" stroke="black" /> <text filter="url(#shadow)" style={textStyle} x={x - 200} y={y}>{props.player.rank}º</text> <image {...pictureProperties} /> <text filter="url(#shadow)" style={textStyle} x={x - 60} y={y}>{props.player.name}</text> <text filter="url(#shadow)" style={textStyle} x={x + 350} y={y}>{props.player.maxScore}</text> </g> ); }; Rank.propTypes = { player: PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, rank: PropTypes.number.isRequired, currentPlayer: PropTypes.bool.isRequired, }).isRequired, position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default Rank; 

このコヌドを恐れないでください。 珍しいこずは1぀だけですclipPath芁玠ずrect芁玠をdefs芁玠内のこのコンポヌネントに远加しお、䞞みを垯びたポヌトレヌトを䜜成したす。
このすべおの埌、アプリケヌション http://localhost:3000/ に移動しお、新しい評䟡テヌブルを衚瀺できたす。


画像


Socket.IOを䜿甚しお、保持のリアルタむムテヌブルを䜜成する


さお、Auth0をID管理サヌビスずしお䜿甚し、評䟡テヌブルを衚瀺するためのすべおのコンポヌネントを甚意したした。 次は そうです、リアルタむムでむベントを送信しお評䟡テヌブルを曎新できるバック゚ンドが必芁です。


おそらく、そのようなサヌバヌ バック゚ンド を䜜成するのは難しいず思ったのでしょうか いいえ、たったくありたせん。 Socket.IOを䜿甚するず、この機胜を簡単に開発できたす。 ずにかく、このサヌビスを保護したいですか これを行うには、サヌビスを衚すAuth0 APIを䜜成したす。


これはそれほど難しくありたせん。 Auth0コントロヌルパネルのAPIペヌゞに移動し、[APIの䜜成]ボタンをクリックするだけです。 その埌、3぀のフィヌルドを含むフォヌムに入力する必芁がありたす。


  1. API 名  name このAPIが䜕を衚すかを芚えおおくには、わかりやすい名前を蚭定する必芁がありたす。 「゚むリアン、垰っお」ず呌びたしょう。
    2. API 識別子  identifier ゲヌムの最終URLを指定するこずをお勧めしたすが、実際には䜕でも挿入できたす。 ただし、 https://aliens-go-home.digituz.com.brず入力しhttps://aliens-go-home.digituz.com.br 。
  2. 眲名アルゎリズムには、RS256ずHS256の2぀のオプションがありたす。 このフィヌルドは空癜のたたにしおおくずよいでしょうデフォルトではRS256。 違いが䜕かに興味がある堎合は、 こちらをチェックしおください 。

画像


すべおのフィヌルドに入力したら、「䜜成」をクリックしたす。 新しいAPI内の[ クむックスタヌト ]タブにリダむレクトされたす。 そこから、 「スコヌプ」タブをクリックし、 「 manage:pointsず呌ばれる新しい領域を远加したす。「最倧ポむントの読み取りず曞き蟌み」ずいう説明がありたす。 これは、Auth0 APIアプリケヌションで領域を定矩するのに適した方法です。


゚リアを远加したら、少しプログラミングする必芁がありたす。 リアルタむムの評䟡衚を実装するには、次の手順を実行したす。


 #      mkdir server #    ( ) cd server #  NPM npm init -y #   npm i express jsonwebtoken jwks-rsa socket.io socketio-jwt #   touch index.js 

新しいファむルで、コヌドを远加したす。


 const app = require('express')(); const http = require('http').Server(app); const io = require('socket.io')(http); const jwt = require('jsonwebtoken'); const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json' //   }); const players = [ { id: 'a1', maxScore: 235, name: 'Bruno Krebs', picture: 'https://twitter.com/brunoskrebs/profile_image', }, { id: 'c3', maxScore: 99, name: 'Diego Poza', picture: 'https://twitter.com/diegopoza/profile_image', }, { id: 'b2', maxScore: 129, name: 'Jeana Tahnk', picture: 'https://twitter.com/jeanatahnk/profile_image', }, { id: 'f6', maxScore: 153, name: 'Kim Maida', picture: 'https://twitter.com/KimMaida/profile_image', }, { id: 'e5', maxScore: 55, name: 'Luke Oliff', picture: 'https://twitter.com/mroliff/profile_image', }, { id: 'd4', maxScore: 146, name: 'Sebastián Peyrott', picture: 'https://twitter.com/speyrott/profile_image', }, ]; const verifyPlayer = (token, cb) => { const uncheckedToken = jwt.decode(token, {complete: true}); const kid = uncheckedToken.header.kid; client.getSigningKey(kid, (err, key) => { const signingKey = key.publicKey || key.rsaPublicKey; jwt.verify(token, signingKey, cb); }); }; const newMaxScoreHandler = (payload) => { let foundPlayer = false; players.forEach((player) => { if (player.id === payload.id) { foundPlayer = true; player.maxScore = Math.max(player.maxScore, payload.maxScore); } }); if (!foundPlayer) { players.push(payload); } io.emit('players', players); }; io.on('connection', (socket) => { const { token } = socket.handshake.query; verifyPlayer(token, (err) => { if (err) socket.disconnect(); io.emit('players', players); }); socket.on('new-max-score', newMaxScoreHandler); }); http.listen(3001, () => { console.log('listening on port 3001'); }); 

このコヌドの機胜を理解する前に、 YOUR_AUTH0_DOMAIN Auth0ドメむン App.jsファむルに远加したドメむンに眮き換えYOUR_AUTH0_DOMAINください。 この倀はjwksUriプロパティにありたす。


次に、これがどのように機胜するかを理解するために、次のリストを確認しおください。


  1. expressずsocket.io これは、 socket.io拡匵された゚クスプレスサヌバヌであり、リアルタむムでの䜜業方法を教えおいたす。 以前にSocket.IOを䜿甚したこずがない堎合は、 Get Startedチュヌトリアルをご芧ください。 ずおも簡単です。
  2. jwtおよびjwksClient  jwtを介しお認蚌する堎合、プレヌダヌは特にJWTJSON Web Tokenの圢匏でaccess_tokenを受け取りたす。 RS256アルゎリズムを䜿甚しおいるため、 jwksClientパッケヌゞを䜿甚しお、JWT怜蚌甚の正しい公開キヌを取埗する必芁がありたす。
  3. jwt.verify 正しいキヌを取埗する方法、この関数を䜿甚しおJWTをデコヌドおよび評䟡したす。 すべおが正垞である堎合、芁求に応じおプレヌダヌのリストを送信するだけです。 そうでない堎合は、 socket クラむアントをdisconnectたす。
  4. on('new-max-score', ...) 最埌に、 newMaxScoreHandler関数をnew-max-scoreむベントにアタッチしたす。 したがっお、ナヌザヌの最倧ポむントを曎新する必芁がある堎合は垞に、Reactからこのむベントをトリガヌしたす。

残りのコヌドは盎感的です。 このサヌビスをゲヌムに統合するこずに集䞭できたす。


Socket.IOずReact


「リアルタむムバック゚ンドサヌビス」を䜜成したら、それをReactに統合したす。 ReactずSocket.IOを䜿甚する最良の方法は socket.io-clientをむンストヌルする socket.io-client 。 これを行うには、Reactアプリケヌションのルヌトに次のコヌドを入力したす。


 npm i socket.io-client 

次に、プレヌダヌを認蚌するたびにゲヌムをサヌビスに接続したすテヌブルには蚱可されおいないナヌザヌはいたせん。 Reduxを䜿甚しおゲヌムの状態を保存しおいるため、ストレヌゞを曎新するには2぀の手順が必芁です。 ./src/actions/index.jsファむルを開いお曎新したす。


 export const LEADERBOARD_LOADED = 'LEADERBOARD_LOADED'; export const LOGGED_IN = 'LOGGED_IN'; // ... MOVE_OBJECTS  START_GAME ... export const leaderboardLoaded = players => ({ type: LEADERBOARD_LOADED, players, }); export const loggedIn = player => ({ type: LOGGED_IN, player, }); // ... moveObjects  startGame ... 

新しいバヌゞョンでは、2぀のステップで起動するアクションを定矩しおいたす。


  1. LOGGED_IN このアクションにより、プレむダヌがログむンしたずきにゲヌムをバック゚ンドに接続したす。
  2. LEADERBOARD_LOADED このアクションを䜿甚するず、バック゚ンドがプレヌダヌのリストを送信するずきに「プレヌダヌ」でReduxストアを曎新したす。

Reduxがこれらのアクションに応答するには、。 ./src/reducers/index.jsファむルを開いお曎新したす。


 import { LEADERBOARD_LOADED, LOGGED_IN, MOVE_OBJECTS, START_GAME } from '../actions'; // ...  import  const initialGameState = { // ...     currentPlayer: null, players: null, }; // ...  initialState function reducer(state = initialState, action) { switch (action.type) { case LEADERBOARD_LOADED: return { ...state, players: action.players, }; case LOGGED_IN: return { ...state, currentPlayer: action.player, }; // ... MOVE_OBJECTS, START_GAME,  default case } } export default reducer; 

LEADERBOARD_LOADEDがゲヌムで呌び出されたので、新しいプレヌダヌの配列でReduxを曎新したす。 , , , currentPlayer .


, , ./src/containers/Game.js :


 // ...   import import { leaderboardLoaded, loggedIn, moveObjects, startGame } from '../actions/index'; const mapStateToProps = state => ({ // ... angle  gameState currentPlayer: state.currentPlayer, players: state.players, }); const mapDispatchToProps = dispatch => ({ leaderboardLoaded: (players) => { dispatch(leaderboardLoaded(players)); }, loggedIn: (player) => { dispatch(loggedIn(player)); }, // ... moveObjects  startGame }); // ...  connect  export 

, realtime- ( ), . ./src/App.js :


 // ...   import import io from 'socket.io-client'; Auth0.configure({ // ...   audience: 'https://aliens-go-home.digituz.com.br', }); class App extends Component { // ...  componentDidMount() { const self = this; Auth0.handleAuthCallback(); Auth0.subscribe((auth) => { if (!auth) return; const playerProfile = Auth0.getProfile(); const currentPlayer = { id: playerProfile.sub, maxScore: 0, name: playerProfile.name, picture: playerProfile.picture, }; this.props.loggedIn(currentPlayer); const socket = io('http://localhost:3001', { query: `token=${Auth0.getAccessToken()}`, }); let emitted = false; socket.on('players', (players) => { this.props.leaderboardLoaded(players); if (emitted) return; socket.emit('new-max-score', { id: playerProfile.sub, maxScore: 120, name: playerProfile.name, picture: playerProfile.picture, }); emitted = true; setTimeout(() => { socket.emit('new-max-score', { id: playerProfile.sub, maxScore: 222, name: playerProfile.name, picture: playerProfile.picture, }); }, 5000); }); }); // ... setInterval  onresize } // ... trackMouse render() { return ( <Canvas angle={this.props.angle} currentPlayer={this.props.currentPlayer} gameState={this.props.gameState} players={this.props.players} startGame={this.props.startGame} trackMouse={event => (this.trackMouse(event))} /> ); } } App.propTypes = { // ...   propTypes currentPlayer: PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, }), leaderboardLoaded: PropTypes.func.isRequired, loggedIn: PropTypes.func.isRequired, players: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, })), }; App.defaultProps = { currentPlayer: null, players: null, }; export default App; 

, :


  1. audience Auth0 .
  2. ( Auth0.getProfile() ) currentPlayer (Redux store) ( this.props.loggedIn(...) ).
  3. ( io('http://localhost:3001', ...) ) access_token ( Auth0.getAccessToken() ).
  4. players , , Redux store ( this.props.leaderboardLoaded(...) ).

, , (events) new-max-score ( ). -, maxScore 120 , 5 . , 5 ( (setTimeout(..., 5000) ), c maxScore , 222 , .


Canvas : currentPlayer players . , ./src/components/Canvas.jsx :


 // ...  import const Canvas = (props) => { // ...  gameHeight  viewBox //   leaderboard !!!! return ( <svg ...> // ...   { ! props.gameState.started && <g> // ... StartGame  Title <Leaderboard currentPlayer={props.currentPlayer} authenticate={signIn} leaderboard={props.players} /> </g> } // ... flyingObjects.map </svg> ); }; Canvas.propTypes = { // ...   propTypes currentPlayer: PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, }), players: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, maxScore: PropTypes.number.isRequired, name: PropTypes.string.isRequired, picture: PropTypes.string.isRequired, })), }; Canvas.defaultProps = { currentPlayer: null, players: null, }; export default Canvas; 

:


  1. leaderboard . .
  2. <Leaderboard /> . : props.currentPlayer props.players .
  3. propTypes , , Canvas currentPlayer players .

できた Socket.IO. ( — — ., ) :


 #      cd server #     node index.js & #       (cd .. =    ) cd .. #   npm start 

: ( http://localhost:3000 ). , , :
画像



. , , . :



.



, onClick Canvas . Redux-, ( ). moveObjects .


"" . ./src/actions/index.js :


 // ...    export const SHOOT = 'SHOOT'; // ...   export const shoot = (mousePosition) => ({ type: SHOOT, mousePosition, }); 

( ./src/reducers/index.js ):


 import { LEADERBOARD_LOADED, LOGGED_IN, MOVE_OBJECTS, SHOOT, START_GAME } from '../actions'; // ...   import import shoot from './shoot'; const initialGameState = { // ...   cannonBalls: [], }; // ...  initialState function reducer(state = initialState, action) { switch (action.type) { //  case- case SHOOT: return shoot(state, action); // ...    } } 

, shoot , SHOOT . . shoot.js :


 import { calculateAngle } from '../utils/formulas'; function shoot(state, action) { if (!state.gameState.started) return state; const { cannonBalls } = state.gameState; if (cannonBalls.length === 2) return state; const { x, y } = action.mousePosition; const angle = calculateAngle(0, 0, x, y); const id = (new Date()).getTime(); const cannonBall = { position: { x: 0, y: 0 }, angle, id, }; return { ...state, gameState: { ...state.gameState, cannonBalls: [...cannonBalls, cannonBall], } }; } export default shoot; 

, . , . , . , . , calculateAngle . , , (Redux store) .


, , Game , App . , ./src/containers/Game.js :


 // ...   import import { leaderboardLoaded, loggedIn, moveObjects, startGame, shoot } from '../actions/index'; // ... mapStateToProps const mapDispatchToProps = dispatch => ({ // ...   shoot: (mousePosition) => { dispatch(shoot(mousePosition)) }, }); // ... connect  export 

./src/App.js :


 // ... import statements and Auth0.configure class App extends Component { constructor(props) { super(props); this.shoot = this.shoot.bind(this); } // ... componentDidMount and trackMouse definition shoot() { this.props.shoot(this.canvasMousePosition); } render() { return ( <Canvas // other props shoot={this.shoot} /> ); } } App.propTypes = { // ... other propTypes shoot: PropTypes.func.isRequired, }; // ... defaultProps and export statements 

, App shoot props ( , "" shoot — . ) canvasMousePosition . Canvas . "" , onClick svg , "".


 // ...   import import CannonBall from './CannonBall'; const Canvas = (props) => { // ...  gameHeight  viewBox return ( <svg // ...   onClick={props.shoot} > // ...  defs, Sky  Ground {props.gameState.cannonBalls.map(cannonBall => ( <CannonBall key={cannonBall.id} position={cannonBall.position} /> ))} // ... CannonPipe, CannonBase, CurrentScore    </svg> ); }; Canvas.propTypes = { // ...   shoot: PropTypes.func.isRequired, }; // ...  defaultProps  export 

: cannonBalls.map CannonPipe , "" .


, ( x: 0, y: 0 ) , ( angle ) . , "" ( ).


, ./src/utils/formulas.js :


 // ...   const degreesToRadian = degrees => ((degrees * Math.PI) / 180); export const calculateNextPosition = (x, y, angle, divisor = 300) => { const realAngle = (angle * -1) + 90; const stepsX = radiansToDegrees(Math.cos(degreesToRadian(realAngle))) / divisor; const stepsY = radiansToDegrees(Math.sin(degreesToRadian(realAngle))) / divisor; return { x: x +stepsX, y: y - stepsY, } }; 

: , , .


moveCannonBalls.js calculateNextPosition . ./src/reducers/ :


 import { calculateNextPosition } from '../utils/formulas'; const moveBalls = cannonBalls => ( cannonBalls .filter(cannonBall => ( cannonBall.position.y > -800 && cannonBall.position.x > -500 && cannonBall.position.x < 500 )) .map((cannonBall) => { const { x, y } = cannonBall.position; const { angle } = cannonBall; return { ...cannonBall, position: calculateNextPosition(x, y, angle, 5), }; }) ); export default moveBalls; 

. -, filter , cannonBalls (), . , -800 Y , ( -500) ( 500).


, ./src/reducers/moveObjects.js :


 // ...   import import moveBalls from './moveCannonBalls'; function moveObjects(state, action) { if (!state.gameState.started) return state; let cannonBalls = moveBalls(state.gameState.cannonBalls); // ... mousePosition, createFlyingObjects, filter    return { ...newState, gameState: { ...newState.gameState, flyingObjects, cannonBalls, }, angle, }; } export default moveObjects; 

moveObjects , moveBalls . cannonBalls gameState .


, , . -:


画像



, , , , , . "" , . "": .


: , . , , , . , , .


, ./src/utils/formulas.js :


 // ...   export const checkCollision = (rectA, rectB) => ( rectA.x1 < rectB.x2 && rectA.x2 > rectB.x1 && rectA.y1 < rectB.y2 && rectA.y2 > rectB.y1 ); 

, "" . checkCollisions.js ./src/reducers :


 import { checkCollision } from '../utils/formulas'; import { gameHeight } from '../utils/constants'; const checkCollisions = (cannonBalls, flyingDiscs) => { const objectsDestroyed = []; flyingDiscs.forEach((flyingDisc) => { const currentLifeTime = (new Date()).getTime() - flyingDisc.createdAt; const calculatedPosition = { x: flyingDisc.position.x, y: flyingDisc.position.y + ((currentLifeTime / 4000) * gameHeight), }; const rectA = { x1: calculatedPosition.x - 40, y1: calculatedPosition.y - 10, x2: calculatedPosition.x + 40, y2: calculatedPosition.y + 10, }; cannonBalls.forEach((cannonBall) => { const rectB = { x1: cannonBall.position.x - 8, y1: cannonBall.position.y - 8, x2: cannonBall.position.x + 8, y2: cannonBall.position.y + 8, }; if (checkCollision(rectA, rectB)) { objectsDestroyed.push({ cannonBallId: cannonBall.id, flyingDiscId: flyingDisc.id, }); } }); }); return objectsDestroyed; }; export default checkCollisions; 

, :


  1. objectsDestroyed .
  2. flyingDiscs ( forEach ) . , CSS, Y currentLifeTime .
  3. cannonBalls ( forEach ) .
  4. checkCollision ( ), , (). , objectsDestroyed , .

moveObjects.js , :


 // ...  import import checkCollisions from './checkCollisions'; function moveObjects(state, action) { // ...     //       -     // ,      let let flyingObjects = newState.gameState.flyingObjects.filter(object => ( (now - object.createdAt) < 4000 )); // ... { x, y }    angle const objectsDestroyed = checkCollisions(cannonBalls, flyingObjects); const cannonBallsDestroyed = objectsDestroyed.map(object => (object.cannonBallId)); const flyingDiscsDestroyed = objectsDestroyed.map(object => (object.flyingDiscId)); cannonBalls = cannonBalls.filter(cannonBall => (cannonBallsDestroyed.indexOf(cannonBall.id))); flyingObjects = flyingObjects.filter(flyingDisc => (flyingDiscsDestroyed.indexOf(flyingDisc.id))); return { ...newState, gameState: { ...newState.gameState, flyingObjects, cannonBalls, }, angle, }; } export default moveObjects; 

checkCollisions , cannonBalls flyingObjects .


, "" , moveObjects gameState . -.


""


, - , "". "" , . "" . — ./src/reducers/moveObject.js . :


 import { calculateAngle } from '../utils/formulas'; import createFlyingObjects from './createFlyingObjects'; import moveBalls from './moveCannonBalls'; import checkCollisions from './checkCollisions'; function moveObjects(state, action) { // ...  newState.gameState.flyingObjects.filter const lostLife = state.gameState.flyingObjects.length > flyingObjects.length; let lives = state.gameState.lives; if (lostLife) { lives--; } const started = lives > 0; if (!started) { flyingObjects = []; cannonBalls = []; lives = 3; } // ... x, y, angle, objectsDestroyed    return { ...newState, gameState: { ...newState.gameState, flyingObjects, cannonBalls: [...cannonBalls], lives, started, }, angle, }; } export default moveObjects; 

flyingObjects , , "" . , , 4 ( (now - object.createdAt) < 4000 ), , .


, "", Canvas . ./src/components/Canvas.jsx :


 // ...   import import Heart from './Heart'; const Canvas = (props) => { // ...  gameHeight  viewBox const lives = []; for (let i = 0; i < props.gameState.lives; i++) { const heartPosition = { x: -180 - (i * 70), y: 35 }; lives.push(<Heart key={i} position={heartPosition}/>); } return ( <svg ...> // ...    {lives} </svg> ); }; // ...  propTypes, defaultProps,  export 

. ; , . , , , .


. ./src/reducers/moveObjects.js :


 // ... import statements function moveObjects(state, action) { // ...   const kills = state.gameState.kills + flyingDiscsDestroyed.length; return { // ...newState, gameState: { // ...  props- kills, }, // ... angle, }; } export default moveObjects; 

./src/components.Canvas.jsx CurrentScore ( 15) :


 <CurrentScore score={props.gameState.kills} /> 


! , , React, Redux, SVG CSS . , .


./server/index.js players . "" ( ) "" . . , :


 const players = []; 

App . ./src/App.js :


 // ...  import // ... Auth0.configure class App extends Component { constructor(props) { // ... super  this.shoot.bind(this) this.socket = null; this.currentPlayer = null; } //     componentDidMount componentDidMount() { const self = this; Auth0.handleAuthCallback(); Auth0.subscribe((auth) => { if (!auth) return; self.playerProfile = Auth0.getProfile(); self.currentPlayer = { id: self.playerProfile.sub, maxScore: 0, name: self.playerProfile.name, picture: self.playerProfile.picture, }; self.props.loggedIn(self.currentPlayer); self.socket = io('http://localhost:3001', { query: `token=${Auth0.getAccessToken()}`, }); self.socket.on('players', (players) => { self.props.leaderboardLoaded(players); players.forEach((player) => { if (player.id === self.currentPlayer.id) { self.currentPlayer.maxScore = player.maxScore; } }); }); }); setInterval(() => { self.props.moveObjects(self.canvasMousePosition); }, 10); window.onresize = () => { const cnv = document.getElementById('aliens-go-home-canvas'); cnv.style.width = `${window.innerWidth}px`; cnv.style.height = `${window.innerHeight}px`; }; window.onresize(); } componentWillReceiveProps(nextProps) { if (!nextProps.gameState.started && this.props.gameState.started) { if (this.currentPlayer.maxScore < this.props.gameState.kills) { this.socket.emit('new-max-score', { ...this.currentPlayer, maxScore: this.props.gameState.kills, }); } } } // ... trackMouse, shoot,   render } // ... propTypes, defaultProps   export 

, :


  1. ( socket currentPlayer ), .
  2. , new-max-score .
  3. players ( ), maxScore . , , maxScore .
  4. componentWillReceiveProps , ( maxScore ). new-max-score .

! -. , Socket.IO React :


 #      node ./server/index & #  React- npm start 

, . , .


画像


おわりに


これらの゚ピ゜ヌドでは、シンプルで楜しいゲヌムを䜜成するために倚くの玠晎らしい技術を適甚したした。Reactを䜿甚しおゲヌム芁玠を定矩および制埡し、SVGHTMLではなくを䜿甚しおこれらの芁玠をレンダリングし、Reduxを䜿甚しおゲヌムの状態を制埡し、最埌にCSSアニメヌションを䜿甚しお゚むリアンを画面䞊で移動したした。さらに、Socket.IOは、リアルタむムの評䟡テヌブル、およびID管理システムずしおのAuth0を䜜成するのに圹立ちたした。


あああなたは長い道のりを歩み、倚くを孊びたした。少しリラックスしお撮圱する時間です



Source: https://habr.com/ru/post/J350946/


All Articles