- 原文地址:Developing Games with React, Redux, and SVG - Part 3
- 原文做者:Bruno Krebs
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:xueshuai
- 校对者:
提示: 在这个系列中,你将学习如何使用 React 和 Redux 控制一堆 SVG 元素来建立一个游戏。这个系列所须要的知识一样也可使你建立使用 React 和 Redux 的其余类型的动画,而不仅是游戏。你可以在下面的 GitHub 仓库中找到文章中开发的最终代码:Aliens Go Home - 第 3 部分html
在这个教程中你开发的游戏叫作 Aliens, Go Home! 这个游戏的想法很简单,你有一门大炮,你将必须杀掉尝试入侵地球的飞行物体。要杀掉这些飞行的物体,你将必须标示和点击 SVG canvas 来使你的大炮发射。前端
若是你有些疑惑,你能够发现完成了的游戏并在这里运行它。可是不要玩的太多,你还有工做必须作。node
“我正在用 React,Redux 和 SVG元素react
建立一个游戏。”android
在 这个系列的第一部分,你已经使用 create-react-app
来启动你的 React 应用,你已经安装和配置了 Redux 来管理游戏的状态。以后,在建立游戏的元素时,例如 Sky
, Ground
, CannonBase
和 CannonPipe
, 你已经学习了如何在 React 组件中使用 SVG。最终,你经过使用事件监听方法给你的大炮添加动画效果和一个 JavaScript interval 来触发 Redux 的 action 更新 CannonBase
的角度。ios
这些为你提供了理解如何使用React,Redux和SVG来建立你的游戏(和其余动画)的方法。git
在 第二部分,你已经建立了游戏中其余的必须元素(例如 Heart
, FlyingObject
和 CannonBall
),使你的玩家可以开始游戏,并使用 CSS 动画让飞行物体飞起来(这就是他们应该作的事,对么?)。github
就算是咱们有了这些很是好的特性,可是他们尚未构成一个完整的游戏。你仍然须要使你的大炮发射炮弹,并完成一个算法来检测飞行物体和炮弹的碰撞。除此以外,你必须在你的玩家杀死外星人的时候,增长 CurrentScore
。web
杀死外星人和看到当前分数的增加很酷,可是你可能会使这个游戏更有吸引力。。这就是为何你要在你的游戏中增长一个排行榜特性。这将会使你的玩家花费更多的时间来达到排行榜的高位。算法
有了这些特性,你能够说你有了一个完整的游戏。因此,为了节约时间,是时候关注他们了。
提示: 若是(不管是什么缘由)你没有 前面两部分 建立的代码,你能够从 这个 GitHub 仓库 克隆他们。克隆以后,你可以继续跟随接下来板块中的指示。
第一件你要作的使你的游戏看起来更像一个真正的游戏的事情就是实现排行榜特性。这个特性将使玩家可以登录,因此你的游戏可以跟踪他们的最高分数和他们的排名。
要使 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 来保护你的游戏是简单和痛苦小的。”
如今你已经配置了 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/
)来看看你的新排行榜特性。
帅气,你已经使用 Auth0 做为你的身份管理服务,而且你也建立了须要展现排行榜的组件。以后,你须要作什么?对了,你须要一个能出发实时事件的后端来更新排行榜。
这可能使你想到:开发一个实时后端服务器困难么?不,不困难。使用 Socket.IO,你能够在很短的时间实现这个特性。无论怎样,在深刻以前,你可能想要好糊这个后端服务,对不对?要作这个,你须要建立一个 Auth0 API 来表明你的服务。
这样作很简单。前往 你的 Auth0 仪表盘的 APIs 页面 而且点击 Create API 按钮,Auth0 会想你展现一个有三个信息须要填的表单:
https://aliens-go-home.digituz.com.br
。在你填完这个表单后,点击 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 应用中触发这个事件。剩余的代码很是直观。所以,你能关注在你的游戏中集成这个服务。
在建立你的实时后端服务以后,是时候将它集成到你的 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', ...)
);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
and props.players
。propTypes
的定义使 Canvas
组件可以使用 currentPlayer
和 players
的值。好了!你已经整合了你的 React 游戏排行榜和 Socket.IO 实时服务。要测试全部的事务,执行如下的命令:
# 进入实时服务的目录
cd server
# 在后台运行这个命令
node index.js &
# 回到你的游戏
cd ..
# 启动 React 开发服务
npm start
复制代码
而后,在浏览器中打开你的游戏(http://localhost:3000
)。这样,在登录以后,你就能看到你出如今了第五的位置,5秒钟以后,你就会跳到第二的位置。
如今,你已经差很少完成了你的游戏的全部东西。你已经建立了游戏须要的 React 元素,你已经添加了绝大部分的动画效果,你已经实现了排行榜特性。这个难题的遗失的部分是:
因此,在接下来的部分,你将关注实现这些部分来完成你的游戏。
要使你的玩家射击大炮炮弹,你将在你的 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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。