- 原文地址:Developing Games with React, Redux, and SVG - Part 3
- 原文作者:Bruno Krebs
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:xueshuai
- 校對者:
使用 React, Redux, and SVG 開發遊戲 - 第 3 部分
提示: 在這個系列中,你將學習如何使用 React 和 Redux 控制一堆 SVG 元素來建立一個遊戲。這個系列所需要的知識同樣也可以使你建立使用 React 和 Redux 的其他型別的動畫,而不只是遊戲。你能夠在下面的 GitHub 倉庫中找到文章中開發的最終程式碼:Aliens Go Home - 第 3 部分
React 遊戲:Aliens, Go Home!
在這個教程中你開發的遊戲叫做 Aliens, Go Home! 這個遊戲的想法很簡單,你有一門大炮,你將必須殺掉嘗試入侵地球的飛行物體。要殺掉這些飛行的物體,你將必須標示和點選 SVG canvas 來使你的大炮發射。
如果你有些疑惑,你可以發現完成了的遊戲並在這裡執行它。但是不要玩的太多,你還有工作必須做。
“我正在用 React,Redux 和 SVG元素
建立一個遊戲。”
之前,在第一部分和第二部分
在 這個系列的第一部分,你已經使用 create-react-app
來啟動你的 React 應用,你已經安裝和配置了 Redux 來管理遊戲的狀態。之後,在建立遊戲的元素時,例如 Sky
, Ground
, CannonBase
和 CannonPipe
, 你已經學習瞭如何在 React 元件中使用 SVG。最終,你通過使用事件監聽方法給你的大炮新增動畫效果和一個 JavaScript interval 來觸發 Redux 的 action 更新 CannonBase
的角度。
這些為你提供了理解如何使用React,Redux和SVG來建立你的遊戲(和其他動畫)的方法。
在 第二部分,你已經建立了遊戲中其他的必須元素(例如 Heart
, FlyingObject
和 CannonBall
),使你的玩家能夠開始遊戲,並使用 CSS 動畫讓飛行物體飛起來(這就是他們應該做的事,對麼?)。
就算是我們有了這些非常好的特性,但是他們還沒有構成一個完整的遊戲。你仍然需要使你的大炮發射炮彈,並完成一個演算法來檢測飛行物體和炮彈的碰撞。除此之外,你必須在你的玩家殺死外星人的時候,增加 CurrentScore
。
殺死外星人和看到當前分數的增長很酷,但是你可能會使這個遊戲更有吸引力。。這就是為什麼你要在你的遊戲中增加一個排行榜特性。這將會使你的玩家花費更多的時間來達到排行榜的高位。
有了這些特性,你可以說你有了一個完整的遊戲。所以,為了節約時間,是時候關注他們了。
提示: 如果(無論是什麼原因)你沒有 前面兩部分 建立的程式碼,你可以從 這個 GitHub 倉庫 克隆他們。克隆之後,你能夠繼續跟隨接下來板塊中的指示。
在你的 React 遊戲裡實現排行榜特性
第一件你要做的使你的遊戲看起來更像一個真正的遊戲的事情就是實現排行榜特性。這個特性將使玩家能夠登陸,所以你的遊戲能夠跟蹤他們的最高分數和他們的排名。
整合 React 和 Auth0
要使 Auth0 管理你的玩家的身份,你必須有一個 Auth0 賬戶。如果你還沒有,你可以 在這裡 註冊一個免費 Auth0 賬戶。
註冊完你的賬戶之後,你只需要建立一個 Auth0 應用 來代表你的遊戲。要做這個,前往 Auth0 的儀表盤中的 Application 頁面 ,然後點選 Create Application 按鈕。儀表盤將會給你展示一個表單,你必須輸入你的應用的 name 和 type 。你能輸入 Aliens, Go Home! 作為名字,並選擇 Single Page Web Application 作為型別(畢竟你的遊戲是基於 React 的 SPA)。然後,你可以點選 Create。
當你點選這個按鈕,儀表盤將會把你重定向到你的新應用的 Quick Start 標籤頁。正如你將在這篇文章中學習如何整合 React 和 Auth0,你不需要使用這個標籤頁。取而代之的,你將需要使用 Settings 標籤頁,所以我們前往這個頁面。
這裡有三件事你需要在這個標籤頁做。第一件是新增 http://localhost:3000
到名為 Allowed Callback URLs 的欄位。正如儀表盤解釋的, 在你的玩家認證之後, Auth0 只會回跳到這個欄位 URLs 中的一個 。所以,如果你想在網路上釋出你的遊戲,不要忘了在那裡同樣加入你的外網 URL (例如 http://aliens-go-home.digituz.com.br
)。
在這個欄位輸入你所有的 URLs 之後,點選 Save 按鈕或者按下 ctrl
+ s
(如果你是用的是 MacBook,你需要按下 command
+ s
)。
你需要做的最後兩件事是複製 Domain 和 Client ID 欄位的值。不管怎樣,在你使用這些值之前,你需要敲一些程式碼。
對於初學者,你將需要在你遊戲的根目錄下輸入以下命令來安裝 auth0-web
包:
npm i auth0-web
複製程式碼
正如你將看到的,這個包將有助於整合 Auth0 和 SPAs。
下一步是在你的遊戲中增加一個登陸按鈕,使你的玩家能夠通過 Auth0\ 認證。完成這個,要在 ./src/components
目錄下建立一個名為 Login.jsx
的檔案,加入以下的程式碼:
import React from 'react';
import PropTypes from 'prop-types';
const Login = (props) => {
const button = {
x: -300, // half width
y: -600, // minus means up (above 0)
width: 600,
height: 300,
style: {
fill: 'transparent',
cursor: 'pointer',
},
onClick: props.authenticate,
};
const text = {
textAnchor: 'middle', // center
x: 0, // center relative to X axis
y: -440, // 440 up
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
檔案,參照下面更新它:
// ... other import statements
import Login from './Login';
import { signIn } from 'auth0-web';
const Canvas = (props) => {
// ... const definitions
return (
<svg ...>
// ... other elements
{ ! props.gameState.started &&
<g>
// ... StartGame and Title components
<Login authenticate={signIn} />
</g>
}
// ... flyingObjects.map
</svg>
);
};
// ... propTypes definition and export statement
複製程式碼
正如你看見的,在這個新版本里,你已經引入了 Login
元件和 auth0-web
包裡的 signIn
方法。然後,你已經把你的新元件加入到了程式碼塊中,只在玩家沒有開始遊戲的時候出現。同樣的,你已經預料到,當點選的時候,登陸按鈕一定會觸發 signIn
方法。
當這些變化發生的時候,最後一件你必須做的事是在你的 Auth0 應用的屬性中配置 auth0-web
。要做這件事,需要開啟 App.js
檔案並按照下面更新它:
// ... other import statements
import * as Auth0 from 'auth0-web';
Auth0.configure({
domain: 'YOUR_AUTH0_DOMAIN',
clientID: 'YOUR_AUTH0_CLIENT_ID',
redirectUri: 'http://localhost:3000/',
responseType: 'token id_token',
scope: 'openid profile manage:points',
});
class App extends Component {
// ... constructor definition
componentDidMount() {
const self = this;
Auth0.handleAuthCallback();
Auth0.subscribe((auth) => {
console.log(auth);
});
// ... setInterval and onresize
}
// ... trackMouse and render functions
}
// ... propTypes definition and export statement
複製程式碼
提示: 你必須使用從你的 Auth0 應用中複製的 Domain 和 Client ID 欄位的值來替換
YOUR_AUTH0_DOMAIN
和YOUR_AUTH0_CLIENT_ID
。除此之外,當你在網路上釋出你的遊戲的時候,你同樣需要替換redirectUri
的值。
這個檔案裡的增強的點十分簡單。這個列表總結了他們:
configure
:你使用這個函式,協同你的 Auth0 應用的屬性,來配置auth0-web
包。handleAuthCallback
:你在componentDidMount
生命週期的鉤子函式 觸發這個方法,來檢測使用者是否是經過 Auth0 認證的。 這個方法只是嘗試從 URL 抓取 tokens,並且如果成功,抓取使用者的文件並把所有的資訊儲存到localstorage
。subscribe
:你使用這個方法來來記錄玩家是否是經過認證的(true
認證過,false
代表其他)。
就是這樣,你的遊戲已經 使用 Auth0 作為它的身份管理服務。如果你現在啟動你的應用(npm start
)並且在你的瀏覽器中瀏覽 (http://localhost:3000
),你講看到登陸按鈕。點選它,它會把你重定向到 Auth0 登陸頁面,在這裡你可以登陸。
當你完成了流程中的註冊,Ahth0 會再一次把你重定向到你的遊戲,handleAuthCallback
方法將會抓去你的 tokens。然後,正如你已經告訴你的應用 console.log
所有的認證狀態的變化,你將能夠看到它在你的瀏覽器控制檯列印了 true
。
“使用 Auth0 來保護你的遊戲是簡單和痛苦小的。”
建立排行榜 React 元件
現在你已經配置了 Auth0 作為你的身份管理系統,你將需要建立展示排行榜和當前玩家最大分數的元件。為這個,你將建立兩個元件:Leaderboard
和 Rank
。你將需要將這個特性拆分成兩個元件,因為正如你所看到的,友好的展示玩家的資料(比如最大分數,姓名,位置和圖片)並不是簡單的事。其實也並不困難,但是你需要編寫一些好的程式碼。所以,把所有的東西加到一個元件之中會看起來很笨拙。
正如你的遊戲還沒有任何玩家,第一件事你需要做的就是定義一些 mock 資料來填充排行榜。做這件事最好的地方就是在 Canvas
元件中。同樣,因為你正要去更新你的 canvas,你能夠繼續深入,使用 Leaderboard
替換 Login
元件(你一會兒將在 Leaderboard
中加入 Login
):
// ... other import statements
// replace Login with the following line
import Leaderboard from './Leaderboard';
const Canvas = (props) => {
// ... const definitions
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 ...>
// ... other elements
{ ! props.gameState.started &&
<g>
// ... StartGame and Title
<Leaderboard currentPlayer={leaderboard[6]} authenticate={signIn} leaderboard={leaderboard} />
</g>
}
// ... flyingObjects.map
</svg>
);
};
// ... propTypes definition and export statement
複製程式碼
在這個檔案的新版本中,你定義一個儲存假玩家的叫做 leaderboard
的陣列常量。這些玩家有以下屬性:id
,maxScore
,name
和 picture
。然後,在 svg
元素中,你增加具有以下引數的 Leaderboard
元件:
currentPlayer
: 這個定義了當前玩家的身份。現在,你正在使用之前定義的假玩家中的一個,所以你能夠看到每一件事是怎麼工作的。傳遞這個引數的目的是使你的排行榜高亮當前玩家。authenticate
: 這個和你加入到之前版本的Login
元件中的引數是一樣的。leaderboard
: 這個是家玩家的陣列列表。你的排行榜將會使用這個來展示當前的排行。
現在,你必須定義 Leaderboard
元件。要做這個,需要在 ./src/components
目錄下建立一個名為 Leaderboard.jsx
的新檔案,並且加入如下程式碼:
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;
複製程式碼
不要害怕!這個元件的程式碼非常簡單:
- 你定義常量
leaderboardTitle
來設定你的排行榜標題是什麼樣的。 - 你定義常量
dashedRectangle
來設定作為你的排行榜容器的rect
元素的樣式。 - 你呼叫
props.leaderboard
變數的sort
方法來排序。之後,你的排行榜就會使最高分在上面,最低分在下面。同樣,如果有兩個玩家打平手,你根據姓名將他們排序。 - 你在上一步(
sort
方法)的結果上呼叫map
方法,使用他們的rank
和 具有currentPlayer
的標誌來補充玩家資訊。你將使用這個標誌來高亮當前玩家出現的行。 - 你在上一步(
map
方法)的結果上呼叫filter
方法來刪除每一個不在前三名玩家的人。事實上,如果當前玩家不屬於這個篩選組,你要使當前玩家保留在最終的陣列裡。 - 最後,如果有一個使用者登陸(
props.currentPlayer && leaderboard.map
)或者正在展示Login
按鈕,你遍歷過濾過得陣列來展示Rank
元素。
最後一件你需要做的事就是建立 Rank
React component。要完成這個,建立一個名為 Rank.jsx
新檔案,同時包括具有以下程式碼的 Leaderboard.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;
複製程式碼
這個程式碼同樣沒有什麼可怕的。唯一不平常的事就是你加入到這個元件的是 clipPath
元素 和一個在 defs
元素中的 rect
元素來建立一個圓的肖像。
有了這些新檔案,你能夠前往你的應用(http://localhost:3000/
)來看看你的新排行榜特性。
使用 Socket.IO 開發一個實時排行榜
帥氣,你已經使用 Auth0 作為你的身份管理服務,並且你也建立了需要展示排行榜的元件。之後,你需要做什麼?對了,你需要一個能出發實時事件的後端來更新排行榜。
這可能使你想到:開發一個實時後端伺服器困難麼?不,不困難。使用 Socket.IO,你可以在很短的時間實現這個特性。不管怎樣,在深入之前,你可能想要好糊這個後端服務,對不對?要做這個,你需要建立一個 Auth0 API 來代表你的服務。
這樣做很簡單。前往 你的 Auth0 儀表盤的 APIs 頁面 並且點選 Create API 按鈕,Auth0 會想你展示一個有三個資訊需要填的表單:
- API的 Name :這裡,你僅僅需要宣告一個友好的名字使你不至於忘掉這個 API 代表的什麼。所以,在這個區域輸入 Aliens, Go Home! 就好啦。
- API的 Identifier :這裡建議的值是你遊戲的最終 URL,但是事實上這可以是任何東西,雖然這樣,在這裡輸入
https://aliens-go-home.digituz.com.br
。 - Signing Algorithm :這裡有兩個選項, RS256 和 HS256 。你最好不要修改這個欄位(例如,保持 RS256)。你過你想要學習他們之間的不同,檢視 這個答案。
在你填完這個表單後,點選 Create 按鈕。會將你重定向到你的新 API 中叫做 Quick Start 的標籤頁。在那裡,點選 Scopes 標籤並且新增叫做 manage:points
的新作用域,他有以下的描述:“讀和寫最大的分數”。在 Auth0 APIs 上定義作用域是很好的實踐
新增完這個作用域之後,你能夠繼續程式設計。來完成你的實時排行榜服務,按照下面的做:
# 在專案根目錄建立一個服務目錄
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');
});
複製程式碼
在學習這部分程式碼做什麼之前,使用你的 Auth0 域(和你新增到 App.js
檔案是一樣那個)替換 YOUR_AUTH0_DOMAIN
。你可以在 jwksUri
屬性值中找到這個佔位符。
現在,為了理解這個事情是怎麼工作的,檢視這個列表:
express
和socket.io
:這只是一個通過 Socket.IO 加強的 Express 伺服器來使它具備實時的特性。如果你以前沒有用過 Socket.IO,檢視他們的 Get Started 教程。它真的很簡單。jwt
和jwksClient
:當 Auth0 認證的時候,你的玩家(在其他事情之外)會在 JWT (JSON Web Token) 表單中得到一個access_token
。因為你使用 RS256 簽名演算法,你需要使用jwksClient
包來獲取正確的公鑰來認證 JWTs。你收到的 JWTs 中包含一個kid
屬性(Key ID),你可以使用這個屬性得到正確的公鑰(如果你感到困惑,你可以在這兒瞭解更多地 JWKS)。jwt.verify
:在找到正確的鑰匙之後,你可以使用這個方法來解碼和認證 JWTs。如果他們都很好,你就給請求的人傳送players
列表。如果他們沒有經過認證,你disconnect
這個socket
(使用者)。on('new-max-score', ...)
:最後,你在new-max-score
事件上附加newMaxScoreHandler
方法。因此,無論什麼時候你需要更新一個使用者的最高分,你會需要在你的 React 應用中觸發這個事件。
剩餘的程式碼非常直觀。因此,你能關注在你的遊戲中整合這個服務。
Socket.IO 和 React
在建立你的實時後端服務之後,是時候將它整合到你的 React 遊戲中了。使用 React 和 Socket.IO 最好的方式是安裝 socket.io-client
包。你可以在你的 React 應用根目錄下輸入以下命令來安裝它:
npm i socket.io-client
複製程式碼
然後,在那之後,無論什麼時候玩家認證,你將使你的遊戲連線你的服務(你不需要給沒有認證的玩家顯示排行榜)。因為你使用 Redux 來儲存遊戲的狀態,你需要兩個 actions 來保持你的 Redux 儲存最新。因此,開啟 ./src/actions/index.js
檔案並且按照下面來更新它:
export const LEADERBOARD_LOADED = 'LEADERBOARD_LOADED';
export const LOGGED_IN = 'LOGGED_IN';
// ... MOVE_OBJECTS and START_GAME ...
export const leaderboardLoaded = players => ({
type: LEADERBOARD_LOADED,
players,
});
export const loggedIn = player => ({
type: LOGGED_IN,
player,
});
// ... moveObjects and startGame ...
複製程式碼
這個新版本定義在兩種情況下會被觸發的 actions:
LOGGED_IN
:當一個玩家登陸,你使用這個 action 連線你的 React 遊戲到實時服務。LEADERBOARD_LOADED
:當實時服務傳送玩家列表,你使用這個 action 用這些玩家來更新 Redux 儲存。
要使你的 Redux 儲存迴應這些 actions,開啟 ./src/reducers/index.js
檔案並且按照下面來更新它:
import {
LEADERBOARD_LOADED, LOGGED_IN,
MOVE_OBJECTS, START_GAME
} from '../actions';
// ... other import statements
const initialGameState = {
// ... other game state properties
currentPlayer: null,
players: null,
};
// ... initialState definition
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, and default cases
}
}
export default reducer;
複製程式碼
現在,無論你的遊戲什麼時候觸發 LEADERBOARD_LOADED
action,你會使用新的玩家陣列列表來更新你的 Redux 儲存。除此之外,無論什麼時候一個玩家登陸(LOGGED_IN
),你將在你的儲存中更新 currentPlayer
。
然後,為了是你的遊戲使用這些新的 actions, 開啟 ./src/containers/Game.js
檔案並且按照下面來更新它:
// ... other import statements
import {
leaderboardLoaded, loggedIn,
moveObjects, startGame
} from '../actions/index';
const mapStateToProps = state => ({
// ... angle and gameState
currentPlayer: state.currentPlayer,
players: state.players,
});
const mapDispatchToProps = dispatch => ({
leaderboardLoaded: (players) => {
dispatch(leaderboardLoaded(players));
},
loggedIn: (player) => {
dispatch(loggedIn(player));
},
// ... moveObjects and startGame
});
// ... connect and export statement
複製程式碼
有了它,你準備好了使你的遊戲接入實時服務來載入和更新排行榜。因此,開啟 ./src/App.js
檔案並且按照下面來更新它:
// ... other import statements
import io from 'socket.io-client';
Auth0.configure({
// ... other properties
audience: 'https://aliens-go-home.digituz.com.br',
});
class App extends Component {
// ... constructor
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 and 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 = {
// ... other propTypes definitions
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;
複製程式碼
正如你在上面看到的程式碼,你做了這些:
- 配置了
Auth0
模組上的audience
屬性; - 抓去了當前玩家的個人資料(
Auth0.getProfile()
)來建立currentPlayer
常量,並且更新了 Redux 儲存(this.props.loggedIn(...)
); - 用玩家的
access_token
連線你的實時服務(io('http://localhost:3001', ...)
); - 監聽實時服務觸發的玩家事件,更新 Redux 儲存(
this.props.leaderboardLoaded(...)
);
然後,你的遊戲還沒有完成,你的玩家還不能殺死外星人,你加入一些臨時程式碼模擬 new-max-score
事件。第一,你出發一個新的 120
分的 maxScore
,把登陸的玩家放在第五的位置。然後,五秒鐘(setTimeout(..., 5000)
)之後,你出發一個新的 222
分的 maxScore
,把登陸的玩家放在第二的位置。
除了這些變化,你向你的 Canvas
傳入兩個新的屬性: currentPlayer
和 players
。因此,你需要開啟 ./src/components/Canvas.jsx
並且更新它:
// ... import statements
const Canvas = (props) => {
// ... gameHeight and viewBox constants
// REMOVE the leaderboard constant !!!!
return (
<svg ...>
// ... other elements
{ ! props.gameState.started &&
<g>
// ... StartGame and Title
<Leaderboard currentPlayer={props.currentPlayer} authenticate={signIn} leaderboard={props.players} />
</g>
}
// ... flyingObjects.map
</svg>
);
};
Canvas.propTypes = {
// ... other propTypes definitions
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;
複製程式碼
在這個檔案裡,你需要做以下的變更:
- 刪除常量
leaderboard
。現在,你通過你的實時服務載入這個常量。 - 更新
<Leaderboard />
元素。你現在已經有了更多地真是資料了:props.currentPlayer
andprops.players
。 - 加強
propTypes
的定義使Canvas
元件能夠使用currentPlayer
和players
的值。
好了!你已經整合了你的 React 遊戲排行榜和 Socket.IO 實時服務。要測試所有的事務,執行以下的命令:
# 進入實時服務的目錄
cd server
# 在後臺執行這個命令
node index.js &
# 回到你的遊戲
cd ..
# 啟動 React 開發服務
npm start
複製程式碼
然後,在瀏覽器中開啟你的遊戲(http://localhost:3000
)。這樣,在登陸之後,你就能看到你出現在了第五的位置,5秒鐘之後,你就會跳到第二的位置。
實現剩餘的部分
現在,你已經差不多完成了你的遊戲的所有東西。你已經建立了遊戲需要的 React 元素,你已經新增了絕大部分的動畫效果,你已經實現了排行榜特性。這個難題的遺失的部分是:
- Shooting Cannon Balls :為了殺外星人,你必須允許你的玩家射擊大炮炮彈。
- Detecting Collisions :正像你的遊戲會有大炮炮彈,飛行的物體到到處動,你必須實現一個檢測這些物體碰撞的演算法。
- Updating Lives and the Current Score :在實現你的玩家殺死飛行物體之後,你的遊戲必須增加他們當前的分數,以至於他們能夠達到新的最大分數。同樣的,你需要在飛行物體入侵地球之後減掉生命。
- Updating the Leaderboard :當實現了上面的所有特性,最後一件你需要做的事是用新的最高分數更新排行榜。
所以,在接下來的部分,你將關注實現這些部分來完成你的遊戲。
發射大炮炮彈
要使你的玩家射擊大炮炮彈,你將在你的 Canvas
新增一個 onClick
時間偵聽器。然後,當點選的時候,你的 canvas 會觸發 Redux 的 action 新增一個炮彈到 Redux store(實際上就是你的遊戲的 state)。炮彈的移動將被 moveObjects
reducer 處理。
要開始實現這個特性,你可以從建立 Redux action 開始。要做這個,開啟 ./src/actions/index.js
檔案,加入以下程式碼:
// ... other string constants
export const SHOOT = 'SHOOT';
// ... other function constants
export const shoot = (mousePosition) => ({
type: SHOOT,
mousePosition,
});
複製程式碼
然後,你能夠準備 reducer(./src/reducers/index.js
)來處理這個 action:
import {
LEADERBOARD_LOADED, LOGGED_IN,
MOVE_OBJECTS, SHOOT, START_GAME
} from '../actions';
// ... other import statements
import shoot from './shoot';
const initialGameState = {
// ... other properties
cannonBalls: [],
};
// ... initialState definition
function reducer(state = initialState, action) {
switch (action.type) {
// other case statements
case SHOOT:
return shoot(state, action);
// ... default statement
}
}
複製程式碼
正如你看到的,你的 reducer 的新版本在接收到 SHOOT
action 時,使用 shoot
方法。你仍然需要定義這個方法。所以,在和 reducer 同樣的目錄下建立一個名為 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 的 state。
在定義這個 action 和 reducer 處理它之後,你將更新 Game
容器給 App
元件提供 action。所以,開啟 ./src/containers/Game.js
檔案並且按照下面的來更新它:
// ... other import statements
import {
leaderboardLoaded, loggedIn,
moveObjects, startGame, shoot
} from '../actions/index';
// ... mapStateToProps
const mapDispatchToProps = dispatch => ({
// ... other functions
shoot: (mousePosition) => {
dispatch(shoot(mousePosition))
},
});
// ... connect and export
複製程式碼
現在,你需要更新 ./src/App.js
檔案來使用你的 dispatch wrapper:
// ... 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
的類中定義一個新的方法使用 canvasMousePosition
來呼叫 shoot
dispatcher。然後,你傳遞把這個新的方法傳遞到 Canvas
元件。所以,你仍然需要加強這個元件,將這個方法附加到 svg
元素的 onClick
事件監聽器並且使它渲染加農炮彈:
// ... other import statements
import CannonBall from './CannonBall';
const Canvas = (props) => {
// ... gameHeight and viewBox constant
return (
<svg
// ... other properties
onClick={props.shoot}
>
// ... defs, Sky and Ground elements
{props.gameState.cannonBalls.map(cannonBall => (
<CannonBall
key={cannonBall.id}
position={cannonBall.position}
/>
))}
// ... CannonPipe, CannonBase, CurrentScore, etc
</svg>
);
};
Canvas.propTypes = {
// ... other props
shoot: PropTypes.func.isRequired,
};
// ... defaultProps and export statement
複製程式碼
提示: 在
CannonPipe
之前 新增cannonBalls.map
很重要,否則炮彈將和大炮自身重疊。
這些改變足夠是你的遊戲在炮彈的初始位置新增炮彈了(x: 0
, y: 0
)並且 他們的彈道(angle
)已經定義好。現在的問題是這些物件是沒有動畫的(其實就是他們不會動)。
要使他們動,你將需要在 ./src/utils/formulas.js
檔案中新增兩個函式:
// ... other functions
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
。這就是,你刪除了 Y-axis 座標小於 -800
,或者向左邊移動太多的(小於 -500
),或者向右邊移動太多的(大於 500
)。
最後,要使用這個方法,你將需要將 ./src/reducers/moveObjects.js
按照下面來重構:
// ... other import statements
import moveBalls from './moveCannonBalls';
function moveObjects(state, action) {
if (!state.gameState.started) return state;
let cannonBalls = moveBalls(state.gameState.cannonBalls);
// ... mousePosition, createFlyingObjects, filter, etc
return {
...newState,
gameState: {
...newState.gameState,
flyingObjects,
cannonBalls,
},
angle,
};
}
export default moveObjects;
複製程式碼
在這個檔案的新版本中,你簡單的加強了之前的 moveObjects
reducer 來使用新的 moveBalls
函式。然後,你使用這個函式的結果來給 gameState
的 cannonBalls
屬性定義一個新陣列。
現在,完成了這些更改之後,你的玩家能夠發射炮彈了。你可以在一個瀏覽器中通過測試你的遊戲來檢視這一點。
檢測碰撞
現在你的遊戲支援發射炮彈並且這裡有飛行的物體入侵地球,這是一個好的時機新增一個檢測碰撞的演算法。有了這個演算法,你可以刪除相碰撞的炮彈和飛行物體。這也使你能夠繼續接下來的特性: 增加當前的分數。
一個好的實現這個檢測碰撞演算法的策略是把炮彈和飛行物體想象成為矩形。儘管這個策略不如按照物體真實形狀實現的演算法準確,但是把它們作為矩形處理會使每件事情變得簡單。除此之外,對於這個遊戲,你不需要很精確,因為,幸運的是,你不需要這個演算法殺死真的外星人。
在腦袋中有這個想法之後,新增接下來的方法到 ./src/utils/formulas.js
檔案中:
// ... other functions
export const checkCollision = (rectA, rectB) => (
rectA.x1 < rectB.x2 && rectA.x2 > rectB.x1 &&
rectA.y1 < rectB.y2 && rectA.y2 > rectB.y1
);
複製程式碼
正像你看到的,把這些物件按照矩形來看待,使你在這些簡單的情況下檢測是否重疊。現在,為了使用這個函式,在 ./src/reducers
目錄下,建立一個名為 checkCollisions.js
的新檔案,新增以下的程式碼:
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;
複製程式碼
檔案中的這些程式碼基本上做了下面幾件事:
- 定義了一個名為
objectsDestroyed
的陣列來儲存所有毀掉的東西。 - 通過迭代
flyingDiscs
陣列(使用forEach
方法)建立矩形來代表飛行物。提示,因為你使用 CSS 動畫來使物體移動,你需要基於currentLifeTime
的 Y-axis 計算他們位置。 - 通過迭代
cannonBalls
陣列(使用forEach
方法)建立矩形來代表炮彈。 - 呼叫
checkCollision
方法,來決定這兩個矩形是否必須被摧毀。然後,如果他們必須被摧毀,他們被新增到objectsDestroyed
陣列,由這個方法返回。
Lastly, you will need to update the moveObjects.js
file to use this function as follows:
最後,你需要更新 moveObjects.js
檔案,參照下面來使用這個方法:
// ... import statements
import checkCollisions from './checkCollisions';
function moveObjects(state, action) {
// ... other statements and definitions
// the only change in the following three lines is that it cannot
// be a const anymore, it must be defined with let
let flyingObjects = newState.gameState.flyingObjects.filter(object => (
(now - object.createdAt) < 4000
));
// ... { x, y } constants and angle constant
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
reducer 把它們從 gameState
刪除。你可以在瀏覽器中看到這個 action。
更新生命數和當前分數
無論什麼時候飛行的物體入侵地球,你必須減少玩家持有的命的數量。所以,當玩家沒有更多地生命值的時候,你必須結束遊戲。要實現這些特性,你只需要更新兩個檔案。第一個檔案是 ./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) {
// ... code until 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, etc ...
return {
...newState,
gameState: {
...newState.gameState,
flyingObjects,
cannonBalls: [...cannonBalls],
lives,
started,
},
angle,
};
}
export default moveObjects;
複製程式碼
這些行新程式碼只是簡單的比較了 flyingObjects
陣列和其在 state
中的初始長度來決定玩家是否失去生命。這個策略有效是因為你把這些程式碼新增在了彈出飛行物體之後並且在刪除碰撞物體之前。這些飛行物體在遊戲中保持 4 秒鐘((now - object.createdAt) < 4000
)。所以,如果這些陣列的長度發生了變化,就意味著飛行物體入侵了地球。
現在,給玩家展示他們的生命數,你需要更新 Canvas
元件。所以,開啟 ./src/components/Canvas.jsx
檔案並且按照下面來更新:
// ... other import statements
import Heart from './Heart';
const Canvas = (props) => {
// ... gameHeight and viewBox constants
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 ...>
// ... all other elements
{lives}
</svg>
);
};
// ... propTypes, defaultProps, and export statements
複製程式碼
有了這些更改,你的遊戲幾乎完成了。玩家已經能夠發射和殺死飛行物體,並且如果太多的它們進攻地球,遊戲結束。現在,為了完成這部分,你需要更新玩家當前的分數,這樣他們才能比較誰殺了更多地外星人。
做這個來加強你的遊戲很簡單。你只需要按以下來更新 ./src/reducers/moveObjects.js
這個檔案:
// ... import statements
function moveObjects(state, action) {
// ... everything else
const kills = state.gameState.kills + flyingDiscsDestroyed.length;
return {
// ...newState,
gameState: {
// ... other props
kills,
},
// ... angle,
};
}
export default moveObjects;
複製程式碼
然後,在 ./src/components.Canvas.jsx
檔案,你需要用這個來替換 CurrentScore
元件(硬編碼值為 15):
<CurrentScore score={props.gameState.kills} />
複製程式碼
“我使用 React、Redux、SVG 和 CSS 動畫建立一個遊戲。”
更新排行榜
好訊息!更新排行榜是你說你使用 React、Redux、SVG 和 CSS 動畫完成了一個遊戲所需要做的最後一件事。同樣的,正如你看到的,這裡的工作很快並且沒有痛苦。
第一,你需要更新 ./server/index.js
檔案來重置 players
陣列。你不希望你釋出的遊戲裡是假使用者和假結果。所以,開啟這個檔案並且刪除所有的假玩家/結果。最後,你會有像下面這樣定義的常量:
const players = [];
複製程式碼
然後,你需要重構 App
元件。所以,開啟 ./src/App.js
檔案並且做下面的修改:
// ... import statetments
// ... Auth0.configure
class App extends Component {
constructor(props) {
// ... super and this.shoot.bind(this)
this.socket = null;
this.currentPlayer = null;
}
// replace the whole content of the componentDidMount method
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,
};
this.props.loggedIn(self.currentPlayer);
self.socket = io('http://localhost:3001', {
query: `token=${Auth0.getAccessToken()}`,
});
self.socket.on('players', (players) => {
this.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, and render method
}
// ... propTypes, defaultProps, and export statement
複製程式碼
做一個總結,這些是你在這個元件中做的更改:
- 你在它的類裡面定義兩個新屬性(
socket
和currentPlayer
),這樣你就能在不同的方法裡使用它們。 - 你刪除用來觸發模擬
new-max-score
事件的假的最高分。 - 你通過迭代
players
陣列(你從 Socket.IO 後臺接收到的)來設定玩家正確的最高分。就這樣,如果他們再一次回來啊,他們仍然會有maxScore
記錄 - 你定義
componentWillReceiveProps
生命週期來檢查玩家是否打到了一個新的maxScore
。如果是,你的遊戲觸發一個new-max-score
事件去更新排行榜
這就是了!你的遊戲已經準備好了第一次。要看所有的行為,用下面的程式碼執行 Socket.IO 後臺和你的 React 應用:
# 在後臺執行後端服務
node ./server/index &
# 執行 React 應用
npm start
複製程式碼
然後,執行瀏覽器,使用不同得 email 地址認證,並且殺一些外星人。你可以看到,當遊戲結束的時候,排行榜將會在兩個瀏覽器更新。
總結
在這個系列中,你使用了很多驚人的技術來建立一個好遊戲。你使用了 React 來定義和控制遊戲元素,你使用了 SVG(代替 HTML)來渲染這些元素,你使用了 Redux 來控制遊戲的狀態,並且你使用了 CSS 動畫使外星人在螢幕上運動。哦,除此之外,你甚至使用了一點 Socket.IO 使你的排行榜是實時的,並使用 Auth0 作為你遊戲的身份管理系統。
唉!你走了很長的路,你在這三篇文章中學了很多。可能是時候休息一下,玩會兒你的遊戲了。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。