中安拓也のブログ

プログラミングについて書くブログ

Angular + Reduxでテトリスを作る

f:id:l08084:20180224195842g:plain

はじめに

以前、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キーでゲームが開始します。

  • : ブロックを右へ移動

  • : ブロックを左へ移動

  • : ブロックを下へ移動

  • : ブロックを回転

ブロックの左右移動・回転については、画面に配置してあるボタンでも操作可能です。

実装

開発環境

  • 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

構成

f:id:l08084:20180226154728p:plain 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