はじめに
以前、Angular5で作成したテトリスをAngular + Reduxで作り直してみました。
https://l08084.github.io/ng-redux-tetris/
実際に👆のURLから遊ぶことができて(PCのChromeでのみ動作確認済み)、
GitHubにソースコードも置いています。
関連記事
Angular + Reduxの環境構築手順については👆の記事を参照してください。
Angular5でテトリスを作った時の記事は👆
遊び方
SPACEキー
でゲームが開始します。
→
: ブロックを右へ移動←
: ブロックを左へ移動↓
: ブロックを下へ移動↑
: ブロックを回転
ブロックの左右移動・回転については、画面に配置してあるボタンでも操作可能です。
実装
開発環境
Angular: 5.2.3
angular-redux/store: 7.1.0
rxjs: 5.5.6
TypeScript: 2.5.3
angular/cli: 1.6.5
OS: darwin x64
Node: 8.1.4
構成
AppComponentがユーザーによるキー入力イベント・タイマーからの呼び出しに応じて以下3つのサービスクラスを呼び出します。
TetrisService: ブロックの座標変更・揃った時のブロック消去・ゲームオーバー判定
ControllerService: キー入力によるブロック移動
RenderService: HTML5のcanvas要素におけるブロックのレンダリング処理
各種サービスでState(状態)の変更が必要になった時はActionクラスが呼び出されます。ActionクラスはActionをディスパッチしてReducerクラスに引き渡し、Stateの変更を行います。Stateの変更後の値については、@select
デコレーターによってAppComponentと各種サービスで取得することができます。
AppComponent・各種サービスクラス
src/app/app.component.html
の一部抜粋:
<div class="title"> <h1 class="big">Angular Redux Tetris</h1> <h2 *ngIf="!(isGameStart$ | async); else tetrisBlock" class="flash">Press spacebar to start the game</h2> </div> <ng-template #tetrisBlock> <canvas #campas width='300' height='600'></canvas> <div class="info"> <div class="info-child">Score: {{ score$ | async }}</div> <div class="info-child">Time:</div> <div class="info-child">{{ digitalTimer$ | async }}</div> </div> </ng-template>
AppComponentのテンプレートであるapp.component.html
では、AngularのngIf
ディレクティブを使うことによって「テトリススタート画面」と「テトリスプレイ画面」の両方を表示しています。スコアscore$
とプレイ時間digitalTimer$
については、Observable型であるため、async
パイプを使用して表示しています。
src/app/app.component.scss
の一部抜粋:
.title { .flash { animation: Flash 1s infinite; } } @keyframes Flash { 50% { opacity: 0; } }
「テトリススタート画面」におけるメッセージ「Press spacebar to start the game」点滅はCSSアニメーションを使って実現しています。
// ブロックを盤面の上の方にセットする this.tetrisAction.setCurrentX(5); this.tetrisAction.setCurrentY(0);
AppComponentや各種サービスクラスでStateを変更する必要がある場合は、↑の通り、Actionクラスのメソッドを呼び出す必要があります。
Action
state/action.ts
の一部抜粋:
export type VoidAction = FluxStandardAction<void, void>; export type StringAction = FluxStandardAction<string, void>; export type InsertShapeToCurrentAction = FluxStandardAction<InsertShapeToCurrentParam, void>; @Injectable() export class TetrisActions { static readonly INSERT_SHAPE_TO_CURRENT = 'INSERT_SHAPE_TO_CURRENT'; static readonly SET_CURRENT_X = 'SET_CURRENT_X'; @dispatch() insertShapeToCurrent = (insertShapeToCurrentParam: InsertShapeToCurrentParam): InsertShapeToCurrentAction => ({ type: TetrisActions.INSERT_SHAPE_TO_CURRENT, payload: insertShapeToCurrentParam, meta: undefined }) @dispatch() setCurrentX = (positionX: number): NumberAction => ({ type: TetrisActions.SET_CURRENT_X, payload: positionX, meta: undefined })
AppComponentおよびサービスクラスから呼び出されたメソッドに応じて、ReducerにActionを(@dispatch
デコレーターで)ディスパッチする。
Reducer
state/reducer.ts
の一部抜粋:
export function rootReducer( lastState: IAppState, action: Action ): IAppState { switch (action.type) { case TetrisActions.INSERT_SHAPE_TO_CURRENT: const x = (action as InsertShapeToCurrentAction).payload.x; const y = (action as InsertShapeToCurrentAction).payload.y; const id = (action as InsertShapeToCurrentAction).payload.id; // 配列の値渡し const newCurrent = lastState.current.concat(); newCurrent[y][x] = id; return { board: lastState.board, isLose: lastState.isLose, current: newCurrent, currentX: lastState.currentX, currentY: lastState.currentY, rotated: lastState.rotated, score: lastState.score, digitalTimer: lastState.digitalTimer, isGameStart: lastState.isGameStart, }; case TetrisActions.SET_CURRENT_X: return { board: lastState.board, isLose: lastState.isLose, current: lastState.current, currentX: (action as NumberAction).payload, currentY: lastState.currentY, rotated: lastState.rotated, score: lastState.score, digitalTimer: lastState.digitalTimer, isGameStart: lastState.isGameStart, }; default: return lastState; } }
state/store.ts
:
import { Action } from 'redux'; import { TetrisActions } from '../state/actions'; export interface IAppState { board: number[][]; // 盤面情報 isLose: boolean; // 一番上までいっちゃったかどうか current: number[][]; // 今操作しているブロックの形 currentX: number; // ブロックの現在の座標 currentY: number; // ブロックの現在の座標 rotated: number[][]; score: number; digitalTimer: string; isGameStart: boolean; } export const INITIAL_STATE: IAppState = { board: null, // 盤面情報 isLose: false, // 一番上までいっちゃったかどうか current: null, // 今操作しているブロックの形 currentX: null, // ブロックの現在の座標 currentY: null, // ブロックの現在の座標 rotated: null, score: 0, digitalTimer: '0:0:00', isGameStart: false, };
Reducerでは、Actionから受け取った情報に応じてStateの次の状態を作成し、作成したStateを返却することによって画面の状態を最新化します。
store.ts
では、IAppState
でStateのプロパティを、INITIAL_STATE
でStateの初期状態を定義しています。