[译] 使用 React、Redux 和 SVG 开发游戏 — Part 1

使用 React、Redux 和 SVG 开发游戏 — Part 1

TL;DR: 在这个系列里,您将学会用 React 和 Redux 来控制一些 SVG 元素来建立一个游戏。经过本系列的学习,您不只能建立游戏,还能用 React 和 Redux 来开发其余类型的动画。您能够在这个 GitHub 仓库: Aliens Go Home - Part 1 下找到最终的开发代码。css


React 游戏:Aliens, Go Home!

在这个系列里您将要开发的游戏叫作 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 项目

首先您要用 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.cssApp 是一个很重要的组件可是他的样式定义须要交给其余组件来处理;
  • 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 上!

安装 Redux 和 PropTypes

在启动了 React 项目并删掉了一些没用的文件以后,您将安装和配置 Redux 来使它成为 您应用程序的惟一数据源. 您也须要安装 PropTypes这个工具将帮助您避免常见的错误。两个工具能够用一行命令来安装:

npm i redux react-redux prop-types
复制代码

如您所见,这行命令包含了第三个 NPM 包:react-redux。尽管您能够直接在 React 里面使用 Redux,但它不是最佳选择。react-redux 对咱们本来须要繁琐手动处理的性能优化有所帮助

配置 Redux 和使用 PropTypes

有了这些包,您就能在您的应用里配置和使用 Redux 了。这个过程很简单,您将须要建立一个 container 组件,一个 presentational 组件,以及一个 reducer。容器组件和视图组件的区别在于,首先须要将视图组件 链接 到 Redux。reducer 是您将要建立的第三个组件,它是 Redux store 里的核心组件。这类组件主要用于当您的应用触发事件后来获取对应的 actions 并根据这些 actions 来调用关联的函数去修改相应的状态。

若是您对这些概念还不熟悉,您能够阅读 这篇文章来更好的理解视图组件和容器组件 以及经过 这篇 Redux 使用教程来学习关于 actionsreducers、和 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-reduxconnect 方法并往 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 组件

在这个系列您将看到,用 React 建立 SVG 组件是很是轻松的事。事实上,用 HTML 和 SVG 建立 React 组件几乎没有区别。基本上,惟一的区别就是 SVG 引入了一些新的元素,而这些元素都是在 SVG 上绘制的。

话虽如此,在用 SVG 和 React 建立组件以前,简单了解下 SVG 仍是颇有帮助的。

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 引入了许多的新元素(例如 circlerect、和 path)。 要使用这些元素,不能简单地在 HTML 元素中定义它们。首先, 您必须在您想要绘制的 SVG 组件里定义一个 svg 元素(画布)。

SVG,Path 元素和三次贝塞尔曲线

使用 SVG 绘制元素能够经过三种方式完成。首先,您可使用像 rectcircleline 这些元素。尽管它们用起来不怎么方便。顾名思义,它们只能让您绘制一些简单的图形。

第二种方式是把它们组合成更为复杂的图形。例如,您能够用一个等边的 矩形(正方形)和两条直线组合成一个房子。可是这种作法仍然有局限性。

使用 path 元素 是更加灵活的第三者方式。这种元素容许开发者建立更加复杂的图形。它接受一组命令来指导浏览器绘制绘制图形。例如,要绘制一个 'L',您能够建立一个 path 元素,其中包含三个命令:

  1. M 20 20: M 是移动的意思,这个命令让浏览器的 画笔 移动到指定的 X 和 Y 坐标(即 20, 20);
  2. V 80: 这个命令让浏览器绘制一条从上一个点到 80 的平行于 y 轴的垂直线;
  3. 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 元素的指令告诉浏览器须要执行如下步骤:

  1. 先绘制一个坐标点 20, 20
  2. 第一个控制点的坐标是 20, 110
  3. 接着第二个控制点的坐标是 110, 110
  4. 结束曲线的终点坐标是 110 20

若是您仍然不知道三次贝塞尔曲线是如何工做的,也不用担忧。在本系列教程里,有将会有机会来练习它的。除此以外,您还能够在网上找到许多关于这个特性的教程并且您也能够经过相似 JSFiddleCodepen 这类工具来练习它。

建立 Canvas 组件

既然您的项目已经结构化,而且您已经了解了 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 点)。
  • widthheight:这些值定义了用户将在屏幕上看到多少个 X 和 Y 坐标。

除了定义 viewBox 特性,您也能够在新版本里定义 preserveAspectRatio 特性。您已经使用了 xMaxYMax none 来强制使画布和它的元素进行统一的缩放。

重构您的画布以后,您须要在 ./src/index.css 文件中添加以下规则:

/* ... body definition ... */

html, body {
  overflow: hidden;
  height: 100%;
}
复制代码

这将是 htmlbody 元素隐藏(禁用)滚动。它也将是这些元素占满这个屏幕。

若是您如今查看您的应用,您会看到您的圆正水平居中并位于屏幕底部附近。

建立 Sky 组件

在使画布占满整个屏幕并将原点轴从新定位到它的中心以后,是时候建立真正的游戏元素了。您能够先定义一个 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 组件,这样才能让网页浏览器知道必须在蓝色背景之上显示它。

建立 Ground 组件

建立完 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组件之间)。若是您对于最后的这些步骤有什么疑问,请在这里给我留言.

建立 Cannon 组件

如今您的游戏了已经有了 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 中提取(initialAxisinitialControlPointendingControlPointendingAxis)接着将它们传入到构建三次贝塞尔曲线的模板字符串中。

有了这个文件,您就能够构建您的炮台了。为了让事情更有条理,您须要把您的炮台分为两部分: CannonBaseCannonPipe

要定义 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 组件并用 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; 复制代码

检查并运行您的应用,您将看到以下矢量图所呈现的画面:

Drawing SVG elements with React and Redux

让 Cannon 可以瞄准

您的游戏愈来愈完善了。您已经给游戏添加了背景元素(SkyGround)和炮台。如今的问题是全部东西都是死的。因此,为了让事情变得更有趣,您要专一于完成炮台的瞄准功能。为此,您要给您的画布添加 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,若是 typeMOVE_OBJECTS, 它将调用 moveObjects 函数。须要注意的是,在定义该函数以前,您还须要在新版本里定义应用的初始化状态,它包含了值为 45angle 属性。这定义了您应用程序里炮台的初始瞄准角度。

如您所见,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 中获取 xy 属性,并把它们传给 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 映射到您 Appprops 里。您将重构 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: 如今您在这里定义了两个属性,anglemoveObjects。首先是 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:您会明确地为该组件定义它须要 angletrackMouse 属性。

就这样!您应该准备好来预览您炮台的瞄准功能。 切换到 terminal,并在项目的根目录运行 npm start (若是它尚未运行)。 而后,在浏览器里打开 http://localhost:3000/ 并移动鼠标。您的炮台将跟随鼠标旋转起来。

多有趣啊!?

“我用 React, Redux 和 SVG 建立了一个能够瞄准的炮台。这多有趣啊!?” 在这里 tweet 咱们

总结和下一步

在本系列的第一部分,您学习了一些重要的主题,它将帮助您建立一个完整游戏。您也使用了 create-react-app 来建立您的项目并建立了一些游戏元素,如炮台、天空和大地。最后,您给炮台添加了瞄准功能。有了这些元素,您就能其余的 React 组件并让他们动起来。

在本系列的下篇文章中,您将再创造一些组件,来让一些飞碟随机出如今预约的位置。以后,您将使您的炮台可以发射一些炮弹。这实在使人激动!

请保持关注!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索