TL; DR:これらのエピソードでは、ReactとReduxでSVG要素を制御してゲームを作成する方法を学びます。 このシリーズで得られた知識により、ゲームだけでなくアニメーションを作成できます。 このパートで開発されたソースコードの最終バージョンは、 GitHubにあります。
ゲームの名前:「エイリアン、家に帰ろう!」
このシリーズで開発するゲームは、エイリアン、ゲットアウェイホーム! ゲームのアイデアはシンプルです。地球に侵入しようとしている「フライングディスク」を撃ち落とす銃があります。 これらのUFOを破壊するには、マウスにカーソルを合わせてクリックして大砲を発射する必要があります。
興味がある場合は、 ここからゲームの最終バージョンを見つけて実行できます。 しかし、ゲームに参加しないでください、あなたは仕事をしています!
前半
最初のシリーズでは、 create-react-appを使用して 、Reactアプリケーションをすばやく起動しました。 Reduxをインストールして構成し、ゲームの状態を制御します。 次に、ReactコンポーネントでSVGの使用をマスターし、 Sky
、 Ground
、 CannonBase
、およびCannonPipe
ゲーム要素を作成しました。 最後に、イベントリスナーと間隔( setInterval
)を使用してReduxアクションをトリガーし、 CannonPipe
角度を変更して、 CannonPipe
。
これらの演習では、React、Redux、およびSVGを使用して、ゲームを作成するスキル(だけでなく)を「ポンプ」しました。
注:何らかの理由で前のセクションで記述したコードがない場合は、 GitHubからコピーしてください 。 コピー後、以下の手順に従ってください。
さらにコンポーネントを作成します。
次のセクションでは、ゲームの残りの要素の作成について説明します。 彼らの読書は長く見えるかもしれませんが、実際には彼らはシンプルで似ています。 指示を完了するには数分かかる場合があります。
このセクションを読んだ後、このシリーズで最も興味深いトピックが表示されます。 これらは、「ランダムな順序での飛行オブジェクトの作成」および「CSSアニメーションを使用した飛行オブジェクトの移動」と呼ばれます。
Cannonball
Reactコンポーネントの作成
次のステップは、 Cannonball
要素( cannonball )を作成することです。 この段階では、この要素を移動せずに残すことに注意してください。 しかし、心配しないでください! まもなく(残りの要素を作成した後)、大砲は複数の核を撃つことができ、いくつかのエイリアンを「揚げる」でしょう。
コンポーネントを作成するには、次のコードで新しいCannonBall.jsx
ファイルを追加します。
import React from 'react'; import PropTypes from 'prop-types'; const CannonBall = (props) => { const ballStyle = { fill: '#777', stroke: '#444', strokeWidth: '2px', }; return ( <ellipse style={ballStyle} cx={props.position.x} cy={props.position.y} rx="16" ry="16" /> ); }; CannonBall.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default CannonBall;
ご覧のとおり、 Cannonball
コンポーネントをキャンバスに表示するには、xおよびy座標を設定する必要があります( x
およびy
プロパティを含むオブジェクトを転送することにより)。 Prop-types
経験があまりない場合、おそらくPropTypes.shape
出会ったのはこれが初めてでしょう。 幸いなことに、この機能には説明は不要です。
コンポーネントを作成したら、それを見ることができます。 これを行うには、 Canvas
コンポーネントのSVG
要素に次のタグを追加するだけです( import CannonBall from './CannonBall';
を追加する必要もありimport CannonBall from './CannonBall';
)。
<CannonBall position={{x: 0, y: -100}}/>
同じ位置を保持している要素の前に追加すると、表示されないことに注意してください。 これを回避するには、最後に配置します( <CannonBase />
直後)。 その後、ブラウザでゲームを開いて新しいコンポーネントを表示できます。
これを行う方法を忘れた場合は、プロジェクトのルートでnpm start
を実行してから開きます http:// localhost:ブラウザで3000 。 先に進む前に、リポジトリにコードをコミットすることも忘れないでください。
現在のスコアコンポーネントを作成する
次のステップは、 CurrentScore
コンポーネント( 現在のスコア )を作成することです。 名前が示すように、このコンポーネントはユーザーが現在獲得しているポイントを示します。 つまり、フライングディスクが破壊されるたびに、このコンポーネントの値は1ずつ増加します。
このコンポーネントを作成する前に、適切なフォントを開発することをお勧めします。 実際、単調に見えないようにゲーム全体のフォントを調整する価値があります。 好きな場所でフォントを見つけて選択できますが、時間をかけたくない場合は、。 ./src/index.css
ファイルの先頭に次の行を追加して./src/index.css
。
@import url('https://fonts.googleapis.com/css?family=Joti+One'); /* ... */
したがって、ゲームフォントJoti OneフォントをGoogleからダウンロードします 。
その後、。 ./src/components
ディレクトリ内に次のコードを使用してCurrentScore.jsx
ファイルを作成します。
import React from 'react'; import PropTypes from 'prop-types'; const CurrentScore = (props) => { const scoreStyle = { fontFamily: '"Joti One", cursive', fontSize: 80, fill: '#d6d33e', }; return ( <g filter="url(#shadow)"> <text style={scoreStyle} x="300" y="80"> {props.score} </text> </g> ); }; CurrentScore.propTypes = { score: PropTypes.number.isRequired, }; export default CurrentScore;
注: Joti Oneフォントを構成しなかった場合(または他のフォントを好む場合)、それに応じてこのコードを変更する必要があります。 さらに、このフォントは作成する他のコンポーネントで使用されるため、それらも更新する必要があります。
ご覧のとおり、 CurrentScore
コンポーネントに必要なプロパティは1つ( props )だけです。 ゲームは開発のこの段階ではポイントをカウントしないため、コンポーネントに表示される固定値を設定します。 これを行うには、 svg
要素内の最後の要素としてCanvas
コンポーネント内に<CurrentScore score={15} />
追加します。 また、 import
を追加して、指定されたコンポーネントを抽出します( import CurrentScore from './CurrentScore';
)。
現在、新しいコンポーネントを評価できない場合があります。 これは、コンポーネントがshadow
フィルターを使用するためです。 このようなフィルターは必須ではありませんが、ゲーム内の画像をより魅力的にします。 さらに、 SVG要素に影を追加するのは非常に簡単です。 これを行うには、 svg
の先頭に次を追加します:
<defs> <filter id="shadow"> <feDropShadow dx="1" dy="1" stdDeviation="2" /> </filter> </defs>
その結果、 Canvas
コンポーネントは次の形式を取ります。
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'; import CannonBall from './CannonBall'; import CurrentScore from './CurrentScore'; 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} > <defs> <filter id="shadow"> <feDropShadow dx="1" dy="1" stdDeviation="2" /> </filter> </defs> <Sky /> <Ground /> <CannonPipe rotation={props.angle} /> <CannonBase /> <CannonBall position={{x: 0, y: -100}}/> <CurrentScore score={15} /> </svg> ); }; Canvas.propTypes = { angle: PropTypes.number.isRequired, trackMouse: PropTypes.func.isRequired, }; export default Canvas;
そして、あなたはそのような写真を取得します:
悪くないでしょ?!
Flying Object Reactコンポーネントの作成
Reactコンポーネントを使用してオブジェクトの飛行を始めてみませんか? これらのオブジェクトは、円や長方形では表されません。 通常、それらは2つの丸い部分(上部と下部)で構成されます。 したがって、それらを作成するには、 FlyingObjectBase
(ベース)とFlyingObjectTop
(頂点)の2つのコンポーネントを使用します。
これらのコンポーネントの1つの形状を決定するために、3次ベジェ曲線が使用されます。 2番目は楕円で記述されます。
./src/components
ディレクトリ内の新しいFlyingObjectBase.jsx
ファイルに最初のコンポーネントFlyingObjectBase
作成することから開始できます。 コンポーネントを決定するためのコードは次のとおりです。
import React from 'react'; import PropTypes from 'prop-types'; const FlyingObjectBase = (props) => { const style = { fill: '#979797', stroke: '#5c5c5c', }; return ( <ellipse cx={props.position.x} cy={props.position.y} rx="40" ry="10" style={style} /> ); }; FlyingObjectBase.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default FlyingObjectBase;
次に、オブジェクトの上部を描画します。 これを行うには、。 ./src/components
ディレクトリ内にFlyingObjectTop.jsx
ファイルを作成し、次のコードを追加します。
import React from 'react'; import PropTypes from 'prop-types'; import { pathFromBezierCurve } from '../utils/formulas'; const FlyingObjectTop = (props) => { const style = { fill: '#b6b6b6', stroke: '#7d7d7d', }; const baseWith = 40; const halfBase = 20; const height = 25; const cubicBezierCurve = { initialAxis: { x: props.position.x - halfBase, y: props.position.y, }, initialControlPoint: { x: 10, y: -height, }, endingControlPoint: { x: 30, y: -height, }, endingAxis: { x: baseWith, y: 0, }, }; return ( <path style={style} d={pathFromBezierCurve(cubicBezierCurve)} /> ); }; FlyingObjectTop.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default FlyingObjectTop;
3次ベジェ曲線の操作の原理がわからない場合は、前の記事を開きます
説明したアクションは、いくつかの飛行オブジェクトの画像には十分ですが、ゲーム内でランダムに表示する必要があり、それらを1つの要素として処理する方が便利です。 これを行うには、別のFlyingObject.jsx
を2つの既存のファイルに追加します。
import React from 'react'; import PropTypes from 'prop-types'; import FlyingObjectBase from './FlyingObjectBase'; import FlyingObjectTop from './FlyingObjectTop'; const FlyingObject = props => ( <g> <FlyingObjectBase position={props.position} /> <FlyingObjectTop position={props.position} /> </g> ); FlyingObject.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default FlyingObject;
1つのコンポーネントのみを使用して、飛行オブジェクトをゲームに追加できるようになりました。 Canvas
を次のように更新して、その動作を確認します。
// ... import FlyingObject from './FlyingObject'; const Canvas = (props) => { // ... return ( <svg ...> // ... <FlyingObject position={{x: -150, y: -300}}/> <FlyingObject position={{x: 150, y: -300}}/> </svg> ); }; // ... propTypes
ハートコンポーネントを作成する
次のコンポーネントは、プレイヤーの残りの「ライブ」を画面に表示する必要があります。 ハート- Heart
よりも良いアイコンを思い付かないでください。 そのため、 Heart.jsx
というファイルを作成します。
import React from 'react'; import PropTypes from 'prop-types'; import { pathFromBezierCurve } from '../utils/formulas'; const Heart = (props) => { const heartStyle = { fill: '#da0d15', stroke: '#a51708', strokeWidth: '2px', }; const leftSide = { initialAxis: { x: props.position.x, y: props.position.y, }, initialControlPoint: { x: -20, y: -20, }, endingControlPoint: { x: -40, y: 10, }, endingAxis: { x: 0, y: 40, }, }; const rightSide = { initialAxis: { x: props.position.x, y: props.position.y, }, initialControlPoint: { x: 20, y: -20, }, endingControlPoint: { x: 40, y: 10, }, endingAxis: { x: 0, y: 40, }, }; return ( <g filter="url(#shadow)"> <path style={heartStyle} d={pathFromBezierCurve(leftSide)} /> <path style={heartStyle} d={pathFromBezierCurve(rightSide)} /> </g> ); }; Heart.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default Heart;
ご覧のとおり、SVGを使用して心臓の形状を記述するには、心臓の各半分に1つずつ、2つの3次ベジェ曲線を使用する必要があります。 また、コンポーネントにposition
プロパティを追加する必要がありました。 ゲームでは複数の「ライフ」があるので、それぞれのハートを別々の位置に描くためにこれが必要です。
それまでの間、キャンバスにハートを1つ追加するだけで、通常どおりに動作するようになります。 Canvas
コンポーネントを開き、以下を追加します。
<Heart position={{x: -300, y: 35}} />
これで、 svg
内の要素の開発は終了するはずです。 imoprt
を追加することも忘れないでimoprt
( import Heart from './Heart';
)。
ゲーム開始ボタンを作成する
各ゲームには開始ボタンが必要です。 ゲームでこれを使用するには、 StartGame.jsx
ファイルと次のコードを追加します。
import React from 'react'; import PropTypes from 'prop-types'; import { gameWidth } from '../utils/constants'; const StartGame = (props) => { const button = { x: gameWidth / -2, // y: -280, // "" ( ) width: gameWidth, height: 200, rx: 10, // border ry: 10, // border style: { fill: 'transparent', cursor: 'pointer', }, onClick: props.onClick, }; const text = { textAnchor: 'middle', // x: 0, // X y: -150, // 150 ( Y) style: { fontFamily: '"Joti One", cursive', fontSize: 60, fill: '#e3e3e3', cursor: 'pointer', }, onClick: props.onClick, }; return ( <g filter="url(#shadow)"> <rect {...button} /> <text {...text}> Tap To Start! </text> </g> ); }; StartGame.propTypes = { onClick: PropTypes.func.isRequired, }; export default StartGame;
画面上に同時に複数のボタンは必要ないため、その位置を静的に(座標x: 0
およびy: -150
)記述しました。 これに加えて、このコンポーネントと前に説明したコンポーネントとの間には2つの違いがあります。
- 最初に、コンポーネントは
onClick
関数を期待します。 ボタンに触れると、この関数はReduxアクションを呼び出し、アプリケーションに新しいゲームを開始するよう指示します。 - 次に、コンポーネントはまだ定義されていない定数
gameWidth
ます。 この定数は、使用可能な領域を記述します。 他の領域は、アプリケーションを全画面に拡大するためにのみ必要です。
gameWidth
定数を定義するには、。 gameWidth
constants.jsファイルを開いて./src/utils/constants.js
ように記述します。
export const gameWidth = 800;
その後、 <StartGame onClick={() => console.log('Aliens, Go Home!')} />
をsvg
最後の要素として追加することにより、 StartGame
コンポーネントをCanvas
追加できます。 そして、いつものように、 import
を追加することを忘れないでください( import StartGame from './StartGame';
):
タイトルを作成する
このシリーズの最終的な開発コンポーネントはTitle
です。 あなたのゲームにはすでに名前があります:「エイリアン、帰りなさい!」 ( エイリアン、家を降りる )。 コードを使用してファイル( ./src/components
ディレクトリ内)を作成することにより、ヘッダーにするのは非常に簡単です。
import React from 'react'; import { pathFromBezierCurve } from '../utils/formulas'; const Title = () => { const textStyle = { fontFamily: '"Joti One", cursive', fontSize: 120, fill: '#cbca62', }; const aliensLineCurve = { initialAxis: { x: -190, y: -950, }, initialControlPoint: { x: 95, y: -50, }, endingControlPoint: { x: 285, y: -50, }, endingAxis: { x: 380, y: 0, }, }; const goHomeLineCurve = { ...aliensLineCurve, initialAxis: { x: -250, y: -780, }, initialControlPoint: { x: 125, y: -90, }, endingControlPoint: { x: 375, y: -90, }, endingAxis: { x: 500, y: 0, }, }; return ( <g filter="url(#shadow)"> <defs> <path id="AliensPath" d={pathFromBezierCurve(aliensLineCurve)} /> <path id="GoHomePath" d={pathFromBezierCurve(goHomeLineCurve)} /> </defs> <text {...textStyle}> <textPath xlinkHref="#AliensPath"> Aliens, </textPath> </text> <text {...textStyle}> <textPath xlinkHref="#GoHomePath"> Go Home! </textPath> </text> </g> ); }; export default Title;
タイトルを湾曲させるには、 textPath
とtextPath
組み合わせを3次ベジェ曲線で使用します。 また、 StartGame
start StartGame
ように、タイトルを静的な位置に設定しStartGame
。
タイトルをキャンバスに表示するには<Title/>
svg
<Title/>
追加し)
Canvas.jsx` )
インポート(
「./Title」からタイトルをインポート; )
。 ただし、ここでアプリケーションを実行すると、新しいアイテムが画面に表示されないことがわかります。 これは、アプリケーションにまだ十分な垂直スペースがないためです。
ゲームをレスポンシブにする
ゲームのサイズを変更してレスポンシブにする( アダプティブ、つまり、ブラウザーウィンドウが変更されると、ゲームの要素のサイズが変わる-トランスレーターコメント )には、2つのことを行う必要があります。 まず、 onresize
イベントonresize
をグローバルwindow
オブジェクトにアタッチします。 これは簡単です./src/App.js
ファイルを開き、次のコードをcomponentDidMount()
メソッドに追加します。
window.onresize = () => { const cnv = document.getElementById('aliens-go-home-canvas'); cnv.style.width = `${window.innerWidth}px`; cnv.style.height = `${window.innerHeight}px`; }; window.onresize();
その後、ブラウザウィンドウのサイズを変更しても、キャンバスのサイズはユーザーウィンドウのサイズと等しくなります。 また、アプリケーションの最初の再生中に、 window.onresize
関数が実行されます。
2番目のポイント:キャンバスのviewBox
プロパティを変更する必要があります。 ここで、Y軸のトップポイントの値を100 - window.innerHeight
として計算する代わりに(この式の100 - window.innerHeight
を忘れた場合、最初の部分を確認し 、 viewBox
の高さがwindow window
innerHeight
高さと等しいことを確認するには、次を使用します:
const gameHeight = 1200; const viewBox = [window.innerWidth / -2, 100 - gameHeight, window.innerWidth, gameHeight];
この場合、高さの値を1200に設定すると、新しいタイトルを正しく表示できます。 さらに、垂直方向のスペースを増やすと、ゲーマーはエイリアンを破壊する時間を増やすことができます。狙いを定めて射撃する方が便利です。
ユーザーにゲームを開始させる
新しいコンポーネントと新しいサイズを考えると、ユーザーにゲームをプレイする機会をどのように与えるかを考える時が来ました。 これを行うには、 Start Game
のStart Game
]ボタンを押してStart Game
するようにゲームを再編成します。 クリックすると、ゲームの状態が大幅に変化するはずです。 ただし、タスクを簡素化するために、ユーザーがボタンをクリックした後、画面からTitle
およびStartGame
コンポーネントを削除することから開始できます。
これを行うには、リデューサーによって処理されてフラグを変更する新しいアクションを作成します( フラグは、値が通常true/false
ある特定の変数です-トランスレーターコメント )。 このようなアクションを作成するには、。 ./src/actions/index.js
ファイルを開き、そこに次のコードを追加します(前のコードには触れないでください!)。
// ... MOVE_OBJECTS ( MOVE_OBJECTCS) export const START_GAME = 'START_GAME'; // ... moveObjects (, moveObjects) export const startGame = () => ({ type: START_GAME, });
その後、。 ./src/reducers/index.js
ファイルをリファクタリングして、新しいアクションを処理できます。 新しいバージョンは次のとおりです。
import { MOVE_OBJECTS, START_GAME } from '../actions'; import moveObjects from './moveObjects'; import startGame from './startGame'; const initialGameState = { started: false, kills: 0, lives: 3, }; const initialState = { angle: 45, gameState: initialGameState, }; function reducer(state = initialState, action) { switch (action.type) { case MOVE_OBJECTS: return moveObjects(state, action); case START_GAME: return startGame(state, initialGameState); default: return state; } } export default reducer;
ご覧のとおり、ゲームの3つのプロパティが含まれるinitialState
内に子オブジェクトが表示されます。
-
started
:ゲームが実行されているかどうかを示すフラグ。 -
kills
数:ダウンした飛行物体の数。 -
lives
:残りの「ライブ」の数。
さらに、 switch
case
新しいcase
を追加しました。 このcase
( START_GAME
などのアクションがSTART_GAME
到着したときにSTART_GAME
する)は、 startGame
関数を呼び出します。 この関数は、 gameStart
プロパティ内のstarted
フラグをgameStart
ます。 さらに、ユーザーがゲームを再び開始するたびに、この機能はkills
数をリセットし、再び3つのlives
を与えlives
。
startGame
関数を実装するには、コードで./src/reducers
ディレクトリ内に./src/reducers
という新しいファイルを作成します。
export default (state, initialGameState) => { return { ...state, gameState: { ...initialGameState, started: true, } } };
ご覧のとおり、新しいファイルのコードは非常に単純です。 Reduxストアに新しい状態オブジェクトのみを返しますgameState
ストアでは、開始済みフラグがtrue
設定され、 gameState
プロパティ内の他のすべてが破棄されます。 そのため、ユーザーは再び3つのライフを取得し、 kills
はリセットされます。
この関数を実装したら、ゲームに転送する必要があります。 新しいgameState
プロパティも渡す必要があります。 これを行うには、。 ./src/containers/Game.js
ファイルを次のように変更します。
import { connect } from 'react-redux'; import App from '../App'; import { moveObjects, startGame } from '../actions/index'; const mapStateToProps = state => ({ angle: state.angle, gameState: state.gameState, }); const mapDispatchToProps = dispatch => ({ moveObjects: (mousePosition) => { dispatch(moveObjects(mousePosition)); }, startGame: () => { dispatch(startGame()); }, }); const Game = connect( mapStateToProps, mapDispatchToProps, )(App); export default Game;
要約すると、ファイルの主な変更点に注意してください。
mapStateToProps
:そのため、 App
コンポーネントはgameState
のプロパティを「気にする」とReduxに伝えgameState
。
mapDispatchToProps
:ReduxはstartGame
関数をstartGame
コンポーネントに渡し、新しいアクションを初期化します。
両方の新しいコンポーネント( gameState
とstartGame
)は、 App
コンポーネントによって直接使用されません。 実際、 Canvas
コンポーネントはこれらを使用するため、それらを渡す必要があります。 これを行うには、。 ./src/App.js
ファイルを開き、 ./src/App.js
ように変換します。
// ... ... class App extends Component { // ... constructor(props) ... // ... componentDidMount() ... // ... trackMouse(event) ... render() { return ( <Canvas angle={this.props.angle} gameState={this.props.gameState} startGame={this.props.startGame} trackMouse={event => (this.trackMouse(event))} /> ); } } App.propTypes = { angle: PropTypes.number.isRequired, gameState: PropTypes.shape({ started: PropTypes.bool.isRequired, kills: PropTypes.number.isRequired, lives: PropTypes.number.isRequired, }).isRequired, moveObjects: PropTypes.func.isRequired, startGame: PropTypes.func.isRequired, }; export default App;
次に、。 ./src/components/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'; import CurrentScore from './CurrentScore' import FlyingObject from './FlyingObject'; import StartGame from './StartGame'; import Title from './Title'; const Canvas = (props) => { const gameHeight = 1200; const viewBox = [window.innerWidth / -2, 100 - gameHeight, window.innerWidth, gameHeight]; return ( <svg id="aliens-go-home-canvas" preserveAspectRatio="xMaxYMax none" onMouseMove={props.trackMouse} viewBox={viewBox} > <defs> <filter id="shadow"> <feDropShadow dx="1" dy="1" stdDeviation="2" /> </filter> </defs> <Sky /> <Ground /> <CannonPipe rotation={props.angle} /> <CannonBase /> <CurrentScore score={15} /> { ! props.gameState.started && <g> <StartGame onClick={() => props.startGame()} /> <Title /> </g> } { props.gameState.started && <g> <FlyingObject position={{x: -150, y: -300}}/> <FlyingObject position={{x: 150, y: -300}}/> </g> } </svg> ); }; Canvas.propTypes = { angle: PropTypes.number.isRequired, gameState: PropTypes.shape({ started: PropTypes.bool.isRequired, kills: PropTypes.number.isRequired, lives: PropTypes.number.isRequired, }).isRequired, trackMouse: PropTypes.func.isRequired, startGame: PropTypes.func.isRequired, }; export default Canvas;
ご覧のとおり、新しいバージョンは、 gameState.started
プロパティがfalse
場合にのみStartGame
およびTitle
コンポーネントが表示されるように編成されていfalse
。 また、ユーザーがStart Game
のStart Game
]ボタンをクリックするまで、飛行オブジェクト( FlyingObject
)を非表示にしました。
ここでアプリケーションを実行すると(ターミナルで[アプリケーション]がまだ起動していない場合はnpm start
を実行します)、変更が行われたことがわかります。 これはゲームを完全にプレイするには不十分ですが、すでにこの段階に近づいています。
空飛ぶ円盤を任意に起動する
Start gameの可能性(機能)を認識した後、 ゲームは、飛行物体が画面上の異なる位置に任意に表示されるように変換する必要があります。 私たちは彼らを飛行と呼んでおり、破壊しようとしているので、あなたはそれらを飛行させる必要があります(画面の下)。 , - .
, . , . , . ./src/utils/constants.js
:
// ... skyAndGroundWidth gameWidth export const createInterval = 1000; export const maxFlyingObjects = 4; export const flyingObjectsStarterYAxis = -1000; export const flyingObjectsStarterPositions = [ -300, -150, 150, 300, ];
, (1000 ) . , -1000
Y ( flyingObjectsStarterYAxis
). , ( flyingObjectsStarterPositions
) X, . .
, , createFlyingObjects.js
./src/reducers
:
import { createInterval, flyingObjectsStarterYAxis, maxFlyingObjects, flyingObjectsStarterPositions } from '../utils/constants'; export default (state) => { if ( ! state.gameState.started) return state; // const now = (new Date()).getTime(); const { lastObjectCreatedAt, flyingObjects } = state.gameState; const createNewObject = ( now - (lastObjectCreatedAt).getTime() > createInterval && flyingObjects.length < maxFlyingObjects ); if ( ! createNewObject) return state; // const id = (new Date()).getTime(); const predefinedPosition = Math.floor(Math.random() * maxFlyingObjects); const flyingObjectPosition = flyingObjectsStarterPositions[predefinedPosition]; const newFlyingObject = { position: { x: flyingObjectPosition, y: flyingObjectsStarterYAxis, }, createdAt: (new Date()).getTime(), id, }; return { ...state, gameState: { ...state.gameState, flyingObjects: [ ...state.gameState.flyingObjects, newFlyingObject ], lastObjectCreatedAt: new Date(), } } }
. . :
- (..
! state.gameState.started
), . - ,
createInterval
maxFlyingObjects
, , .createNewObject
. -
createNewObject
true
,Math.floor
0 3 (Math.random() * maxFlyingObjects
), , . - ,
newFlyingObject
. - ( state )
lastObjectCreatedAt
.
, , , . , ( action
), . MOVE_OBJECTS
10 , . moveObjects
( ./src/reducers/moveObjects.js
) :
import { calculateAngle } from '../utils/formulas'; import createFlyingObjects from './createFlyingObjects'; function moveObjects(state, action) { const mousePosition = action.mousePosition || { x: 0, y: 0, }; const newState = createFlyingObjects(state); const { x, y } = mousePosition; const angle = calculateAngle(0, 0, x, y); return { ...newState, angle, }; } export default moveObjects;
moveObjects
:
- -,
mousePosition
,action
. , ,mousePosition
. - -,
newState
createFlyingObjects
; . - ,
newState
, .
App
Canvas
, , ./src/reducers/index.js
initialState
:
// ... ... const initialGameState = { // ... ... flyingObjects: [], lastObjectCreatedAt: new Date(), }; // ... ...
, , — flyingObjects
PropTypes
App
:
// ... ... // ... App ... App.propTypes = { // ... other propTypes definitions ... gameState: PropTypes.shape({ // ... other propTypes definitions ... flyingObjects: PropTypes.arrayOf(PropTypes.shape({ position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, id: PropTypes.number.isRequired, })).isRequired, // ... other propTypes definitions ... }).isRequired, // ... other propTypes definitions ... }; export default App;
Canvas
, . FlyingObject
:
// ... ... const Canvas = (props) => { // ... ... return ( <svg ... > // ... svg react ... {props.gameState.flyingObjects.map(flyingObject => ( <FlyingObject key={flyingObject.id} position={flyingObject.position} /> ))} </svg> ); }; Canvas.propTypes = { // ... PropTypes ... gameState: PropTypes.shape({ // ... PropTypes ... flyingObjects: PropTypes.arrayOf(PropTypes.shape({ position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, id: PropTypes.number.isRequired, })).isRequired, }).isRequired, // ... PropTypes ... }; export default Canvas;
! , .
: Start Game
, . - - , . X. .
CSS .
. JavaScript . , . — CSS. , , .
, , , . , NPM- CSS React. styled-components
.
( "" — . ) CSS,styled-components
CSS- . — ! —styled-components
.
, ( ) :
npm i styled-components
FlyingObject
( ./src/components/FlyingObject.jsx
) :
import React from 'react'; import PropTypes from 'prop-types'; import styled, { keyframes } from 'styled-components'; import FlyingObjectBase from './FlyingObjectBase'; import FlyingObjectTop from './FlyingObjectTop'; import { gameHeight } from '../utils/constants'; const moveVertically = keyframes` 0% { transform: translateY(0); } 100% { transform: translateY(${gameHeight}px); } `; const Move = styled.g` animation: ${moveVertically} 4s linear; `; const FlyingObject = props => ( <Move> <FlyingObjectBase position={props.position} /> <FlyingObjectTop position={props.position} /> </Move> ); FlyingObject.propTypes = { position: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired }).isRequired, }; export default FlyingObject;
FlyingObjectBase
FlyingObjectTop
Move
. - g
SVG,
css , moveVertically
. , , styled-components
, " CSS " MDN .
, / , ( CSS) , ( transform: translateY(0);
) ( transform: translateY(${gameHeight}px);
).
, gameHeight
./src/utils/constants.js
. , , flyingObjectsStarterYAxis
, , . , .
, constants.js
:
// ... export const flyingObjectsStarterYAxis = -1100; // flyingObjectsStarterPositions ... export const gameHeight = 1200;
, 4 , . , ./src/reducers/moveObjects.js
:
import { calculateAngle } from '../utils/formulas'; import createFlyingObjects from './createFlyingObjects'; function moveObjects(state, action) { const mousePosition = action.mousePosition || { x: 0, y: 0, }; const newState = createFlyingObjects(state); const now = (new Date()).getTime(); const flyingObjects = newState.gameState.flyingObjects.filter(object => ( (now - object.createdAt) < 4000 )); const { x, y } = mousePosition; const angle = calculateAngle(0, 0, x, y); return { ...newState, gameState: { ...newState.gameState, flyingObjects, }, angle, }; } export default moveObjects;
, flyingObjects
( gameState
) , , 4000 (4 ).
Start Game
, , SVG . , , , .
, . CSS .
. : , "" "" (kills). auth0
Socket.IO
. !