使用React、 Redux 和 SVG 开发游戏
本文翻译自:Developing Games with React, Redux, and SVG - Part 1
转载英文原作请注明原作者与出处。转载本译本请注明译者与译者博客
这段太长别看:在这系列教程中,你将学会如何使用React和Redux去控制一堆SVG元素来制作一个游戏。这一个系列所带给你的知识也可以让你使用React和Redux去制作其他的动画和特效,并不仅限于游戏。你可以在这里找到第一部分的全部代码:Aliens Go Home - Part 1
React 游戏 :外星人,滚回家!
在本系列开发的游戏名为《外星人,滚回家!》(Aliens , Go Home !)。这个游戏很简单:你用一个加农炮,来消灭试图入侵地球的飞碟。你必须通过准确点击SVG元素来发射加农炮。
如果你忍不住好奇心,可以先去看看可以试玩的最终版本(链接已经挂了,不知道作者什么时候恢复,你可以clone第三部分的代码自己运行试玩)。但是别玩太久!你还有正事要做呢!
知识储备
学习本系列之前,你需要一些知识储备:
- Web开发基本知识,主要是JavaScript。
- 有node环境。
- 会用Node包管理工具npm。
- 你并不需要十分精通JavaScript、React和SVG。当然,如果你玩的很6,你学起来会很轻松,并且能很快抓住重点(译者:建议还是先学点儿React和Redux吧,不然能做出来但是看不懂的)。
本系列还包含了一些值得关注的其他相关文章、帖子、文档的链接,对于一些话题这里面可能有更好的解释。
开始之前
译者:下面这些都是在安利你使用Git和GitHub。我觉得不用看了。因为我不觉得有 会React却不知道Git也没有GitHub 的这种工程师存在。不过负责一点,我还是全翻译了。
尽管前面的知识储备章节没有提到Git,但是它真的是一个很好地工具。所有的职业开发者在开发项目时都会使用Git或者其他版本控制系统比如SVN,哪怕是很小的玩具项目。
你写的项目总是要进行版本控制和代码备份的,你不用为此支付费用。你可以使用GitHub(最好的)这种平台或者BitBucket(说实话,也不错)来做这件事。
除了可以确保你的代码安全地保留下来,上面这些工具还可以让你牢牢掌控住自己的开发进程。例如,如果你用了Git,然后你写了一个全是BUG的版本,你可以仅用几条命令就回到上次一记录的版本。
另一个好处就是,在学习本系列教程的时候,你可以每做完一个章节就commit一次。这样你可以清除地知道每个章节你都进行了哪些修改和新增,这让你学习教程变得更加轻松。
所以,帮自己一个忙,装个Git吧。然后,在Github创建一个账号并上传你的代码吧!每次做完一个章节,都commit一下。哦对了,不要忘记push。
使用 Create-React-App 创建一个 React 项目
最快速创建我们的项目的方式,是使用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
在创建了项目并且移除无用文件之后,你应该安装并且配置Redux来统一管理应用的状态树。同时你也应该安装PropTypes来帮助你避免数据类型引发的错误。安装着两个工具只需要一条命令就够了:
npm i redux react-redux prop-types
像你看到的一样,上面的命令行包含了一个第三方NPM包react-redux。尽管你可以直接在React上使用redux而不是redux-react,但是并不推荐这么做。react-redux对React做了一些优化,如果我们手动来做这些事的话就太麻烦了。
配置 Redux 并使用 PropTypes
你可以通过适当的配置这些包,来让你的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组件
在本系列教程中你将会看到,使用在react中创建svg组件非常简单。事实上,创建HTML组件和创建SVG组件几乎没有什么区别。唯一的区别是,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 , path 标签和三次贝塞尔曲线
有三种方式来完成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上进行练习。
创建一个 React 画布组件
现在你已经有了一个结构化的项目,并且你已经知道了我们需要用到的所有的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%;
}
这会让你的应用铺满整个屏幕。并且禁止滚动,溢出部分隐藏。这时你再次运行你的应用,会发现之前的左上角四分之一圆跑到底部中心并且变成整圆了。
创建 React 天空组件
完成了画布铺满屏幕和原点重定位的工作之后,是时候开始制作真正的游戏元素了。你可以从游戏的背景开始——天空组件。跟前面一样,在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如果你看出来了欢迎指出。