テトリスの作り方(其の二)

概要:自作プログラム『SPACE TETRIS』の作り方を丁寧に解説!

■ピースを回転させる

私が引っ掛かっていた事の一つがこれです。
回転中に他のブロックをすり抜けたり、回転後に他のブロックの中に埋まったり……
などなどおかしな現象が起こらないようにするにはどうしたらいいんだろう?と。
ここでも“割り切り”が重要です。
どうするかというと、回転中に他のブロックをすり抜けるのは黙認してしまうのです。

具体的には、回転したピースを作成してその有効性を調べます。
回転する為に他のブロックとぶつかることになろうが知った事じゃありません。
とにかく回転できたという仮定をぶちたてて、その真偽を調べるのです。

回転は右方向45度です。
私は左方向は必要ないと判断しましたが、そこはあなたは好みで変えて下さい。

/* ブロックを回転させる */
// 戻り値:TURE(回転した) or FALSE(回転不可)
BOOL TurnPiece(void)
{
    int x,y,offsetX,offsetY;
    BYTE pTurn[PIECE_WIDTH][PIECE_HEIGHT];
    DWORD pcTurn[PIECE_WIDTH][PIECE_HEIGHT];

    /* 回転したブロックを生成する */
    for(y=0;y<PIECE_HEIGHT;y++){
        for(x=0;x<PIECE_WIDTH;x++){
            pTurn[(PIECE_HEIGHT-1)-y][x]=piece[x][y];
            pcTurn[(PIECE_HEIGHT-1)-y][x]=pColor[x][y];
        }
    }
    /* 回転可能かどうかを調べる */
    for(y=0;y<PIECE_HEIGHT;y++){
        for(x=0;x<PIECE_WIDTH;x++){
            if(pTurn[x][y]){
                offsetX=(location.x)+x;
                offsetY=(location.y)+y;
                if(offsetX<0 || offsetX>=FIELD_WIDTH
                    || offsetY>=FIELD_HEIGHT    // ↓offsetY>=0 は添字の有効性を調べている
                    || (offsetY>=0 && field[offsetX][offsetY])){    //既にブロックがある
                        return FALSE;
                }
            }
        }
    }
    for(y=0;y<PIECE_HEIGHT;y++){
        for(x=0;x<PIECE_WIDTH;x++){
            piece[x][y]=pTurn[x][y];
            pColor[x][y]=pcTurn[x][y];
        }
    }
    return TRUE;
}

難しいのは次の箇所でしょう。

if(pTurn[x][y]){
  offsetX=(location.x)+x;
  offsetY=(location.y)+y;
  if(offsetX<0 || offsetX>=FIELD_WIDTH
    || offsetY>=FIELD_HEIGHT  // ↓offsetY>=0 は添字の有効性を調べている
    || (offsetY>=0 && field[offsetX][offsetY])){  //既にブロックがある
        return FALSE;
  }
}

if(pTurn[x][y]) は回転したピースの中でブロックのある位置を探しています。
もしブロックがあれば
offsetX=(location.x)+x;
offsetY=(location.y)+y;
でそのブロックのマス座標における位置を計算します。

offsetX<0 || offsetX>=FIELD_WIDTH || offsetY>=FIELD_HEIGHT
は回転したブロックがゲームフィールドから出ていないかを調べています。
当然の事ながらゲームフィールドを出たら回転不可です。
ただし、上へは出ても良いことにします。

(offsetY>=0 && field[offsetX][offsetY])
はゲームフィールドにブロックがあるかを調べています。
ゲームフィールドにもうブロックがあるのならば回転不可です。
ところで offsetY は負の値となることもあるので事前にチェックしているのです。
おそらく皆さんは大丈夫でしょうが、
条件文がチェックされる順番や真偽確定における動作に注目して下さい。
上のように書けば絶対にありえない配列にアクセスする事はありませんよね?

このチェックをくぐり抜ければ回転可能だと判断できます。
従って、回転したピースをもとのピースにコピーします。
回転不可なら回転したピースからもとのピースへのコピーは行われないので、
回転せずそのままということになります。

■ピースを固定する

ちょっと話を戻して、下方向に移動しようとしたけどできなかった場合を考えてみましょう。
それはつまり、ピースがゲームフィールドの下かブロックの上段に達したということであり、
ピースをその場所に固定させなければなりません。

ここでちょっとした問題が起きます。
ゲームフィールドの上にはみ出したブロックはどうするのか?という問題です。
上だけ見えないゲームフィールドを用意するという方法も考えられますが、
複雑化しますし、新しいブロックの出現位置が塞がれて
プレイヤーの見えない場所でゲームオーバーになるなどの問題も別途発生するため、
ここでも“割り切って”上にはみ出したブロックは消えるという仕様にしましょう。
プレイヤーにとっても優しい仕様だといえます。

/* ブロックを位置情報に従ってフィールドにコピーする */
void PieceToField(void)
{
    for(int y=0;y<PIECE_HEIGHT;y++){
        for(int x=0;x<PIECE_WIDTH;x++){    // ↓(location.y)+y>=0 は添字の有効性を調べている
            if(piece[x][y] && (location.y)+y>=0){
                field[(location.x)+x][(location.y)+y]=piece[x][y];
                fColor[(location.x)+x][(location.y)+y]=pColor[x][y];
            }
        }
    }
}

■埋まっている行を削除する

ピースを固定させたら各行が埋まっているかどうかをチェックしなければなりません。

field[][] にはブロックがあれば 1 、なければ 0 という値が記録されています。
従って、これらを一行分足した合計が FIELD_WIDTH と等しければ、その行は埋まっていると判断できます。

走査は下から行います。
浮いているブロックというのはあり得ないので、
field[][] の合計値が 0 ならばその行も含めてそれより上にはもうブロックはないと判断できます。

/* 各行を調べ、行が埋まっている場合は行を削除する */
// 戻り値:削除した行数
int DeleteLine(void)
{
    int x,y,delCount=0;
    for(y=FIELD_HEIGHT-1;y>=0;y--){
        int lineCount=0;
        for(x=0;x<FIELD_WIDTH;x++){
            lineCount+=field[x][y];
        }

        if(lineCount==0) break;    /* これより上にブロックはない */
        if(lineCount!=FIELD_WIDTH) continue;

        /* 一行削除する */
        delCount++;
        for(x=0;x<FIELD_WIDTH;x++){
            field[x][y]=0;
        }
    }
    return delCount;
}

返却値は削除した行数です。
これは点数計算に使うことができます。

■削除した行を詰める

これがちょっとやっかいです。
というのも、削除した行と本当に空白だった行との区別が難しいからです。
削除したのは一行だけかもしれませんし、四行連続で削除したかもしれません。
したがって、空白行がなくなるまで詰めなければならないのですが……
本当に空白だった行、つまりそれ以上ブロックがない行に対して処理したら無限ループにおちいります。
四行より多いことはないという条件を使えば無限ループを回避できますが、
もっと賢い方法として、外部から削除した行数を与えてもらうという解法があります。

ShiftLine() 関数で重要なのは y の値をいつ変更するか?です。
行を詰めても上から降りてきた行もまた空白行だったという事もあり得るので、
行を詰めた場合は y の値を変更せずにもう一度同じ行を調べます。

ちょっと難しいプログラムですが、上の行がシフトしてくる様子をイメージできれば理解できるでしょう。

// 削除した行を詰める
void ShiftLine(int delCount)    // delCount:削除した行数
{
    int x,y;
    for(y=FIELD_HEIGHT-1;y>=0 && delCount>0; ){
        int lineCount=0;
        for(x=0;x<FIELD_WIDTH;x++){
            lineCount+=field[x][y];
        }

        if(lineCount!=0){
            y--;
            continue;
        }

        // 一行詰める
        delCount--;
        for(int iy=y;iy>=0;iy--){
            for(x=0;x<FIELD_WIDTH;x++){
                if(iy-1>=0){
                    field[x][iy]=field[x][iy-1];
                    fColor[x][iy]=fColor[x][iy-1];
                }else{
                    field[x][0]=0;    /* 0 行より上はないので 0 で埋める */
                    fColor[x][0]=0;
                }
            }
        }
    }
}

ところで、簡単化のためには DeleteLine() 関数と ShiftLine() 関数は統合してしまうべきですが、
演出上の問題で二つに分けました。
プレイヤーに確かに行が消えたという状況を見せるためにはこうするのが最適解です。
つまり、DeleteLine() 関数を実行してから ShiftLine() 関数を実行するまでに時間をおくのです。
もしも二つの関数を統合してしまったら、行が消えたということは視覚されずに
シフトしただけのようにプレイヤーには映ることでしょう。
これはかなり見づらいですし、大きすぎる制約として他の類似ゲームへの応用が利かなくなってしまいます。


前へ 2/4 次へ

戻る / ホーム