本文翻译自:Developing Games with React, Redux, and SVG - Part 1css
转载英文原做请注明原做者与出处。转载本译本请注明译者与译者博客html
这段太长别看:在这系列教程中,你将学会如何使用React
和Redux
去控制一堆SVG
元素来制做一个游戏。这一个系列所带给你的知识也可让你使用React
和Redux
去制做其余的动画和特效,并不只限于游戏。你能够在这里找到第一部分的所有代码:Aliens Go Home - Part 1node
在本系列开发的游戏名为《外星人,滚回家!》(Aliens , Go Home !)。这个游戏很简单:你用一个加农炮,来消灭试图入侵地球的飞碟。你必须经过准确点击SVG
元素来发射加农炮。 若是你忍不住好奇心,能够先去看看能够试玩的最终版本(连接已经挂了,不知道做者何时恢复,你能够clone第三部分的代码本身运行试玩)。可是别玩过久!你还有正事要作呢!react
第三部分的最终代码git
学习本系列以前,你须要一些知识储备:github
本系列还包含了一些值得关注的其余相关文章、帖子、文档的连接,对于一些话题这里面可能有更好的解释。web
译者:下面这些都是在安利你使用Git
和GitHub
。我以为不用看了。由于我不以为有 会React
殊不知道Git
也没有GitHub
的这种工程师存在。不过负责一点,我仍是全翻译了。npm
尽管前面的知识储备章节没有提到Git
,可是它真的是一个很好地工具。全部的职业开发者在开发项目时都会使用Git
或者其余版本控制系统好比SVN
,哪怕是很小的玩具项目。json
你写的项目老是要进行版本控制和代码备份的,你不用为此支付费用。你可使用GitHub
(最好的)这种平台或者BitBucket
(说实话,也不错)来作这件事。redux
除了能够确保你的代码安全地保留下来,上面这些工具还可让你紧紧掌控住本身的开发进程。例如,若是你用了Git
,而后你写了一个全是BUG
的版本,你能够仅用几条命令就回到上次一记录的版本。
另外一个好处就是,在学习本系列教程的时候,你能够每作完一个章节就commit
一次。这样你能够清除地知道每一个章节你都进行了哪些修改和新增,这让你学习教程变得更加轻松。
因此,帮本身一个忙,装个Git
吧。而后,在Github建立一个帐号并上传你的代码吧!每次作完一个章节,都commit
一下。哦对了,不要忘记push
。
最快速建立咱们的项目的方式,是使用create-react-app
。也许你已经知道(不知道也不要紧),create-react-app
是一个Facebook开发的脚手架,帮助React
开发者瞬间生成一个项目的基础目录结构。安装了Node
和npm
以后,你能够安装并直接执行create-react-app
来建立项目。
# 使用 npx 将会下载
# create-react-app 而且执行它
npx create-react-app aliens-go-home
# 进入项目目录
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
很是流行,它有清晰的文档而且社区支持也很是棒。若是你对它感兴趣,能够去这里更进一步地了解它:official create-react-app GitHub repository。这是它的使用手册:create-react-app user guides
如今,你须要作的是:移除一些咱们不须要的东西。好比,你能够把下面这些文件删掉。
App.css
:App
组件很重要,可是样式将会委托给其余组件来定义。App.test.js
:测试相关的内容可能会在其余文章处理,可是本次教程不涉及。logo.svg
:在这个游戏里你不须要React
的logo。移除文件后,启动程序可能会抛出异常,由于咱们把LOGO和CSS删了。只要把App.js
中LOGO和CSS的import
语句也删掉就ok了。
咱们重构一下src/App.js
的render()
方法:
render() {
return (
<div className="App">
<h1>We will create an awesome game with React, Redux, and SVG!</h1>
</div>
);
}
复制代码
以后npm start
运行你的项目。
别忘了每个章节都commit
一次代码哦。
在建立了项目而且移除无用文件以后,你应该安装而且配置Redux来统一管理应用的状态树。同时你也应该安装PropTypes来帮助你避免数据类型引起的错误。安装着两个工具只须要一条命令就够了:
npm i redux react-redux prop-types
复制代码
像你看到的同样,上面的命令行包含了一个第三方NPM
包react-redux
。尽管你能够直接在React
上使用redux
而不是redux-react
,可是并不推荐这么作。react-redux
对React
作了一些优化,若是咱们手动来作这些事的话就太麻烦了。
你能够经过适当的配置这些包,来让你的app使用redux
。过程很简单,你须要建立一个container
组件,一个presentational
组件,和一个reducer
。container
组件和presentational
组件的区别在于,前者只是用来把presentational
组件connect
到Redux
的。你将建立的第三个组件是一个reducer
,是Redux store
的核心组件。这种组件负责处理页面行为触发的事件,并调用相应的事件处理函数,并响应这些页面行为所做出的状态树的修改。
若是上面这些概念你都不熟悉,你能够读这篇文章来了解container
和presentational
组件的概念。你还能够经过这篇文章Redux教程来了解Redux
中的action
、reducer
和store
。虽然很是建议学习这些文章,可是你也能够先不学,先把本系列教程作完。
咱们最好从建立一个reducer
开始,由于这家伙不依赖其余任何人(事实上,是反过来的,别人都依赖它)。为了让代码更加结构化,你能够在src
中建立一个reducers
文件夹专门用来存放reducer
。而后咱们在里面添加一个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
初始化了一个简单的app的状态message
。内容是“整合React和Redux很容易,不是吗?”。很快咱们将定义一些action
而后在这个文件中处理它们。
下一步,你能够重构App
组件,来给用户展现这条message
。你已经安装了prop-ypes
,是时候使用它了。用下面的代码替换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
定义你的组件指望获得的数据类型很是容易。你只须要定义App
组件的propTypes
属性,在里面规定接受的数据类型就能够了。这里有一些关于如何定义基本的数据类型验证清单,好比这个,这个还有这个。若是须要的话,你能够看一下。
尽管你已经定义了你的App
组件须要渲染什么以及初始化了你的Redux store
,你还须要作一些事情把这些家伙捆绑到一块儿,如今他们是松散的,没什么联系。这就是container
组件要作的事了!跟前面同样,为了代码的结构化,你能够在src
中建立一个containers
文件夹用来专门存放container
组件。而后在src/containers
中建立一个Game.js
。这个container
组件将使用redux-react
提供了connect
工具来连接state.message
和App
组件的message props
,Game.js
的代码以下:
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
把全部模块联通。咱们在index.js
中渠初始化Redux store
,把它传入Game
容器——它将会获取message
并传递给App
。重构后的代码以下:
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__(),
//__REDUX_DEVTOOLS_EXTENSION__是一个调试扩展工具,不传也不要紧
);
/* eslint-enable */
ReactDOM.render(
<Provider store={store}>
<Game />
</Provider>,
document.getElementById('root'),
);
registerServiceWorker();
复制代码
你已经完成了这一部分!你能够去项目根目录执行npm start
来看一下知否正常工做了。
在本系列教程中你将会看到,使用在react
中建立svg
组件很是简单。事实上,建立HTML
组件和建立SVG
组件几乎没有什么区别。惟一的区别是,svg
建立出的元素都是在画在一个svg
画布上的。
不过,在开始以前,先来一块儿快速了解一下svg
相关知识仍是很重要的。
svg
是最酷、最灵活的web标准之一。svg
表明一种标记语言Scalable Vector Graphics
。他让开发者有能力绘制2D的矢量图形。svg
和HTML
很是类似。他们都是基于XML
的标记语言而且均可以跟其余web标准很好地写做共存好比css
和dom
。这意味着你能够给svg
跟其余普通元素同样地赋予样式,包括动画效果。
在本系列教程中,你会用react
建立不止一打的svg
元素。你还会组装svg
来造成你的游戏元素,好比你的加农炮和炮弹!
关于svg
更加严禁周密的阐释不在本系列教程范围,并且会让文章过于冗长。若是你期待学习更多svg
的知识,能够看这两篇文章:
然而,开始以前,一些基础少许的svg
知识须要明白。
svg
和dom
的组合让开发者能够轻松地在react
中使用svg
。svg
坐标系跟笛卡尔坐标系很类似可是是反过来的。这意味着Y轴朝下是正。X轴不变。这种表现能够经过调用transformation轻易地改掉。然而,为了避免让其余开发者迷惑,咱们不会修改默认的坐标体系。你很快会习惯的~svg
提供了更多的形状标签,好比rect
、circle
和path
。你能够很是简单的将他们包裹在HTML标签里。在画svg
图形或者建立react
中的svg
组件以前你必须先定义好svg
标签。将图形们包裹在<svg></svg>
中。有三种方式来完成svg
的绘制。第一种,你能够直接使用rect
,circle
和line
来绘制基本形状。他们可能不是很灵活,可是画基本形状很好用。他们的含义跟名字同样,长方形,圈儿和线。
第二种方式是把基本图形进行组合,生成复杂的图形。好比,你能够作一个宽高相等的长方形,你就获得了一个正方形,而后用两条line
来作个三角两边扣在正方形上面,最后,你就画出了一个房子。然而这种方式的灵活性仍是有限制。
第三种方式就是使用path
标签。这种方式让开发者拥有绘制很是复杂的图形的能力。它经过接受一组命令以指示浏览器如何绘制图形来实现。好比你要画一个大写的L
,你能够建立一个带有三个命令的path
元素。
M 20 20
:这条命令指示浏览器拿起‘画笔’前往(20,20)这个坐标点。v 80
:这条命令指示浏览器画一条线,从上条命令的点画至Y轴80的位置。H 50
:这条命令指示浏览器画一条线,从上条命令的终点画至X轴50的位置。<svg>
<path d="M 20 20 V 80 H 50" stroke="black" stroke-width="2" fill="transparent" />
</svg>
复制代码
path
标签还能够接受不少其余的命令。其中最为重要的就是三次贝塞尔曲线。这个命令可让你经过两个参照点和两个控制点来绘制出平滑的曲线。
在Mozialla教程中,是这样阐释svg
中的三次贝塞尔曲线的:
"三次贝塞尔曲线的每一个点都有两个控制点。所以你须要设定好三个点来建立贝塞尔曲线。最后一个就是你将要绘制的终点。另外两个是控制点。[......]控制点从本质上描述了你的线的每一个起点的斜率。贝塞尔函数会依照你设立的两个控制点和结束点来绘制平滑的曲线。"
例如,画一个'U'形状的曲线:
<svg>
<path d="M 20 20 C 20 110, 110 110, 110 20" stroke="black" fill="transparent"/>
</svg>
复制代码
命令的含义以下:
(20,20)
开始绘制;(20,110)
;(110,110,)
;(110,20)
处结束绘制;若是你不能确切地明白贝塞尔曲线的工做原理,不要担忧。在本系列中你会有练习的机会的。除此以外,你能够在网上找到不少教程,而且能够常常在JSFiddle和Codepen上进行练习。
如今你已经有了一个结构化的项目,而且你已经知道了咱们须要用到的全部的svg
的知识,是时候开始动手作游戏了!你须要制做的第一个组件就是画布组件(不是那个Canvas),你将在这上面绘制你的游戏元素。
这个组建的行为是一个presentational
组件。像以前同样,你能够建立一个文件夹来专门存放这类组件。建立一个src/components
文件夹。由于咱们接下来要建立的组件是一个画布,那么给这个组件起名为Canvas
再好不过了。
译者:再次强调一下,本文全部Canvas和画布等词语,都不是Canvas标签,本文跟Cavnas技术没有关系。
在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;
复制代码
若是你这时候运行你的项目,你会看见浏览器上只有四分之一个圆在左上角。这是由于默认坐标系的缘由——左上角为(0,0)
。除此以外,你会发现,svg
没有铺满屏幕。
为了更好玩,你可让你的画布铺满整个屏幕。你可能还想改一下坐标原点的位置,让它处于X的中间而且更靠近底部(一下子你将会把加农炮放在坐标中心)。要完成上面的事情,你须要修改两个文件。Canvas.jsx
和index.css
。
你能够先修改画布组件的代码,像下面这样:
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
属性。这个属性作得事情是:让你的画布内容只填满部分容器(在这里是浏览器的可视范围)。你也看到了,这个属性接受4个参数:
译者:建议看看这篇博客,应该就懂了:理解SVG viewport,viewBox,preserveAspectRatio缩放
min-x
:这个属性的值定义了可视的作左侧的点。因此,为了让原点在屏幕中心,你须要把屏幕宽度除以-2
复制给这个属性。注意,这里你要使用-2
来让你的画布在原点左右展现相同的长度,而且左负右正。min-y
:一样,咱们须要原点在Y方向靠近底部,可是留有100
的空余空间。因而将100减去屏幕高度的值赋予该属性。width
和height
规定了可视区域的范围有多大。除了设置viewBox
以外,你必须设置一个属性叫作preserveAspectRatio
。而且赋值为xMaxYMax none
来使svg
和它全部子元素的缩放都统一。
重构完Canvas.jsx
以后你须要编写一下样式src/index.css
html, body {
overflow: hidden;
height: 100%;
}
复制代码
这会让你的应用铺满整个屏幕。而且禁止滚动,溢出部分隐藏。这时你再次运行你的应用,会发现以前的左上角四分之一圆跑到底部中心而且变成整圆了。
完成了画布铺满屏幕和原点重定位的工做以后,是时候开始制做真正的游戏元素了。你能够从游戏的背景开始——天空组件。跟前面同样,在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
个点,不论用户的分辨率或者屏幕方向如何,都会有一致的视觉体验。这样,你就有能力去控制全部的飞碟,知道他们将会在这些点(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;
复制代码
如今你在上浏览器查看你的应用,会发现已经有蓝色的天空背景了。
注意:若是你先制造circle
元素,后制做天空的话,那么你就看不到圆了。由于svg
不支持z-index
相似的属性。svg
彻底根据定义顺序来决定谁把谁盖住。因此若是你颠倒顺序,就看不见圆了。
建立了游戏元素天空以后,你能够开始建立地面组件了。一样的步骤,建立src/components/Ground.jsx
并编写以下代码:
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;
复制代码
这个组件没什么花哨的,就是一个rect
和一条line
。然而你可能发现了,这个组件用了一个常量宽度5000
。因此,定义一个常量宽度会是一个好主意。那么这个常量应该放在哪里呢?咱们能够添加一个constants.js
文件来专门存储常量。而后把它放在一个叫作utils
的文件夹中。
建立src/utils
文件夹并建立src/utils/constants.js
文件并编写以下代码:
// very wide to provide as full screen feeling
export const skyAndGroundWidth = 5000;
复制代码
以后,你能够重构Sky.js
和Ground.js
来使用这些常量。别忘了把Ground
组件添加到画布组件中去。注意顺序,顺序应该是Sky
->Ground
->circle
。若是你没办法独立完成这部分,参考此次提交。
你已经在你的游戏里定义了天空和地面组件。下一步,你会想作一些有趣的事儿了。你能够建立一些元素来表明你的加农炮。这些元素组成的组件可能比前两个组件复杂一些。他们可能须要不少行的代码,不过这是因为咱们要使用贝塞尔曲线来绘制。
你可能记得,定义一个贝塞尔曲线依赖于四个点。一个path开始点,和三个贝塞尔曲线相关的点(一个结束点两个控制点)。这些定义在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}
`;
};
复制代码
这个代码很简单,他只是根据传入的四个参数来返回一个贝塞尔曲线路径字符串。有了这个文件,你如今能够开始建立你的加农炮了。为了让代码更加结构化。你能够把加农炮拆分为两部分:CannonBase
和CannonPipe
(炮主体和炮管)。
在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;
复制代码
这个元素除了贝塞尔出现之外没有什么新东西了。最后浏览器会绘制一个深棕色描边浅棕色填充的加农炮主体。
加农炮管的组件代码和上面的很像。不一样点是,它将使用不一样的颜色,而且将传入其余点参数给pathFromBezierCurve
公式来获取炮管绘制路径。除此以外,这个元素还会使用transform
属性来伪装炮管的转动。编辑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;
复制代码
运行你的程序,到目前为止,你的应用应该长下面这个样子了:
你的游戏开发正在稳步进行。你已经建立了背景和你的加农炮。如今问题是全部东西都是毫无生机的。因此,咱们应该让你的大炮进行瞄准,增长一点儿动态。你能够添加mousemove
事件,来不断从新渲染你的大炮以达到瞄准的效果。可是这样会让你的游戏性能降低。
为了克服这种情况,你应该设置一个统一的计时器,定时检测鼠标的位置并更新你的CannonPipe
的角度。即便更换了战略,你仍是要监听mousemove
事件,不一样的是,此次不会触发重渲染
了。它只会更新你游戏里的属性,而后计时器会使用这些属性来更新redux
的store
而后触发页面更新。
这是第一次你须要使用redux action
来更新你的应用的store
。一样的,你要建立一个文件夹叫作actions
来放置全部的redux action
。建立src/actions/index.js
,并编写以下代码:
export const MOVE_OBJECTS = 'MOVE_OBJECTS';
export const moveObjects = mousePosition => ({
type: MOVE_OBJECTS,
mousePosition,
});
复制代码
注意:这里给这个action起名字叫MOVE_OBJECT
。由于在下一章节还会用到这个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;
复制代码
这个文件如今的版本接管了一个动做,若是动做类型是MOVE_OBJECTS
,它就会调用一个moveObject
方法。你还须要定义这个方法,不过在这以前你须要注意一下,这里的初始化状态也改变了。添加了一个45的angle
。这将时你的应用启动时炮管的初始角度。
像你看到的同样,moveObject
也是一个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
计算一个新的角度。最后生成一个新的state
。
你应该注意到了calculateAngle
尚未在formulas.js
中定义呢。两点角度计算背后的数学知识不是本教程涉及的,若是你感兴趣,能够去这里看看。src/utils/formulas.js
中增长的代码以下:
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;
};
复制代码
注意:atan
方法是JavaScript
方法提供的对象。返回弧度制。你须要的是角度制。这就是为何还须要一个radiansToDegrees
函数来处理。
定义好你的react action
和reducer
以后,你要开始使用他们了。由于你的游戏依赖于redux
来管理状态,你须要map
你的moveObject
方法到App
组件的props
上。重构Game.js
:
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;
复制代码
有了这些mapping
,你能够专一于App
组件。那么,打开/src/App.js
来重构一下:
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
:你定义了一个生命周期函数启动一个统一的计时器,来触发moveObject
动做。trackMouse
:你定义了这个方法更新App
组件的canvasMousePosition
属性。这个属性被moveObject
方法使用。注意,这个位置并非HTML
中鼠标的位置,而是相对于咱们的画布而言的坐标位置。咱们稍后会定义获取这个位置的方法。App.propTypes
:你如今定义了两个属性以及数据类型验证。angle
是炮管的角度。moveObject
是移动游戏元素的方法。两个都是必传属性。下面咱们在formulas.js
中添加getCanvasPosition
方法:
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的这个话题。
最后一起须要完成的是,让你的加农炮瞄准行为成为一个画布的组件。重构src/Canvas.jsx
。
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
组件mapping
)。svg.onMouseMove
:你已经添加了鼠标移动事件监听,让你的组件能够察觉到鼠标位置的变化。Canvas.propTypes
:你已经明确地定义了画布组件须要angle
和trackMouse
属性。有趣吗?
在本教程第第一部分,你已经学会了一些能够支撑你完成此次开发的重要的知识点。你已经会用create-react-app
建立项目了。你还会建立一些游戏元素,好比天空、陆地和加农炮。最后你完成了加农炮的瞄准工做。有了这些,你已经准备好进行剩余部分react
组件的开发工做,并让他们动起来了。
在本教程的下一部分你将会建立这些组件,而后你将会作一些在预约位置范围随机出现的飞碟。固然你还会完成射击工做,让你的加农炮把它们打下来!Awesome!
敬请期待!
译者:第二部分 大概下周末发布 已发布。上面的内容若有错误,欢迎指出。代码错误您也可直接去做者原文评论。翻译错误请直接指出。很是感谢!可能会有错别字,我眼睛已经要看花了2333若是你看出来了欢迎指出。