【翻譯】使用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若是你看出來了歡迎指出。

相關文章
相關標籤/搜索