【翻譯】使用React、 Redux 和 SVG 開發遊戲(一)

MasterOfPuppets發表於2018-05-27

使用React、 Redux 和 SVG 開發遊戲

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

轉載英文原作請註明原作者與出處。轉載本譯本請註明譯者譯者部落格

這段太長別看:在這系列教程中,你將學會如何使用ReactRedux去控制一堆SVG元素來製作一個遊戲。這一個系列所帶給你的知識也可以讓你使用ReactRedux去製作其他的動畫和特效,並不僅限於遊戲。你可以在這裡找到第一部分的全部程式碼:Aliens Go Home - Part 1

React 遊戲 :外星人,滾回家!

在本系列開發的遊戲名為《外星人,滾回家!》(Aliens , Go Home !)。這個遊戲很簡單:你用一個加農炮,來消滅試圖入侵地球的飛碟。你必須通過準確點選SVG元素來發射加農炮。 如果你忍不住好奇心,可以先去看看可以試玩的最終版本(連結已經掛了,不知道作者什麼時候恢復,你可以clone第三部分的程式碼自己執行試玩)。但是別玩太久!你還有正事要做呢!


第三部分的最終程式碼


知識儲備

學習本系列之前,你需要一些知識儲備:

  • Web開發基本知識,主要是JavaScript。
  • 有node環境。
  • 會用Node包管理工具npm。
  • 你並不需要十分精通JavaScript、React和SVG。當然,如果你玩的很6,你學起來會很輕鬆,並且能很快抓住重點(譯者:建議還是先學點兒React和Redux吧,不然能做出來但是看不懂的)。

本系列還包含了一些值得關注的其他相關文章、帖子、文件的連結,對於一些話題這裡面可能有更好的解釋。

開始之前

譯者:下面這些都是在安利你使用GitGitHub。我覺得不用看了。因為我不覺得有 會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開發者瞬間生成一個專案的基礎目錄結構。安裝了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如果你看出來了歡迎指出。

相關文章