SVG + Reactでゲームを開発しています。 パート1

TL; DR:これらのエピソードでは、ReactとReduxでSVG要素を制御してゲームを作成する方法を学びます。 このシリーズで得られた知識により、ゲームだけでなくアニメーションを作成できます。 このパートで開発されたソースコードの最終バージョンはGitHubにあります







画像







ゲームの名前:「エイリアン、家に帰ろう!」







このシリーズで開発するゲームは、エイリアン、ゲットアウェイホーム! ゲームのアイデアはシンプルです。地球に侵入しようとしている「フライングディスク」を撃ち落とす銃があります。 これらのUFOを破壊するには、マウスにカーソルを合わせてクリックして大砲を発射する必要があります。







興味がある場合は、 ここでゲームの最終バージョンを見つけて実行できます( リンク切れ-翻訳者のコメント )。 しかし、ゲームに参加しないでください、あなたは仕事をしています!







前提条件



記事を読むには、Web開発(主にJavaScriptについて)とNode.jsおよびNPMがプリインストールされているコンピューターについてある程度の知識が必要です。 このチュートリアルシリーズを正常に完了するために、JavaScript、React、Redux、およびSVGの深い知識は必要ありません。 ただし、主題にいる場合は、一部の部分とそれらの相互の対応を理解しやすくなります。







それでも、このシリーズには、注目を集めるに値するトピックをよりよく説明するのに役立つ関連記事、投稿、およびドキュメントへのリンクが含まれています。







始める前に



前のセクションではGitについて言及していませんでしたが、これはいくつかの問題を解決するための優れたツールであることは注目に値します。 すべてのプロの開発者は、Git(またはMercurialやSVNなどの別のバージョン管理システム)を使用して、「ホーム」プロジェクト中でも活動を進めます。







バックアップなしでプロジェクトを作成する理由 あなたもそれを支払う必要はありません。 GitHub (最高!)やBitBucket (悪くない、正直言って)などのサービスを使用し 、信頼できるクラウドインフラストラクチャにコードを保存できます。







このようなツールを使用すると、コードの安全性に自信が持て、開発プロセスを直接促進できます。 たとえば、「バグ」を使用してアプリケーションの新しいバージョンを作成すると、Gitを使用して簡単な手順をいくつか実行するだけで、簡単に以前のバージョンのコードに戻ることができます。







もう1つの重要な利点は、このシリーズの各セクションを追跡し、段階的に開発されたコードをコミットできることです。 これにより、各セクションの後にコードの変更を簡単に確認できます。 このチュートリアルを読みながら、今すぐあなたの人生を楽にしてください。







一般的には、自分でGitをインストールしてください。 また、 GitHubにアカウント(まだ持っていない場合)とプロジェクトを保存するリポジトリを作成します。 次に、各セクションを完了した後、変更をリポジトリーにコミットします。 ああ、 あなたの変更プッシュすることを忘れないでください







Create-React-Appを使用したプロジェクトのクイックスタート



React、Redux、SVGを使用してゲームを作成する最初のステップは、 create-react-app



を使用create-react-app



てプロジェクトをすばやく開始することです。 おそらく既にご存知のとおり(そうでない場合は大したことはありません)、 create-react-appはFacebookがサポートするオープンソースツールであり、開発者がすぐにReactを使い始めるのに役立ちます Node.jsとNPMがインストールされている場合(後者のバージョンは5.2以降である必要があります)、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ツールは人気があり、十分に文書化されており、コミュニティのサポートが良好です。 詳細を調べることに興味がある場合は、githubのcreate-react-appリポジトリを確認し 、ユーザーガイドを読むことができます







現時点では、以下にリストされているファイルを取り除くことができます。これは、将来それらが役に立たなくなるためです。









これらのファイルを削除すると、プロジェクトを開始しようとするとエラーが発生する場合があります。 これは、。 ./src/App.js



ファイルから2つの「インポート」を削除することで簡単に修正できます。







 //     ./src/App.js import logo from './logo.svg'; import './App.css';
      
      





また、 render()



メソッドをリファクタリングすることにより:







 // ...        (  ) render() { return ( <div className="App"> <h1>We will create an awesome game with React, Redux, and SVG!</h1> </div> ); } // ... (    - , )
      
      





コミットすることを忘れないでください!


ReduxとPropTypesをインストールする



プロジェクトをデプロイし、プロジェクトから不要なファイルを削除した後、アプリケーションで唯一の真のデータソースとして Redux 構成する必要があります。 PropTypesもインストールする必要があります。 これにより、いくつかの一般的なエラーを回避できます。 1つのコマンドで両方のツールをインストールできます。







 npm i redux react-redux prop-types
      
      





ご覧のとおり、上記のコマンドには3番目のNPMパッケージであるreact-redux



ます。 ReduxをReactで直接使用することはお勧めしません。 react-reduxパッケージは 、パフォーマンスの最適化の面倒な手動処理​​を行います。







Reduxの構成とPropTypesの使用



説明したパッケージを使用して、Reduxを使用するようにアプリケーションを構成できます。 これは簡単です。 コンテナ (スマートコンポーネント)、 プレゼンテーションコンポーネント(愚かなコンポーネント)、およびレデューサーを作成するだけです。 スマートコンポーネントと愚かなコンポーネントの違いは、最初のコンポーネントが単純に愚かなコンポーネントをReduxに接続することです。 作成する3番目の要素であるレデューサーは、Reduxストアのメインコンポーネントです。 このコンポーネントは、アプリケーションのさまざまなイベントによって引き起こされる「アクション」(アクション)を実行し、これらのアクションに基づいて「ストア」(データソース)を変更する機能を適用します。







このすべてについてわからない場合は、コンポーネント(鈍い)およびコンテナー(スマート)コンポーネントについて詳しく説明しているこの記事を読むことができます。 また、 アクションゲームレデューサー 、およびストアに慣れるためにRedux実用ガイドを開きます 。 これらの概念を学ぶことを強くお勧めしますが、追加の読書に悩まされることなく勉強を続けることができます。

この要素は他の要素から独立しているため(実際には逆のことが当てはまる)、レデューサーを作成してプロセスを開始する方が便利です。 構造を維持するには、 reducers



という新しいディレクトリを作成し、その中にsrc



を配置し、 index.js



というファイルを追加します。 このファイルには、次のソースコードが含まれる場合があります。







 const initialState = { message: `React  Redux  ,   ?`, }; function reducer(state = initialState) { return state; } export default reducer;
      
      





したがって、Reducerは、ReactとReduxを簡単に統合できるメッセージでアプリケーションの状態を初期化するだけです。 このファイルでは、すぐにアクションの定義と処理を開始します。







その後、 App



コンポーネントをリファクタリングして、このメッセージをユーザーに表示できます。 PropTypes



を使用します。 これを行うには、。 ./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;
      
      





ご覧のとおり、 PropTypes



を使用すると、コンポーネントが期待する型を決定するのPropTypes



非常に簡単です。 App



コンポーネントのPropTypes



プロパティを必要なパラメーターで設定するだけです。 ネットワークには、基本的なPropTypes



定義と高度なPropTypes



定義を作成する方法を説明したチートシートがあります(たとえば、 これこれ、およびこれ ) 必要に応じてチェックしてください。







store



store



)の初期状態とApp



コンポーネントの表示内容を決定したら、これらの要素をリンクする必要があります。 これがコンテナの目的です。 構造内にコンテナを作成するは、 src



ディレクトリ内にontainers



という名前のディレクトリを作成する必要があります。 その後、新しいディレクトリで、 Game.js



ファイル内にGame



というコンポーネントを作成します。 このコンテナは、 react-redux



connect



機能を使用して、 state.message



App



コンポーネントのメッセージパラメーターに渡します。







 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 ストアを初期化し、 Game



コンテナーに転送します(メッセージを受信し、 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



を実行します。 したがって、アプリケーションを開発モード( dev-mode )で実行すると、デフォルトのブラウザーで開きます。







ReactでSVGコンポーネントを作成する



このシリーズでは、Reactを使用してSVGコンポーネントを簡単に作成できることに感謝します。 実際、ReactコンポーネントをHTMLとSVGのどちらで作成しても違いはほとんどありません。 主な違いは、新しい要素がSVGに導入され、これらの要素がSVGキャンバスに描画されることです。







SVGとReactを使用して独自のコンポーネントを作成する前に、SVGについて簡単に理解しておくと役立ちます。







SVGの概要



SVGは、最もクールで柔軟なWeb標準の1つです。 SVGは、Scalable Vector Graphicsの略で、開発者が2次元のベクターグラフィックスを記述することができるマークアップ言語です。 SVGはHTMLに非常に似ています。 どちらもXMLベースのマークアップ言語であり、CSSやDOMなどの他のWeb標準とうまく連携します。 CSSルールは、アニメーションを含むSVGとHTMLの両方に等しく適用されます。







このシリーズでは、Reactを使用して、12を超えるSVGコンポーネントを作成します。 ゲームオブジェクト(砲弾を発射する大砲)を作成するには、SVG要素を構成(グループ化)する必要さえあります。







SVGのより詳細な調査は、この記事のフレームワーク内では不可能であり、長すぎます。 SVGマークアップ言語について詳しく知りたい場合は、Mozillaが提供するチュートリアルと 、この記事で紹介するSVG座標系に関する資料をお読み ください







ただし、独自のコンポーネントの作成を開始する前に、SVGのいくつかの特性を学ぶことが重要です。 第1に、SVGをDOMと組み合わせて使用​​すると、開発者は「何かをする」ことができます。 ReactでSVGを使用するのは非常に簡単です。







第二に、SVG座標系はデカルト平面に似ており、逆さまになっています。 したがって、デフォルトでは、負の値はX軸の上に垂直に表示されます。この場合、水平値はデカルト平面と同じように配置されます。つまり、負の値はY軸の左側に配置されます。この動作は、SVGキャンバスに変換を適用することで簡単に変更できます 。 ただし、開発者間の混乱を避けるために、デフォルト設定を使用することをお勧めします。 すぐに慣れるでしょう。







最後に、SVGは多くの新しい要素( circle



rect



path



)を導入することに注意してください。 これらの要素を使用するには、HTML要素内で定義するだけでは不十分です。 最初に、すべてのSVGコンポーネントを描画するsvg



要素(キャンバス)を定義する必要があります。







SVG、パス要素、三次ベジェ曲線



SVG要素の描画は、3つの方法で実行できます。 まず、 rect



circle



line



などの基本的な要素を使用できます。 ただし、これらの要素は特に柔軟ではありません。 名前(長方形、円、線)に従って単純な図形を描画できます。







2番目の方法は、基本要素を組み合わせて、より複雑な形状を取得することです。 たとえば、家を描くために、同じ辺(正方形を取得)と2本の線で長方形( rect



)を使用できます。 ただし、このアプローチは依然として非常に厳しく制限されています。







3番目の最も柔軟な方法は、 パス要素を使用することです 。 このオプションにより、開発者はかなり複雑なフォームを作成できます。 図を描画するには、ブラウザに特定のコマンドを指定します。 たとえば、「L」を描画するには、3つのコマンドを含むpath



要素を作成できます。









 <svg> <path d="M 20 20 V 80 H 50" stroke="black" stroke-width="2" fill="transparent" /> </svg>
      
      





Path



要素は他の多くのコマンドを受け入れます。 最も重要なものの1つは、3次ベジェ曲線のチームです。 2つの基準点と2つの制御点を使用して、「滑らかな」曲線を追加できます。







「各ポイントの3次ベジェ曲線は2つの制御点を取ります。したがって、3次ベジェ曲線を作成するには、3つの座標セットを指定する必要があります。最後の座標セットは、曲線の終了点を示します。他の2つのセットは制御点です。基本的に、コントロールポイントは特定のポイントでのラインの勾配を表します。ベジェ関数は、ラインの先頭に設定した勾配から最後に設定した勾配までの滑らかな曲線を作成します。 -Mozilla Developer Network

たとえば、「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. 2番目の制御点の座標: 110, 110



    ;
  4. 曲線の終点の座標: 110 20



    ;


3次ベジエ曲線の動作原理をまだ理解していない場合は、絶望しないでください。 このシリーズで練習する機会があります。 さらに、インターネット上でこの機能に関する多くのガイドを見つけることができ、 JSFiddleCodepenなどのツールでいつでも練習できます。







Canvasコンポーネントの作成



(これは<canvas></canvas>



に関するものではなく、Canvas反応コンポーネント(ロシア語のキャンバス)-翻訳者のコメント)







プロジェクトの構造を作成し、SVGの基本を学習したら、ゲームの作成を開始できます。 作成する最初の要素は、ゲーム要素の描画に使用されるSVGキャンバスです。







このコンポーネントは、プレゼンテーション(愚かな)として特徴付けられます。 したがって、. ./src



ディレクトリ内に./src



ディレクトリを作成して、新しいコンポーネントとその「兄弟」( 隣接/子要素-翻訳者コメント )を保存できます。 これがあなたのキャンバスになるので、 Canvas



よりも自然な名前を思い付くのは難しいです。 ./src/components/



ディレクトリ内に./src/components/



という新しいファイルを作成し、次のコードを追加します。







 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;
      
      





Canvas



コンポーネントを使用するようにApp



コンポーネントを書き直します。







 import React, {Component} from 'react'; import Canvas from './components/Canvas'; class App extends Component { render() { return ( <Canvas /> ); } } export default App;
      
      





プロジェクトを実行し( npm start



)、アプリケーションをテストすると、ブラウザーはこの円の4分の1だけを描画することがわかります。 これは、デフォルトでは、原点が画面の左上隅にあるためです。 さらに、 svg



要素が画面全体を占有していないことがわかります。







より面白くて便利なコントロールを作成するには、キャンバス( <Canvas/>



)を全画面表示に適したものにします。 開始点をX軸の中心に移動して、下部に近づけることができます(後で銃をオリジナルに追加します)。 両方の条件を満たすには、2つのファイルを変更する必要があります: ./src/components/Canvas.jsx



/ ./src/index.css



/ ./src/components/Canvas.jsx



./src/index.css









Canvas



の内容を置き換えることから始めて、次のコードを適用します。







 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属性を設定します 。 この属性により、キャンバスとそのコンテンツが特定のコンテナ(この場合、ウィンドウ/ブラウザの内側の領域)に対応する必要があることを決定できます。 ご覧のとおり、viewBox属性は4つの数字で構成されています。









viewBox



属性の定義に加えて、新しいバージョンではpreserveAspectRatioという属性も設定します。 キャンバスとその要素の均一なスケーリングを強制するために、 xMaxYMax none



を使用しxMaxYMax none









preserveAspectRatio



インストールすると、reactから警告が発生しました-翻訳者のコメント







キャンバスをリファクタリングした後、次のルールを./src/index.css



ファイルに追加する必要があります。







 /* ... body definition ... */ html, body { overflow: hidden; height: 100%; }
      
      





これは、 html



およびbody



要素(タグ)がスクロールを非表示(および無効)にするために行われます。 さらに、アイテムは全画面で表示されます。







今すぐアプリケーションをチェックすると、円は画面の下部の中央の水平方向にあることがわかります。







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ポイントを設定するため、これによりゲームの一貫性が確保され、すべてのユーザーに同じ領域が表示されます。 したがって、フライングディスクが表示される場所と、指定されたポイントを通過する時間を決定できます。







キャンバスに空( Sky



コンポーネント)を表示するには、エディターでCanvas.jsx



ファイルを開き、 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



)、円はまだ下部の中央にあり、背景が青になっていることがわかります。







circle



要素の後にSky



要素を追加すると、 circle



は表示されなくなります。 これは、SVG z-index



サポートしていないためです。 SVGは、リストされている順序に従って、どの要素が「より高い」かを判別します。 つまり、 Sky



後にcircle



要素を記述して、Webブラウザーが青い背景の上に表示するようにする必要があります。







Groundコンポーネントの作成



Sky



コンポーネントを作成したら、 Ground



コンポーネントの作成に進みます。 これを行うには、。 Ground.jsx



ディレクトリに新しい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



定数も含まれています。 したがって、このようなグローバル定数を保存するファイルを作成するとよいでしょう。







この結論に達したので、。 ./src/



ディレクトリ内にutils



という新しいディレクトリを作成し、この新しいディレクトリ内にconstants.js



というファイルを作成します。 現時点では、このファイルに保存する定数は1つだけです。







 //  ,      export const skyAndGroundWidth = 5000;
      
      





Sky



Ground



, .







Ground



(, (.. Sky



circle



)). - , , .







Cannon ()



, . . , , . . , , .







, : , . , d



path



, : M 20 20 C 20 110, 110 110, 110 20



.







, formula.js



./src/utils/



, :







 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} `; };
      
      





, () ( initialAxis



, initialControlPoint



, endControlPoint



, endAxis



) cubicBezierCurve



, .







, . : CannonBase



() CannonPipe



().







CannonBase



CannonBase.jsx



./src/components



:







 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;
      
      





. - ( #75450e



) "" - ( #a16012



).







CannonPipe



CannonBase



. , pathFromBezierCurve



, . , transform



.







CannonPipe.jsx



./src/components/



:







 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;
      
      





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;
      
      





:







画像









! ( Sky



Ground



) ( CannonBase



+ CannonPipe



). . , - , . onmousemove



, .. , , .







, , CannonPipe



. onmousemove



, - () . ( ), ( Redux).







Redux action ( , ) ( — ). , Actions



./src/



. index.js



, :







 export const MOVE_OBJECTS = 'MOVE_OBJECTS'; export const moveObjects = mousePosition => ({ type: MOVE_OBJECTS, mousePosition, });
      
      





: MOVE_OBJECTS , . .







( index.js



./src/reducers/



):







 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



, moveObjects



. , , ( initial state ) , angle



45



. .







, moveObjects



. , , , . moveObjects.js



./src/reducers/



:







 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;
      
      





, x



y



mousePosition



calculateAngle



. , , ( ) .







, , calculateAngle



formula.js



, ? , , , , StackExchange , , . , formula.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; };
      
      





: atan



, Math



, . . radiansToDegrees



.







, , . Redux, action ( ) moveObjects



props ( ) App



. 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;
      
      





( mapStateToProps



mapDispatchToProps



) App



props



. 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;
      
      





, . :









App



formula.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;
      
      





:









! . npm start



( ). http://localhost:3000/ - . .







, ?!









, . create-react-app



, , . , . , .







, , . .







!







!







翻訳者から



, "" . どう思いますか?














All Articles