本文翻譯自:Developing Games with React, Redux, and SVG - Part 1css
轉載英文原做請註明原做者與出處。轉載本譯本請註明譯者與譯者博客html
這段太長別看:在這系列教程中,你將學會如何使用React
和Redux
去控制一堆SVG
元素來製做一個遊戲。這一個系列所帶給你的知識也可讓你使用React
和Redux
去製做其餘的動畫和特效,並不只限於遊戲。你能夠在這裏找到第一部分的所有代碼:Aliens Go Home - Part 1node
在本系列開發的遊戲名爲《外星人,滾回家!》(Aliens , Go Home !)。這個遊戲很簡單:你用一個加農炮,來消滅試圖入侵地球的飛碟。你必須經過準確點擊SVG
元素來發射加農炮。 若是你忍不住好奇心,能夠先去看看能夠試玩的最終版本(連接已經掛了,不知道做者何時恢復,你能夠clone第三部分的代碼本身運行試玩)。可是別玩過久!你還有正事要作呢!react
第三部分的最終代碼git
學習本系列以前,你須要一些知識儲備:github
本系列還包含了一些值得關注的其餘相關文章、帖子、文檔的連接,對於一些話題這裏面可能有更好的解釋。web
譯者:下面這些都是在安利你使用Git
和GitHub
。我以爲不用看了。由於我不以爲有 會React
殊不知道Git
也沒有GitHub
的這種工程師存在。不過負責一點,我仍是全翻譯了。npm
儘管前面的知識儲備章節沒有提到Git
,可是它真的是一個很好地工具。全部的職業開發者在開發項目時都會使用Git
或者其餘版本控制系統好比SVN
,哪怕是很小的玩具項目。json
你寫的項目老是要進行版本控制和代碼備份的,你不用爲此支付費用。你可使用GitHub
(最好的)這種平臺或者BitBucket
(說實話,也不錯)來作這件事。redux
除了能夠確保你的代碼安全地保留下來,上面這些工具還可讓你緊緊掌控住本身的開發進程。例如,若是你用了Git
,而後你寫了一個全是BUG
的版本,你能夠僅用幾條命令就回到上次一記錄的版本。
另外一個好處就是,在學習本系列教程的時候,你能夠每作完一個章節就commit
一次。這樣你能夠清除地知道每一個章節你都進行了哪些修改和新增,這讓你學習教程變得更加輕鬆。
因此,幫本身一個忙,裝個Git
吧。而後,在Github建立一個帳號並上傳你的代碼吧!每次作完一個章節,都commit
一下。哦對了,不要忘記push
。
最快速建立咱們的項目的方式,是使用create-react-app
。也許你已經知道(不知道也不要緊),create-react-app
是一個Facebook開發的腳手架,幫助React
開發者瞬間生成一個項目的基礎目錄結構。安裝了Node
和npm
以後,你能夠安裝並直接執行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.css
:App
組件很重要,可是樣式將會委託給其餘組件來定義。App.test.js
:測試相關的內容可能會在其餘文章處理,可是本次教程不涉及。logo.svg
:在這個遊戲裏你不須要React
的logo。移除文件後,啓動程序可能會拋出異常,由於咱們把LOGO和CSS刪了。只要把App.js
中LOGO和CSS的import
語句也刪掉就ok了。
咱們重構一下src/App.js
的render()
方法:
render() {
return (
<div className="App">
<h1>We will create an awesome game with React, Redux, and SVG!</h1>
</div>
);
}
複製代碼
以後npm start
運行你的項目。
別忘了每個章節都commit
一次代碼哦。
在建立了項目而且移除無用文件以後,你應該安裝而且配置Redux來統一管理應用的狀態樹。同時你也應該安裝PropTypes來幫助你避免數據類型引起的錯誤。安裝着兩個工具只須要一條命令就夠了:
npm i redux react-redux prop-types
複製代碼
像你看到的同樣,上面的命令行包含了一個第三方NPM
包react-redux
。儘管你能夠直接在React
上使用redux
而不是redux-react
,可是並不推薦這麼作。react-redux
對React
作了一些優化,若是咱們手動來作這些事的話就太麻煩了。
你能夠經過適當的配置這些包,來讓你的app使用redux
。過程很簡單,你須要建立一個container
組件,一個presentational
組件,和一個reducer
。container
組件和presentational
組件的區別在於,前者只是用來把presentational
組件connect
到Redux
的。你將建立的第三個組件是一個reducer
,是Redux store
的核心組件。這種組件負責處理頁面行爲觸發的事件,並調用相應的事件處理函數,並響應這些頁面行爲所做出的狀態樹的修改。
若是上面這些概念你都不熟悉,你能夠讀這篇文章來了解container
和presentational
組件的概念。你還能夠經過這篇文章Redux教程來了解Redux
中的action
、reducer
和store
。雖然很是建議學習這些文章,可是你也能夠先不學,先把本系列教程作完。
咱們最好從建立一個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.message
和App
組件的message props
,Game.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
組件很是簡單。事實上,建立HTML
組件和建立SVG
組件幾乎沒有什麼區別。惟一的區別是,svg
建立出的元素都是在畫在一個svg
畫布上的。
不過,在開始以前,先來一塊兒快速瞭解一下svg
相關知識仍是很重要的。
svg
是最酷、最靈活的web標準之一。svg
表明一種標記語言Scalable Vector Graphics
。他讓開發者有能力繪製2D的矢量圖形。svg
和HTML
很是類似。他們都是基於XML
的標記語言而且均可以跟其餘web標準很好地寫做共存好比css
和dom
。這意味着你能夠給svg
跟其餘普通元素同樣地賦予樣式,包括動畫效果。
在本系列教程中,你會用react
建立不止一打的svg
元素。你還會組裝svg
來造成你的遊戲元素,好比你的加農炮和炮彈!
關於svg
更加嚴禁周密的闡釋不在本系列教程範圍,並且會讓文章過於冗長。若是你期待學習更多svg
的知識,能夠看這兩篇文章:
然而,開始以前,一些基礎少許的svg
知識須要明白。
svg
和dom
的組合讓開發者能夠輕鬆地在react
中使用svg
。svg
座標系跟笛卡爾座標系很類似可是是反過來的。這意味着Y軸朝下是正。X軸不變。這種表現能夠經過調用transformation輕易地改掉。然而,爲了避免讓其餘開發者迷惑,咱們不會修改默認的座標體系。你很快會習慣的~svg
提供了更多的形狀標籤,好比rect
、circle
和path
。你能夠很是簡單的將他們包裹在HTML標籤裏。在畫svg
圖形或者建立react
中的svg
組件以前你必須先定義好svg
標籤。將圖形們包裹在<svg></svg>
中。有三種方式來完成svg
的繪製。第一種,你能夠直接使用rect
,circle
和line
來繪製基本形狀。他們可能不是很靈活,可是畫基本形狀很好用。他們的含義跟名字同樣,長方形,圈兒和線。
第二種方式是把基本圖形進行組合,生成複雜的圖形。好比,你能夠作一個寬高相等的長方形,你就獲得了一個正方形,而後用兩條line
來作個三角兩邊扣在正方形上面,最後,你就畫出了一個房子。然而這種方式的靈活性仍是有限制。
第三種方式就是使用path
標籤。這種方式讓開發者擁有繪製很是複雜的圖形的能力。它經過接受一組命令以指示瀏覽器如何繪製圖形來實現。好比你要畫一個大寫的L
,你能夠建立一個帶有三個命令的path
元素。
M 20 20
:這條命令指示瀏覽器拿起‘畫筆’前往(20,20)這個座標點。v 80
:這條命令指示瀏覽器畫一條線,從上條命令的點畫至Y軸80的位置。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>
複製代碼
命令的含義以下:
(20,20)
開始繪製;(20,110)
;(110,110,)
;(110,20)
處結束繪製;若是你不能確切地明白貝塞爾曲線的工做原理,不要擔憂。在本系列中你會有練習的機會的。除此以外,你能夠在網上找到不少教程,而且能夠常常在JSFiddle和Codepen上進行練習。
如今你已經有了一個結構化的項目,而且你已經知道了咱們須要用到的全部的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.jsx
和index.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減去屏幕高度的值賦予該屬性。width
和height
規定了可視區域的範圍有多大。除了設置viewBox
以外,你必須設置一個屬性叫作preserveAspectRatio
。而且賦值爲xMaxYMax none
來使svg
和它全部子元素的縮放都統一。
重構完Canvas.jsx
以後你須要編寫一下樣式src/index.css
html, body {
overflow: hidden;
height: 100%;
}
複製代碼
這會讓你的應用鋪滿整個屏幕。而且禁止滾動,溢出部分隱藏。這時你再次運行你的應用,會發現以前的左上角四分之一圓跑到底部中心而且變成整圓了。
完成了畫布鋪滿屏幕和原點重定位的工做以後,是時候開始製做真正的遊戲元素了。你能夠從遊戲的背景開始——天空組件。跟前面同樣,在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.js
和Ground.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}
`;
};
複製代碼
這個代碼很簡單,他只是根據傳入的四個參數來返回一個貝塞爾曲線路徑字符串。有了這個文件,你如今能夠開始建立你的加農炮了。爲了讓代碼更加結構化。你能夠把加農炮拆分爲兩部分:CannonBase
和CannonPipe
(炮主體和炮管)。
在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
標籤移除,把CannonBase
和CannonPipe
添加進去:
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;
複製代碼
運行你的程序,到目前爲止,你的應用應該長下面這個樣子了:
你的遊戲開發正在穩步進行。你已經建立了背景和你的加農炮。如今問題是全部東西都是毫無生機的。因此,咱們應該讓你的大炮進行瞄準,增長一點兒動態。你能夠添加mousemove
事件,來不斷從新渲染你的大炮以達到瞄準的效果。可是這樣會讓你的遊戲性能降低。
爲了克服這種情況,你應該設置一個統一的計時器,定時檢測鼠標的位置並更新你的CannonPipe
的角度。即便更換了戰略,你仍是要監聽mousemove
事件,不一樣的是,此次不會觸發重渲染
了。它只會更新你遊戲裏的屬性,而後計時器會使用這些屬性來更新redux
的store
而後觸發頁面更新。
這是第一次你須要使用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
中提取x
和y
座標,使用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 action
和reducer
以後,你要開始使用他們了。由於你的遊戲依賴於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
:你已經明確地定義了畫布組件須要angle
和trackMouse
屬性。有趣嗎?
在本教程第第一部分,你已經學會了一些能夠支撐你完成此次開發的重要的知識點。你已經會用create-react-app
建立項目了。你還會建立一些遊戲元素,好比天空、陸地和加農炮。最後你完成了加農炮的瞄準工做。有了這些,你已經準備好進行剩餘部分react
組件的開發工做,並讓他們動起來了。
在本教程的下一部分你將會建立這些組件,而後你將會作一些在預約位置範圍隨機出現的飛碟。固然你還會完成射擊工做,讓你的加農炮把它們打下來!Awesome!
敬請期待!
譯者:第二部分 大概下週末發佈 已發佈。上面的內容若有錯誤,歡迎指出。代碼錯誤您也可直接去做者原文評論。翻譯錯誤請直接指出。很是感謝!可能會有錯別字,我眼睛已經要看花了2333若是你看出來了歡迎指出。