[譯] 使用 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索