【翻译】使用React、 Redux 和 SVG 开发游戏(一)

使用React、 Redux 和 SVG 开发游戏

本文翻译自:Developing Games with React, Redux, and SVG - Part 1css

转载英文原做请注明原做者与出处。转载本译本请注明译者译者博客html

这段太长别看:在这系列教程中,你将学会如何使用ReactRedux去控制一堆SVG元素来制做一个游戏。这一个系列所带给你的知识也可让你使用ReactRedux去制做其余的动画和特效,并不只限于游戏。你能够在这里找到第一部分的所有代码:Aliens Go Home - Part 1node

React 游戏 :外星人,滚回家!

在本系列开发的游戏名为《外星人,滚回家!》(Aliens , Go Home !)。这个游戏很简单:你用一个加农炮,来消灭试图入侵地球的飞碟。你必须经过准确点击SVG元素来发射加农炮。 若是你忍不住好奇心,能够先去看看能够试玩的最终版本(连接已经挂了,不知道做者何时恢复,你能够clone第三部分的代码本身运行试玩)。可是别玩过久!你还有正事要作呢!react


第三部分的最终代码git


知识储备

学习本系列以前,你须要一些知识储备:github

  • Web开发基本知识,主要是JavaScript。
  • 有node环境。
  • 会用Node包管理工具npm。
  • 你并不须要十分精通JavaScript、React和SVG。固然,若是你玩的很6,你学起来会很轻松,而且能很快抓住重点(译者:建议仍是先学点儿React和Redux吧,否则能作出来可是看不懂的)。

本系列还包含了一些值得关注的其余相关文章、帖子、文档的连接,对于一些话题这里面可能有更好的解释。web

开始以前

译者:下面这些都是在安利你使用GitGitHub。我以为不用看了。由于我不以为有 会React殊不知道Git也没有GitHub 的这种工程师存在。不过负责一点,我仍是全翻译了。npm

尽管前面的知识储备章节没有提到Git,可是它真的是一个很好地工具。全部的职业开发者在开发项目时都会使用Git或者其余版本控制系统好比SVN,哪怕是很小的玩具项目。json

你写的项目老是要进行版本控制和代码备份的,你不用为此支付费用。你可使用GitHub(最好的)这种平台或者BitBucket(说实话,也不错)来作这件事。redux

除了能够确保你的代码安全地保留下来,上面这些工具还可让你紧紧掌控住本身的开发进程。例如,若是你用了Git,而后你写了一个全是BUG的版本,你能够仅用几条命令就回到上次一记录的版本。

另外一个好处就是,在学习本系列教程的时候,你能够每作完一个章节就commit一次。这样你能够清除地知道每一个章节你都进行了哪些修改和新增,这让你学习教程变得更加轻松。

因此,帮本身一个忙,装个Git吧。而后,在Github建立一个帐号并上传你的代码吧!每次作完一个章节,都commit一下。哦对了,不要忘记push

使用 Create-React-App 建立一个 React 项目

最快速建立咱们的项目的方式,是使用create-react-app。也许你已经知道(不知道也不要紧),create-react-app是一个Facebook开发的脚手架,帮助React开发者瞬间生成一个项目的基础目录结构。安装了Nodenpm以后,你能够安装并直接执行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.cssApp组件很重要,可是样式将会委托给其余组件来定义。
  • App.test.js:测试相关的内容可能会在其余文章处理,可是本次教程不涉及。
  • logo.svg:在这个游戏里你不须要React的logo。

移除文件后,启动程序可能会抛出异常,由于咱们把LOGO和CSS删了。只要把App.js中LOGO和CSS的import语句也删掉就ok了。

咱们重构一下src/App.jsrender()方法:

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
复制代码

像你看到的同样,上面的命令行包含了一个第三方NPMreact-redux。尽管你能够直接在React上使用redux而不是redux-react,可是并不推荐这么作。react-reduxReact作了一些优化,若是咱们手动来作这些事的话就太麻烦了。

配置 Redux 并使用 PropTypes

你能够经过适当的配置这些包,来让你的app使用redux。过程很简单,你须要建立一个container组件,一个presentational组件,和一个reducercontainer组件和presentational组件的区别在于,前者只是用来把presentational组件connectRedux的。你将建立的第三个组件是一个reducer,是Redux store的核心组件。这种组件负责处理页面行为触发的事件,并调用相应的事件处理函数,并响应这些页面行为所做出的状态树的修改。

若是上面这些概念你都不熟悉,你能够读这篇文章来了解containerpresentational组件的概念。你还能够经过这篇文章Redux教程来了解Redux中的actionreducerstore。虽然很是建议学习这些文章,可是你也能够先不学,先把本系列教程作完。

咱们最好从建立一个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.messageApp组件的message propsGame.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的矢量图形。svgHTML很是类似。他们都是基于XML的标记语言而且均可以跟其余web标准很好地写做共存好比cssdom。这意味着你能够给svg跟其余普通元素同样地赋予样式,包括动画效果。

在本系列教程中,你会用react建立不止一打的svg元素。你还会组装svg来造成你的游戏元素,好比你的加农炮和炮弹!

关于svg更加严禁周密的阐释不在本系列教程范围,并且会让文章过于冗长。若是你期待学习更多svg的知识,能够看这两篇文章:

然而,开始以前,一些基础少许的svg知识须要明白。

  • svgdom的组合让开发者能够轻松地在react中使用svg
  • svg坐标系跟笛卡尔坐标系很类似可是是反过来的。这意味着Y轴朝下是正。X轴不变。这种表现能够经过调用transformation轻易地改掉。然而,为了避免让其余开发者迷惑,咱们不会修改默认的坐标体系。你很快会习惯的~
  • 另一件你须要知道的事情是,svg提供了更多的形状标签,好比rectcirclepath。你能够很是简单的将他们包裹在HTML标签里。在画svg图形或者建立react中的svg组件以前你必须先定义好svg标签。将图形们包裹在<svg></svg>中。

SVG , path 标签和三次贝塞尔曲线

有三种方式来完成svg的绘制。第一种,你能够直接使用rectcircleline来绘制基本形状。他们可能不是很灵活,可是画基本形状很好用。他们的含义跟名字同样,长方形,圈儿和线。

第二种方式是把基本图形进行组合,生成复杂的图形。好比,你能够作一个宽高相等的长方形,你就获得了一个正方形,而后用两条line来作个三角两边扣在正方形上面,最后,你就画出了一个房子。然而这种方式的灵活性仍是有限制。

第三种方式就是使用path标签。这种方式让开发者拥有绘制很是复杂的图形的能力。它经过接受一组命令以指示浏览器如何绘制图形来实现。好比你要画一个大写的L,你能够建立一个带有三个命令的path元素。

  1. M 20 20:这条命令指示浏览器拿起‘画笔’前往(20,20)这个坐标点。
  2. v 80:这条命令指示浏览器画一条线,从上条命令的点画至Y轴80的位置。
  3. 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>
复制代码

命令的含义以下:

  1. (20,20)开始绘制;
  2. 第一个控制点是(20,110)
  3. 第二个控制点是(110,110,)
  4. 在点(110,20)处结束绘制;

若是你不能确切地明白贝塞尔曲线的工做原理,不要担忧。在本系列中你会有练习的机会的。除此以外,你能够在网上找到不少教程,而且能够常常在JSFiddleCodepen上进行练习。

建立一个 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.jsxindex.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减去屏幕高度的值赋予该属性。
  • widthheight规定了可视区域的范围有多大。

除了设置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.jsGround.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}
  `;
};

复制代码

这个代码很简单,他只是根据传入的四个参数来返回一个贝塞尔曲线路径字符串。有了这个文件,你如今能够开始建立你的加农炮了。为了让代码更加结构化。你能够把加农炮拆分为两部分:CannonBaseCannonPipe(炮主体和炮管)。

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标签移除,把CannonBaseCannonPipe添加进去:

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;
复制代码

运行你的程序,到目前为止,你的应用应该长下面这个样子了:

image

让你的大炮瞄准

你的游戏开发正在稳步进行。你已经建立了背景和你的加农炮。如今问题是全部东西都是毫无生机的。因此,咱们应该让你的大炮进行瞄准,增长一点儿动态。你能够添加mousemove事件,来不断从新渲染你的大炮以达到瞄准的效果。可是这样会让你的游戏性能降低。

为了克服这种情况,你应该设置一个统一的计时器,定时检测鼠标的位置并更新你的CannonPipe的角度。即便更换了战略,你仍是要监听mousemove事件,不一样的是,此次不会触发重渲染了。它只会更新你游戏里的属性,而后计时器会使用这些属性来更新reduxstore而后触发页面更新。

这是第一次你须要使用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中提取xy坐标,使用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 actionreducer以后,你要开始使用他们了。由于你的游戏依赖于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:你已经明确地定义了画布组件须要angletrackMouse属性。

有趣吗?

总结和接下来的步骤

在本教程第第一部分,你已经学会了一些能够支撑你完成此次开发的重要的知识点。你已经会用create-react-app建立项目了。你还会建立一些游戏元素,好比天空、陆地和加农炮。最后你完成了加农炮的瞄准工做。有了这些,你已经准备好进行剩余部分react组件的开发工做,并让他们动起来了。

在本教程的下一部分你将会建立这些组件,而后你将会作一些在预约位置范围随机出现的飞碟。固然你还会完成射击工做,让你的加农炮把它们打下来!Awesome!

敬请期待!

译者:第二部分 大概下周末发布 已发布。上面的内容若有错误,欢迎指出。代码错误您也可直接去做者原文评论。翻译错误请直接指出。很是感谢!可能会有错别字,我眼睛已经要看花了2333若是你看出来了欢迎指出。

相关文章
相关标签/搜索