ブロック落下ゲームの作り方 – テトリス風のパズルゲームを作ろう(後編)

ゲーム

2回にわけてミニマムに遊べるテトリス風の落ちものパズルゲームを作っていきます。ゲームエンジンやライブラリを使わずに、素のHTMLとJavaScriptだけでゲームを作ります。

以下のような人のためにあります。

ゲームを作ってみたいけど、何をどうしていいか分からない。
単純なゲームを作りたいだけなのに、UnityとかUnreal Engineは面倒くさい。

ある程度プログラミングの知識がある人を対象にしています。

先に読んでおくもの

この記事には前編があります。ここでは語らない、ブロックが落ちる、積み上がる、行が消えるなどの実装については、下記記事を読んでください。

この記事で作るもの

テトリスっぽいゲームが遊べるように完成させます。

  • ブロックの種類の定義
  • ブロックの回転
  • ゲームの状態管理

を作っていきます。

ソースコード

前編と同じ部分、記事で説明している部分は一部省略しています。

const canvas = document.getElementById('gameCanvas');
const context = canvas.getContext('2d');

const SCREEN_W = 10
const SCREEN_H = 20
const BLOCK_SIZE = 20

const State = {
    STANBY: 0,
    STARTING: 1,
    GAME: 2,
    GAME_OVER: 3
}

let screen = Array(SCREEN_H*SCREEN_W);

// ブロック
let blockX = 5;
let blockY = 0;
let rotate = 0;

const blockShapeTable = [
// 省略
]

let blockType;

// ゲーム状態管理
let gameState = State.STANBY;

// 入力ハンドラー
document.addEventListener('keydown', keyDownHandler, false);

function keyDownHandler(event) {
    if (event.key === 'Left' || event.key === 'ArrowLeft') {
        if (!blockHit(blockX-1, blockY, blockType, rotate))
            blockX -= 1;
    } 

    if (event.key === 'Right' || event.key === 'ArrowRight') {
        if (!blockHit(blockX+1, blockY, blockType, rotate))
            blockX += 1;
    }

    if (event.key === 'd') {
        if (!blockHit(blockX, blockY+1, blockType, rotate))
            blockY += 1;
    }

    if (event.key === 'x') {
        new_rotate = (rotate+1)%4;
        if (!blockHit(blockX, blockY+1, blockType, new_rotate))
            rotate = new_rotate;
    }

    if (event.key == 's') {
        if (gameState == State.STANBY) gameState = State.STARTING;
    }
    if (gameState == State.GAME_OVER) gameState = State.STANBY; 
}

function drawRect(x, y, color) {
    if (color==0) {
        context.fillStyle = 'black';
        context.fillRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
    } else {
        context.fillStyle = color;
        context.fillRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);

        context.strokeStyle = 'gray';
        context.lineWidth = 1;
        context.strokeRect((x+1) * BLOCK_SIZE, (y+1) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
    }
}

function drawScreen() {
    for (j = 0; j < SCREEN_H; j++) {
        for (i = 0; i < SCREEN_W; i++) {
            color = screen[ i + j*SCREEN_W ];
            drawRect(i, j, color);
        }
    }
}

function hasBrick(x,y) {
    if (x < 0 || x >= SCREEN_W || y >= SCREEN_H)
        return true;
    return 0!=screen[x + y*SCREEN_W];
}

function drawFallBlock(x, y, block, rotate) {
// 記事で説明しているので省略
}

function blockHit(x, y, block, rotate) {
// 記事で説明しているので省略
}

function putBlock(x, y, block) {
// 記事で説明しているので省略
}

function setBrick(x, y, block) {
    screen[x + y*SCREEN_W] = block;
}

// ・・・省略

const WAIT_COUNT = 40;
let wait_count = 0;

function is_in_interval(count) {
    wait_count += 1;
    if (wait_count < count) {
        return true;
    }
    wait_count = 0;
    return false;
}

function update() {
    if (is_in_interval(WAIT_COUNT))
        return;

    if (blockHit(blockX, blockY+1, blockType, rotate)) {
        putBlock(blockX, blockY, blockType, rotate); // ブロックを積み上げる
        // check Game Over
        for (i = 0; i < SCREEN_W; i++) {
            if (screen[i] != 0) 
                gameState = State.GAME_OVER;
        }
        initFallBlock();
        return;
    }

    checkLineComplete(); 

    blockY += 1; 
    
}

function draw_text_center(text, color='#fff') {
    x = BLOCK_SIZE;
    y = canvas.height/2 - 40;
    context.fillStyle = 'black';
    context.fillRect(x, y, canvas.width-BLOCK_SIZE*2, 60);

    context.fillStyle = color;
    context.font = '16px Consolas';
    context.textAlign = 'left';
    text_w = context.measureText(text).width;
    context.fillText(text, canvas.width/2-text_w/2, canvas.height/2);
}

function initFallBlock() {
    blockX = 4;
    blockY = 0;
    rotate = 0;
    bsi = Math.floor(Math.random()*7);
    blockType = blockShapeTable[bsi];
}

function init() {
    initFallBlock();
    screen.fill(0);
    rotate = 0;
}

function clearScreen() {
    context.fillStyle = 'gray'
    context.fillRect(0, 0, canvas.width, canvas.height);
}

function draw() {
    switch (gameState) {
    case State.STANBY:
        clearScreen();
        draw_text_center("Press 'S' Key to Start");
        break;
    case State.STARTING:
        init();
        gameState = State.GAME;
        break;
    case State.GAME:
        clearScreen();
        drawScreen();
        drawFallBlock(blockX, blockY, blockType, rotate);
        update();
        break;
    case State.GAME_OVER:
        draw_text_center("Game Over!!", '#f00');
        break;
    }
}

setInterval(draw, 10);

ブロックの種類

ブロックの形は下記の図の通り、7種類あります。

4 x 2の配列で形を定義できますが、扱いやすいので1次元配列で実装しています。ソースコードの下記の部分です。

const blockShapeTable = [
    { 
        shape: [1,1,0,0,
                1,1,0,0], // O type
        color: 'yellow'
    },
    {
        shape: [1,1,1,1,
                0,0,0,0], // I type
        color: 'aqua'
    },
    {
        shape: [0,1,1,0,
                1,1,0,0], // S type
        color: 'green'
    },
    {
        shape: [1,1,0,0,
                0,1,1,0], // Z type
        color: 'red'
    },
    {
        shape: [1,0,0,0,
                1,1,1,0], // L type
        color: 'orange'
    },
    {
        shape: [0,0,1,0,
                1,1,1,0], // J type
        color: 'blue'
    },
    {
        shape: [1,1,1,0,
                0,1,0,0], // T type
        color: 'purple'
    }
]

ブロックの描画と回転

まずは、回転していない場合の、落ちるブロックの描画について説明します。

ブロックの形は4 x 2の配列のデータで表されます。0はなにもない、1はブロックのピースが存在することを示します。下記図のように横から順に要素を舐めていって、データが1だった場合に、画面の該当する座標に塗りつぶし矩形を描画します。

(x, y) がブロックの形状データの左上 (i=0, j=0)だとして、図のブロックの形だと、(i=1, j=0), (i=2, j=0), (i=0, j=1), (i=1, j=1)がピースが存在するので、(x+1, y), (x+2, y), (x, j+1), (x+1, y+1) の座標に矩形を描画する訳です。

下記が、ソースの実装部分です。drawFallBlock()関数の中の case 0: の部分です。

function drawFallBlock(x, y, block, rotate) {
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 4; i++) {
            if (block.shape[i+j*4]==1) {
                switch(rotate) {
                case 0: drawRect(x+i, y+j, block.color); break;
                ・・・
                }
            }
        }
    }
}

左に90°回転

ブロックの回転は、ブロックのピースの矩形を描画する時のi, j の方向を変えることで実現します。左に90°回転したときは、下記図の通りです。

コードは先ほどと同じdrawFallBlock()関数の中の、case 1: の部分です。描画する際の座標の計算が (x + j, y – i) となっているのがポイントです。

function drawFallBlock(x, y, block, rotate) {
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 4; i++) {
            if (block.shape[i+j*4]==1) {
                switch(rotate) {
                ・・・
                case 1: drawRect(x+j, y-i, block.color); break;
                ・・・
                }
            }
        }
    }
}

左に180°回転、270°回転

ソースコードでは、drawFallBlock()関数の中の、case 2: case 3: の部分です。
drawFallBlock()関数の全貌は下記のとおりです。

function drawFallBlock(x, y, block, rotate) {
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 4; i++) {
            if (block.shape[i+j*4]==1) {
                switch(rotate) {
                case 0: drawRect(x+i, y+j, block.color); break;
                case 1: drawRect(x+j, y-i, block.color); break;
                case 2: drawRect(x-i, y-j, block.color); break;
                case 3: drawRect(x-j, y+i, block.color); break;
                }
            }
        }
    }
}

回転の中心をどこにするか?

ここでは実装を簡単にするために、回転の中心をブロックの形データの左上のピース(i=0, j=0)としました。実際、これでも遊べますが、ちょっと「回転がきれいじゃない」と感じる人もいるかと思います。O typeでも回転しちゃいますしね。

もう少し工夫して、形ごとに回転の中心を変えてみるということも考えられます。興味ある人は、いろいろ実装を変えてみてください。

回転した状態の当たり判定と積み上げ処理

落ちるブロックの描画と考え方は同じです。当たり判定の関数と積み上げ処理の関数は、前回の記事で紹介したものがベースになります。これに対して回転の方向の処理を追加する、だけです。

function blockHit(x, y, block, rotate) {
    // ブロックの形データ4 x 2を見ていく
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 4; i++) {
            if (block.shape[i+j*4]==1) {
                flg = false;
                switch(rotate) {
                case 0: flg = hasBrick(x+i, y+j); break;
                case 1: flg = hasBrick(x+j, y-i); break;
                case 2: flg = hasBrick(x-i, y-j); break;
                case 3: flg = hasBrick(x-j, y+i); break;
                }
                if (flg) return true;
            }
        }
    }
    return false;
}

function putBlock(x, y, block) {
    // ブロックの形データ4 x 2を見ていく
    for (j = 0; j < 2; j++) {
        for (i = 0; i < 4; i++) {
            if (block.shape[i+j*4]==1) {
                switch(rotate) {
                case 0: flg = setBrick(x+i, y+j, block.color); break;
                case 1: flg = setBrick(x+j, y-i, block.color); break;
                case 2: flg = setBrick(x-i, y-j, block.color); break;
                case 3: flg = setBrick(x-j, y+i, block.color); break;
                }
            }
        }
    }
}

ゲームの状態管理

一応、ゲームっぽくするために、最低限、以下の状態遷移を作っています。

もう少し演出を増やして、例えば行が消える時にチカチカとアニメーションさせる、とかさせたい場合には状態を増やす必要があります。

さて、この後は

元祖落ちゲーのテトリス以降、いろいろなバリエーションの落ちゲーが出ました。筆者はコラムス、ぷよぷよくらいしか知らないですが、ググると色々あります。これらのバリエーションを自分で作っていくのも楽しいと思います。


コメント

タイトルとURLをコピーしました