React + SVGでのゲーム開発。 パート2

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つの違いがあります。









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



内に子オブジェクトが表示されます。









さらに、 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(), } } }
      
      





. . :







  1. (.. ! state.gameState.started



    ), .
  2. , createInterval



    maxFlyingObjects



    , , . createNewObject



    .
  3. createNewObject



    true



    , Math.floor



    0 3 ( Math.random() * maxFlyingObjects



    ), , .
  4. , newFlyingObject



    .
  5. ( 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



:









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



. !








All Articles