- 原文地址:Developing Games with React, Redux, and SVG - Part 1
- 原文做者:Bruno Krebs
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:zephyrJS
- 校对者:allenlongbaobao、dandyxu
TL;DR: 在这个系列里,您将学会用 React 和 Redux 来控制一些 SVG 元素来建立一个游戏。经过本系列的学习,您不只能建立游戏,还能用 React 和 Redux 来开发其余类型的动画。您能够在这个 GitHub 仓库: Aliens Go Home - Part 1 下找到最终的开发代码。css
在这个系列里您将要开发的游戏叫作 Aliens, Go Home! 这个游戏的想法很简单,您将拥有一座炮台,而后您必须消灭那些试图入侵地球的飞碟。为了消灭这些飞碟,您必须在 SVG 画布上经过瞄准和点击来操做炮台的射击。html
若是您很好奇, 您能够找到 这个游戏最终运行版。但别太沉迷其中,您还要完成它的开发。前端
做为学习这个系列的先决条件,您将须要一些 web 开发的知识 (主要是 JavaScript) 和一台 安装了Node.js and NPM 的电脑。您能够在没有很深的 JavaScript 编程语言知识,甚至不知晓 React、Redux 和 SVG 是如何工做的状况下学习本系列的内容。可是,若是您具有这些,您将花更少的时间来领会不一样的主题以及它们是如何组合在一块儿的。node
然而,更值得关注的是本系列包含的相关文章、帖子和文档,它们为主题提供了更好的补充说明。react
尽管前面没有提到 Git,但它确实是一个很好的开发工具。全部专业的开发者都会用 Git (或者其余的版本控制系统好比 Mercurial 或 SVN) 来开发,甚至是用于我的的业余项目。android
为何您建立了一个项目却不去备份它?您甚至没必要付费就可使用。由于您用了相似 GitHub (最佳选择!) 或 BitBucket (老实说并不差) 的服务而且将您的代码保存在值得信赖的云服务器上。ios
除了确保您的代码安全以外,这些工具还有助于您把握项目开发的进度。例如,若是您正在使用 Git 并且您的 app 的新版本恰好有一些 bug,只需几行命令,就能轻松回滚到以前写的代码。git
另外一个重要的好处是您能够为这个系列的任何一部分来提交代码。就像这样,您将 轻松地看到这些部分的修改建议,经过本教程的学习,您的生活将变得更轻松。github
因此,快给您本身安装个 Git 吧。另外,在 GitHub 上建立一个帐号 (若是您尚未 GitHub 帐户) 而且把您的项目保存到仓库里。而后,每完成一部分,就把修改提交到这个仓库上。噢,可别忘了 push 这个操做啊。web
首先您要用 create-react-app
来引导您建立一个 React、Redux 和 SVG 的游戏项目。您可能了解过它 (若是不知道也不要紧),create-react-app
是一个由 Facebook 持有的开源工具,它帮助开发者快速的开始他的 React 项目。须要安装 Node.js 和 NPM 到本地 (5.2 或以上版本), 您甚至不用安装 create-react-app
就能使用它:
# using npx will download (if needed)
# create-react-app and execute it
npx create-react-app aliens-go-home
# change directory to the new project
cd aliens-go-home
复制代码
该工具将建立相似下面的目录结构:
|- node_modules
|- public
|- favicon.ico
|- index.html
|- manifest.json
|- src
|- App.css
|- App.js
|- App.test.js
|- index.css
|- index.js
|- logo.svg
|- registerServiceWorker.js
|- .gitignore
|- package.json
|- package-lock.json
|- README.md
复制代码
create-react-app
是很是热门的,它有着完善的文档和社区支持。例如,若是您想要了解它细节,您能够查看 create-react-app
官方的 GitHub 仓库 以及 他的使用指南。
如今,您会想把您不须要的文件删掉。例如,您能够处理以下文件:
App.css
:App
是一个很重要的组件可是他的样式定义须要交给其余组件来处理;App.test.js
:测试的内容会在其余的文章里提到,如今您还不须要用到它;logo.svg
:这个游戏里您不会用到 React 的 logo;删除这些文件后,若是您执行这个项目它极可能会报错。但您只须要删除 ./src/App.js
文件里引用的两句话就能轻松解决:
// remove both lines from ./src/App.js
import logo from './logo.svg';
import './App.css';
复制代码
而后重构下 render()
方法:
// ... import statement and class definition
render() {
return (
<div className="App"> <h1>We will create an awesome game with React, Redux, and SVG!</h1> </div>
);
}
// ... closing bracket and export statement
复制代码
千万别忘了 提交您的文件到 Git 上!
在启动了 React 项目并删掉了一些没用的文件以后,您将安装和配置 Redux 来使它成为 您应用程序的惟一数据源. 您也须要安装 PropTypes,这个工具将帮助您避免常见的错误。两个工具能够用一行命令来安装:
npm i redux react-redux prop-types
复制代码
如您所见,这行命令包含了第三个 NPM 包:react-redux
。尽管您能够直接在 React 里面使用 Redux,但它不是最佳选择。react-redux
对咱们本来须要繁琐手动处理的性能优化有所帮助。
有了这些包,您就能在您的应用里配置和使用 Redux 了。这个过程很简单,您将须要建立一个 container 组件,一个 presentational 组件,以及一个 reducer。容器组件和视图组件的区别在于,首先须要将视图组件 链接
到 Redux。reducer 是您将要建立的第三个组件,它是 Redux store 里的核心组件。这类组件主要用于当您的应用触发事件后来获取对应的 actions 并根据这些 actions 来调用关联的函数去修改相应的状态。
若是您对这些概念还不熟悉,您能够阅读 这篇文章来更好的理解视图组件和容器组件 以及经过 这篇 Redux 使用教程来学习关于 actions、reducers、和 store 的概念. 尽管学会这些概念是很值得推荐的,但即便都不懂您也能无障碍地学习本系列的教程。
您最好先建立 reducer 来开始您的项目,由于它不依赖其它资源(事实上,正好相反)。为了把它们组合起来,您须要在 src
目录里面建立一个叫作 reducers
的新目录,而后往里面添加一个 index.js
文件。这个文件包含的源码以下:
const initialState = {
message: `It's easy to integrate React and Redux, isn't it?`,
};
function reducer(state = initialState) {
return state;
}
export default reducer;
复制代码
如今,您的 reducer 将简单地初始化一个叫 message
的应用状态,它将很容易的集成到 React 和 Redux 中。紧接着,您将定义 actions 并在文件中操做它们。
而后,您能够重构您的应用来向用户展现这个 message。此刻是您安装并使用 prop-types
的好时机。为此, 您须要打开 ./src/App.js
文件并替换成以下内容:
import React, {Component} from 'react';
import PropTypes from 'prop-types';
class App extends Component {
render() {
return (
<div className="App"> <h1>{this.props.message}</h1> </div>
);
}
}
App.propTypes = {
message: PropTypes.string.isRequired,
};
export default App;
复制代码
如您所见,用 prop-types
定义您组件所指望的类型是垂手可得的。您只须要用相应的 props
来定义组件的 propTypes
属性。网上总结了一些关于 propTypes 的基础和高级的用法的备忘录(例如 这个、这个、还有这个)。若是须要,就去看看吧。
尽管您定义了须要渲染的 App
组件以及用 Redux store 初始化了 state,您仍然须要某种方法把组件组合在一块儿。这时候 container 组件登场了。用一种用组织的方式来定义您的 container,您将在 src
目录里建立一个 containers
目录。而后,您就能够在新目录下的 Game.js
里面建立一个叫 Game
的容器。这个组件将使用 react-redux
的 connect
方法并往 App
组件的 message
属性中传入 state.message
的值:
import { connect } from 'react-redux';
import App from '../App';
const mapStateToProps = state => ({
message: state.message,
});
const Game = connect(
mapStateToProps,
)(App);
export default Game;
复制代码
快大功告成了。最后一步是重构 ./src/index.js
来把它们组织在一块儿,咱们经过初始化 Redux store 和把它传进 Game
容器(该容器将获取 message
并把它传给 App
)来完成这一步。下面就是 ./src/index.js
文件重构后的代码:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import './index.css';
import Game from './containers/Game';
import reducer from './reducers';
import registerServiceWorker from './registerServiceWorker';
/* eslint-disable no-underscore-dangle */
const store = createStore(
reducer, /* preloadedState, */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
/* eslint-enable */
ReactDOM.render(
<Provider store={store}> <Game /> </Provider>,
document.getElementById('root'),
);
registerServiceWorker();
复制代码
搞定!如今您能够到项目的根目录运行 npm start
来看看是否一切正常。这将在开发模式中运行您的应用程序并在默认浏览器中打开它。
“集成 React 和 Redux 是很是容易的。”
在这里 tweet 咱们![]()
在这个系列您将看到,用 React 建立 SVG 组件是很是轻松的事。事实上,用 HTML 和 SVG 建立 React 组件几乎没有区别。基本上,惟一的区别就是 SVG 引入了一些新的元素,而这些元素都是在 SVG 上绘制的。
话虽如此,在用 SVG 和 React 建立组件以前,简单了解下 SVG 仍是颇有帮助的。
SVG 是最酷和最灵活的 web 标准之一。SVG 是可伸缩矢量图形 (Scalable Vector Graphics) 标准,它是一种标记语言,容许开发人员绘制二维的矢量图形。它与 HTML 很是类似。这两种技术都是基于 XML 标记语言,能够很好地与 CSS 和 DOM 等其余 Web 标准兼容。这意味着您能够将 CSS 规则应用于 SVG 元素,就像您对 HTML 元素 (包括动画) 所作的那样。
在本系列教程里,您将用 React 建立许多 SVG 组件。您甚至将组合(填充)SVG 元素到您的 game 元素里(就像往大炮里填充炮弹同样)。
关于 SVG 详尽的介绍并不在本系列的探讨访问以内,它将使本文过于冗长。因此,若是您想学习关于 SVG 标记语言更详尽的内容,您能够去查看 Mozilla 提供的 SVG 教程 以及在 这篇文章中了解关于 SVG 坐标系的内容。
可是,在开始建立组件以前,您须要了解一些关于 SVG 的重要特性。首先,开发者能够将 SVG 和 DOM 组合在一块儿来实现某些使人兴奋的功能。咱们能够很轻松地把 React 和 SVG 结合起来。
其次,SVG 坐标系跟笛卡尔平面很是类似,但倒是上下颠倒的。那意味着在 x 轴上方(y 轴上半轴)默认是负值。另外一方面,横坐标的值跟笛卡尔平面同样(即负值显示在 y 轴的左侧)。这些行为很容易经过 在 SVG 的画布里转化 来修改。可是,为了避免使其它的开发人员感到困惑,最好仍是使用默认的方式。您将很快习惯它的用法。
第三也是最后一件事,您须要知道 SVG 引入了许多的新元素(例如 circle
、rect
、和 path
)。 要使用这些元素,不能简单地在 HTML 元素中定义它们。首先, 您必须在您想要绘制的 SVG 组件里定义一个 svg 元素(画布)。
使用 SVG 绘制元素能够经过三种方式完成。首先,您可使用像 rect
,circle
和 line
这些元素。尽管它们用起来不怎么方便。顾名思义,它们只能让您绘制一些简单的图形。
第二种方式是把它们组合成更为复杂的图形。例如,您能够用一个等边的 矩形
(正方形)和两条直线组合成一个房子。可是这种作法仍然有局限性。
使用 path
元素 是更加灵活的第三者方式。这种元素容许开发者建立更加复杂的图形。它接受一组命令来指导浏览器绘制绘制图形。例如,要绘制一个 'L',您能够建立一个 path
元素,其中包含三个命令:
M 20 20
: M
是移动的意思,这个命令让浏览器的 画笔
移动到指定的 X 和 Y 坐标(即 20, 20
);V 80
: 这个命令让浏览器绘制一条从上一个点到 80
的平行于 y 轴的垂直线;H 50
: 这个命令让浏览器绘制一条从上一个点到 50
的平行于 x 轴的水平线;<svg>
<path d="M 20 20 V 80 H 50" stroke="black" stroke-width="2" fill="transparent" /> </svg>
复制代码
path
元素接受许多其余命令。其中,最重要的命令之一就是 三次贝塞尔曲线命令. 此命令容许您在路径中添加一些平滑曲线,方法是获取两个参考点和两个控制点。
Mozilla 教程介绍了三次贝塞尔曲线在 SVG 上是如何工做的:
”三次贝塞尔曲线的每一个点都有两个控制点来控制。所以,为了建立三次贝塞尔曲线,您须要定义三组坐标。最后一组坐标表示曲线的终点。另外两组是控制点。[...]。控制点实际上描述的是曲线起始点的斜率。Bezier 函数建立一个平滑曲线,描述了从起点斜率到终点斜率的渐变过程“ —Mozilla 开发者网络
例如,绘制一个 “U”,您能够按照以下步骤执行:
<svg>
<path d="M 20 20 C 20 110, 110 110, 110 20" stroke="black" fill="transparent"/>
</svg>
复制代码
在这个例子里,传递给 path
元素的指令告诉浏览器须要执行如下步骤:
20, 20
;20, 110
;110, 110
;110 20
;若是您仍然不知道三次贝塞尔曲线是如何工做的,也不用担忧。在本系列教程里,有将会有机会来练习它的。除此以外,您还能够在网上找到许多关于这个特性的教程并且您也能够经过相似 JSFiddle 和 Codepen 这类工具来练习它。
既然您的项目已经结构化,而且您已经了解了 SVG 的基本知识,那么是时候开始建立您的游戏了。您须要建立的第一个元素是 SVG 画布,您将使用它来绘制游戏的元素。
这是一个视图组件。所以,您能够在 ./src
目录下建立一个名为 Component
目录,用来保存和它相似的组件。您的动画都将在上面绘制,叫 Canvas
是在天然不过的事了。所以,在 ./src/components/
目录下建立 Canvas.jsx
文件并添加以下代码:
import React from 'react';
const Canvas = () => {
const style = {
border: '1px solid black',
};
return (
<svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" style={style} > <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas; 复制代码
有了这个文件后,您将重构您的 App
组件来使用 Canvas
:
import React, {Component} from 'react';
import Canvas from './components/Canvas';
class App extends Component {
render() {
return (
<Canvas /> ); } } export default App; 复制代码
若是您运行了(npm start
)命令并查看了您的应用,您将看到浏览器只绘制了圆的四分之一。这是由于坐标系原点默认在窗口的左上角。另外,您也会看到 svg
并无占满整个屏幕。
为了便于管理,您最好将画布填充满整个屏幕。您也会但愿从新定位它的原点,使其位于 X 轴的中心,而且靠近底部(一会您就会把您的炮台放在原点上)。同时,您须要修改这两个文件:./src/components/Canvas.jsx
和 ./src/index.css
。
您能够把 Canva
组件的内容替换成以下代码:
import React from 'react';
const Canvas = () => {
const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
return (
<svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas; 复制代码
在新的版本里,您会为 svg
元素定义 viewBox
特性。此特性的做用是定义画布及其内容必须适合特定容器(在当前的例子里指的是 window/browser)。如您所见,viewBox
特性有 4 个参数:
min-x
:这个值定义的是用户看到的最左边的点。所以,要使 y 轴(和圆)出如今屏幕中心,能够将屏幕宽度除以负 2(window.innerWidth/-2
),来获得这个属性(min-x
)。注意您要使用 -2
来平分原点左(负)右(正)两边的数值。min-y
:这个值定义了您画布最上边的点。这里,您经过 100
减去 window.innerHeight
来给 Y 原点以后空出了一些区域(100
点)。width
和 height
:这些值定义了用户将在屏幕上看到多少个 X 和 Y 坐标。除了定义 viewBox
特性,您也能够在新版本里定义 preserveAspectRatio
特性。您已经使用了 xMaxYMax none
来强制使画布和它的元素进行统一的缩放。
重构您的画布以后,您须要在 ./src/index.css
文件中添加以下规则:
/* ... body definition ... */
html, body {
overflow: hidden;
height: 100%;
}
复制代码
这将是 html
和 body
元素隐藏(禁用)滚动。它也将是这些元素占满这个屏幕。
若是您如今查看您的应用,您会看到您的圆正水平居中并位于屏幕底部附近。
在使画布占满整个屏幕并将原点轴从新定位到它的中心以后,是时候建立真正的游戏元素了。您能够先定义一个 sky 组件来做为您的游戏背景。为此,能够在 ./src/Components/
目录下建立 Sky.jsx
文件,代码以下:
import React from 'react';
const Sky = () => {
const skyStyle = {
fill: '#30abef',
};
const skyWidth = 5000;
const gameHeight = 1200;
return (
<rect style={skyStyle} x={skyWidth / -2} y={100 - gameHeight} width={skyWidth} height={gameHeight} /> ); }; export default Sky; 复制代码
您可能会感到奇怪为何要给您的游戏设置如此巨大的区域(宽 5000
和高 1200
)。事实上,宽度在这个游戏中并不重要。您只须要设置能够覆盖任何尺寸的屏幕就够了。
如今,高度是很重要的。很快,不管用户分辨率是多少横屏仍是竖屏,您都会把画布高度强制显示为 1200
。这将给您游戏带来一致地体验,每一个用户都将会在同一区域看到您的游戏。像这样,您将会定义飞碟将出如今哪里以及它们将须要多长时间经过这些点。
要想您的画布显示您的新天空,请在编辑器打开 Canvas.jsx
并对其进行重构:
import React from 'react';
import Sky from './Sky';
const Canvas = () => {
const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
return (
<svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <Sky /> <circle cx={0} cy={0} r={50} /> </svg> ); }; export default Canvas; 复制代码
若是您如今检查您的应用(npm start
),您将看到您的圆仍在正中央靠近底部的位置,并且您如今有了一个蓝色(fill: '#30abef'
)的背景。
注意: 若是您将
Sky
组件放到circle
组件后面,您将再也看不到后者。这是由于 SVG 并不 支持z-index
属性。SVG 依赖于所列元素的顺序来决定哪一个元素高于另外一个元素。也就是说,您必须在Sky
组件以后定义Circle
组件,这样才能让网页浏览器知道必须在蓝色背景之上显示它。
建立完 Sky
组件后, 接下来您能够建立 Ground
组件。为此,在 ./src/Components/
目录下建立一个名为 Cround.js
的新文件,并添加以下代码:
import React from 'react';
const Ground = () => {
const groundStyle = {
fill: '#59a941',
};
const division = {
stroke: '#458232',
strokeWidth: '3px',
};
const groundWidth = 5000;
return (
<g id="ground">
<rect
id="ground-2"
data-name="ground"
style={groundStyle}
x={groundWidth / -2}
y={0}
width={groundWidth}
height={100}
/>
<line
x1={groundWidth / -2}
y1={0}
x2={groundWidth / 2}
y2={0}
style={division}
/>
</g>
);
};
export default Ground;
复制代码
这是一个并不怎么花哨的组件。它只由一个矩形和一条线组成。可是,如您所见,它仍是须要一个值为 5000
的常量来定义宽度。所以,专门建立一个文件来保存这样的全局常量是一个不错的选择。
就像这样,在 ./src/
目录下建立一个名为 utils
的新目录,紧接着,在这个新目录下建立一个名为 constants.js
文件。 如今,您能够往里面添加一个常量:
// very wide to provide as full screen feeling
export const skyAndGroundWidth = 5000;
复制代码
以后,您就能够重构您的 Sky
组件和 Ground
组件来使用这个新常量。
结束这节后,可别忘了往您的画布里添加 Groud
组件(记得要放在 Sky
组件和 Circle
组件之间)。若是您对于最后的这些步骤有什么疑问,请在这里给我留言.
如今您的游戏了已经有了 sky 组件和 ground 组件了。接下来,您将添加一些更加有趣的东西。也许,是时候让您的 cannon 组件登场了。这些组件会比其它的两个组件要复杂些。它们将会有更多行代码,这是因为您将要用三次贝塞尔曲线来绘制它们。
您可能还记得,在 SVG 上定义三次贝塞尔曲线须要四个点:起点,终点以及两个控制点。这些点在 path
元素上的 d
属性里定义,就像这样:M 20 20 C 20 110, 110 110, 110 20
。
为了不重复您可在代码里使用 模板字符串 来建立这些曲线,您能够在 ./src/utils/
目录下建立一个名为 formulas.js
的文件,并定义一个传入某些参数就会返回这些字符串的函数:
export const pathFromBezierCurve = (cubicBezierCurve) => {
const {
initialAxis, initialControlPoint, endingControlPoint, endingAxis,
} = cubicBezierCurve;
return ` M${initialAxis.x} ${initialAxis.y} c ${initialControlPoint.x} ${initialControlPoint.y} ${endingControlPoint.x} ${endingControlPoint.y} ${endingAxis.x} ${endingAxis.y} `;
};
复制代码
这段代码十分简单,它先从 cubicBezierCurve
中提取(initialAxis
,initialControlPoint
,endingControlPoint
,endingAxis
)接着将它们传入到构建三次贝塞尔曲线的模板字符串中。
有了这个文件,您就能够构建您的炮台了。为了让事情更有条理,您须要把您的炮台分为两部分: CannonBase
和 CannonPipe
。
要定义 CannonBase
,需在 ./src/components
目录下建立 CannonBase.jsx
文件并添加以下代码:
import React from 'react';
import { pathFromBezierCurve } from '../utils/formulas';
const CannonBase = (props) => {
const cannonBaseStyle = {
fill: '#a16012',
stroke: '#75450e',
strokeWidth: '2px',
};
const baseWith = 80;
const halfBase = 40;
const height = 60;
const negativeHeight = height * -1;
const cubicBezierCurve = {
initialAxis: {
x: -halfBase,
y: height,
},
initialControlPoint: {
x: 20,
y: negativeHeight,
},
endingControlPoint: {
x: 60,
y: negativeHeight,
},
endingAxis: {
x: baseWith,
y: 0,
},
};
return (
<g>
<path
style={cannonBaseStyle}
d={pathFromBezierCurve(cubicBezierCurve)}
/>
<line
x1={-halfBase}
y1={height}
x2={halfBase}
y2={height}
style={cannonBaseStyle}
/>
</g>
);
};
export default CannonBase;
复制代码
除了三次贝塞尔曲线,这个组件没有其余新意。最后,浏览器会渲染出一个带有深棕色的曲线和亮棕色背景的元素。
建立 CannonPipe
的代码将会相似于 CannonBase
。不一样之处在于它将使用其余颜色,并用其余的坐标点来传 pathFromBezierCurve
函数来绘制炮管。另外,这个组件还会使用 transform 属性来模拟炮台的旋转。
为了建立这个组件,./src/components/
目录下建立 CannonPipe.jsx
文件并添加以下代码:
import React from 'react';
import PropTypes from 'prop-types';
import { pathFromBezierCurve } from '../utils/formulas';
const CannonPipe = (props) => {
const cannonPipeStyle = {
fill: '#999',
stroke: '#666',
strokeWidth: '2px',
};
const transform = `rotate(${props.rotation}, 0, 0)`;
const muzzleWidth = 40;
const halfMuzzle = 20;
const height = 100;
const yBasis = 70;
const cubicBezierCurve = {
initialAxis: {
x: -halfMuzzle,
y: -yBasis,
},
initialControlPoint: {
x: -40,
y: height * 1.7,
},
endingControlPoint: {
x: 80,
y: height * 1.7,
},
endingAxis: {
x: muzzleWidth,
y: 0,
},
};
return (
<g transform={transform}>
<path
style={cannonPipeStyle}
d={pathFromBezierCurve(cubicBezierCurve)}
/>
<line
x1={-halfMuzzle}
y1={-yBasis}
x2={halfMuzzle}
y2={-yBasis}
style={cannonPipeStyle}
/>
</g>
);
};
CannonPipe.propTypes = {
rotation: PropTypes.number.isRequired,
};
export default CannonPipe;
复制代码
以后,从您的画布中移除 circle
组件并用 CannonBase
和 CannonPipe
来替代它。这是重构以后的代码:
import React from 'react';
import Sky from './Sky';
import Ground from './Ground';
import CannonBase from './CannonBase';
import CannonPipe from './CannonPipe';
const Canvas = () => {
const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
return (
<svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" viewBox={viewBox} > <Sky /> <Ground /> <CannonPipe rotation={45} /> <CannonBase /> </svg> ); }; export default Canvas; 复制代码
检查并运行您的应用,您将看到以下矢量图所呈现的画面:
您的游戏愈来愈完善了。您已经给游戏添加了背景元素(Sky
和 Ground
)和炮台。如今的问题是全部东西都是死的。因此,为了让事情变得更有趣,您要专一于完成炮台的瞄准功能。为此,您要给您的画布添加 onmousemove
时间监听器并在每次触发是刷新它(即,每次用户移动鼠标的时候),但这会下降您的游戏性能。
为了解决这种情况,您须要设置一个 固定的间隔 来检查最后一个鼠标的位置,以调整您的 CannonPipe
的角度。这个策略里您将继续使用 onmousemove
时间监听器,不一样的是这些事件不会一直触发从新渲染。它们只将更新游戏中的一个属性,而后间隔地使用这个属性来触发从新选择(经过更新 Redux store)。
这是您第一次要用 Redux 的 action 来更新应用程序的状态(或者是说炮台的角度)。像这样,您须要在 ./src/
目录下建立 actions
的新目录。在新目录里,您须要建立 index.js
文件并添加以下代码:
export const MOVE_OBJECTS = 'MOVE_OBJECTS';
export const moveObjects = mousePosition => ({
type: MOVE_OBJECTS,
mousePosition,
});
复制代码
注意: 您将调用
MOVE_OBJECTS
这个指令由于您不只会用它来更新炮台。在 本系列的下个教程里,您还将使用一样的指令来移动炮弹和飞碟。
在定义完 Redux action 后,您将重构您的 reducer(./src/reducers/
中的 index.js
文件)来处理它:
import { MOVE_OBJECTS } from '../actions';
import moveObjects from './moveObjects';
const initialState = {
angle: 45,
};
function reducer(state = initialState, action) {
switch (action.type) {
case MOVE_OBJECTS:
return moveObjects(state, action);
default:
return state;
}
}
export default reducer;
复制代码
这个文件的新版本执行一个 action,若是 type
是 MOVE_OBJECTS
, 它将调用 moveObjects
函数。须要注意的是,在定义该函数以前,您还须要在新版本里定义应用的初始化状态,它包含了值为 45
的 angle
属性。这定义了您应用程序里炮台的初始瞄准角度。
如您所见,moveObjects
函数就是一个 reducer。您将会在新文件里定义这个函数由于您将会有大量的 reducer
而您但愿更好的管理和维护它们。所以,在 ./src/reducers/
目录里建立 moveObjects.js
文件并添加以下代码:
import { calculateAngle } from '../utils/formulas';
function moveObjects(state, action) {
if (!action.mousePosition) return state;
const { x, y } = action.mousePosition;
const angle = calculateAngle(0, 0, x, y);
return {
...state,
angle,
};
}
export default moveObjects;
复制代码
这段代码很简单,它只是从 mousePosition
中获取 x
和 y
属性,并把它们传给 calculateAngle
函数来获取新的 angle
。最后,会用新的 angle 来生成新的 state。
如今,您可能已经发现您尚未在 formulas.js
文件中定义 calculateAngle
函数,对吗?关于如何用两个点来算出须要的角度已经超出了本章的讨论范围,若是您感兴趣的话,能够查阅 StackExchange 上的这个问题 来理解其背后究竟发生了什么。最后,您须要在 formulas.js
文件(./src/utils/formulas
)里添加以下函数:
export const radiansToDegrees = radians => ((radians * 180) / Math.PI);
// https://math.stackexchange.com/questions/714378/find-the-angle-that-creating-with-y-axis-in-degrees
export const calculateAngle = (x1, y1, x2, y2) => {
if (x2 >= 0 && y2 >= 0) {
return 90;
} else if (x2 < 0 && y2 >= 0) {
return -90;
}
const dividend = x2 - x1;
const divisor = y2 - y1;
const quotient = dividend / divisor;
return radiansToDegrees(Math.atan(quotient)) * -1;
};
复制代码
注意: 由 JavaScript 的
Math
对象提供的atan
函数来算出一个弧度值。您将须要把这个值转换为度数。这就是您为何要定义(和使用)radiansToDegrees
函数的缘由。
在以后新定义的 action 和 reducer 里,您将会继续用到这个函数。但您的游戏依赖于 Redux 来管理它的状态时,您须要将 moveObjects
映射到您 App
的 props
里。您将重构 Game
容器来完成这些操做。所以,打开 Game.js
文件(./src/containers
)并替换成以下代码:
import { connect } from 'react-redux';
import App from '../App';
import { moveObjects } from '../actions/index';
const mapStateToProps = state => ({
angle: state.angle,
});
const mapDispatchToProps = dispatch => ({
moveObjects: (mousePosition) => {
dispatch(moveObjects(mousePosition));
},
});
const Game = connect(
mapStateToProps,
mapDispatchToProps,
)(App);
export default Game;
复制代码
有了这些映射之后,您只须要把精力放在如何在 App
组件里使用它们。因此,打开 App.js
文件(在 ./src/
目录下)并替换成以下代码:
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import { getCanvasPosition } from './utils/formulas';
import Canvas from './components/Canvas';
class App extends Component {
componentDidMount() {
const self = this;
setInterval(() => {
self.props.moveObjects(self.canvasMousePosition);
}, 10);
}
trackMouse(event) {
this.canvasMousePosition = getCanvasPosition(event);
}
render() {
return (
<Canvas angle={this.props.angle} trackMouse={event => (this.trackMouse(event))} /> ); } } App.propTypes = { angle: PropTypes.number.isRequired, moveObjects: PropTypes.func.isRequired, }; export default App; 复制代码
您会发现咱们对这个新版本作了不少修改。总结以下:
componentDidMount
: 您定义了 生命周期方法 来间断地触发 moveObjects
指令。trackMouse
: 您定义了这个方法用来更新 App
组件的 canvasMousePosition
属性。这个属性受控于 moveObjects
指令。注意这个属性获取的不是 HTML 文档上的鼠标位置。而是引用您画布里的相对位置。您将在稍后定义 canvasMousePosition
函数。render
: 如今这个方法会把 angle
属性和 trackMouse
方法传入到 Canvas
组件里。这个组件将使用更新 angle
方式来渲染您的 cannon 组件并将 trackMouse
做为事件监听器添加到 svg
元素上。稍后您将更新这个组件。App.propTypes
: 如今您在这里定义了两个属性,angle
和 moveObjects
。首先是 angle
属性,它是用来定义您的炮台的瞄准角度度。其次是 moveObjects
函数,它将每隔一段时间更新您的 cannon 组件。如今已经更新完了 App
组件,接下来您须要往 formulas.js
文件里添加以下代码:
export const getCanvasPosition = (event) => {
// mouse position on auto-scaling canvas
// https://stackoverflow.com/a/10298843/1232793
const svg = document.getElementById('aliens-go-home-canvas');
const point = svg.createSVGPoint();
point.x = event.clientX;
point.y = event.clientY;
const { x, y } = point.matrixTransform(svg.getScreenCTM().inverse());
return {x, y};
};
复制代码
若是您对为何须要它感兴趣,在 StackOverflow 上您会找的答案。
最后一步是更新您的 Canvas
组件来使您的炮台可以瞄准。打开 Canvas.jsx
文件(在 ./src/components
里)并替换成以下内容:
import React from 'react';
import PropTypes from 'prop-types';
import Sky from './Sky';
import Ground from './Ground';
import CannonBase from './CannonBase';
import CannonPipe from './CannonPipe';
const Canvas = (props) => {
const viewBox = [window.innerWidth / -2, 100 - window.innerHeight, window.innerWidth, window.innerHeight];
return (
<svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" onMouseMove={props.trackMouse} viewBox={viewBox} > <Sky /> <Ground /> <CannonPipe rotation={props.angle} /> <CannonBase /> </svg> ); }; Canvas.propTypes = { angle: PropTypes.number.isRequired, trackMouse: PropTypes.func.isRequired, }; export default Canvas; 复制代码
当前版本和上一个版本的区别有:
CannonPipe.rotation
:这个属性再也不是写死的了。如今,它被绑定到 Redux store 所提供的状态里(经过 App
映射)。svg.onMouseMove
:您会将此事件监听器添加到画布中,以使得 App
组件能感知到鼠标的位置。Canvas.propTypes
:您会明确地为该组件定义它须要 angle
和 trackMouse
属性。就这样!您应该准备好来预览您炮台的瞄准功能。 切换到 terminal,并在项目的根目录运行 npm start
(若是它尚未运行)。 而后,在浏览器里打开 http://localhost:3000/ 并移动鼠标。您的炮台将跟随鼠标旋转起来。
多有趣啊!?
“我用 React, Redux 和 SVG 建立了一个能够瞄准的炮台。这多有趣啊!?” 在这里 tweet 咱们
![]()
在本系列的第一部分,您学习了一些重要的主题,它将帮助您建立一个完整游戏。您也使用了 create-react-app
来建立您的项目并建立了一些游戏元素,如炮台、天空和大地。最后,您给炮台添加了瞄准功能。有了这些元素,您就能其余的 React 组件并让他们动起来。
在本系列的下篇文章中,您将再创造一些组件,来让一些飞碟随机出如今预约的位置。以后,您将使您的炮台可以发射一些炮弹。这实在使人激动!
请保持关注!
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。