はじめに
以前、Angular5で作成したテトリスをAngular + Reduxで作り直してみました。
https://l08084.github.io/ng-redux-tetris/
実際に👆のURLから遊ぶことができて(PCのChromeでのみ動作確認済み)、
github.com
GitHubにソースコードも置いています。
関連記事
l08084.hatenablog.com
Angular + Reduxの環境構築手順については👆の記事を参照してください。
l08084.hatenablog.com
Angular5でテトリスを作った時の記事は👆
遊び方
SPACEキー
でゲームが開始します。
→
: ブロックを右へ移動
←
: ブロックを左へ移動
↓
: ブロックを下へ移動
↑
: ブロックを回転
ブロックの左右移動・回転については、画面に配置してあるボタンでも操作可能です。
実装
開発環境
構成
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の初期状態を定義しています。
参考サイト
qiita.com
coderecipe.jp
github.com