SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

ゲームプログラミング入門

落ち物ゲームの作り方 第2回:「聖夜の落とし物」編

隣接する同じ種類のブロックを走査する方法


  • X ポスト
  • このエントリーをはてなブックマークに追加

一定数以上つながっていたら消滅フラグを立てる

 前述のCount関数とVanish関数を呼び出す関数です。直接に消すことができるのは色ブロックだけなので、妨害ブロックと空セルがいくつ繋がっているかは調べません。何個つなげたら消えるかはg_del_minグローバル変数に設定しておきます。

BYTE g_del_min=4;    // 何個つなげたら消えるか

// 色ブロックの繋がり具合を調べる
static DWORD CheckBlocks(void)    // TETRA から変更
{
    FIELD f;
    BYTE x,y;

    // コピーを作る
    for(y=0;y<FH;y++)
        for(x=0;x<FW;x++)
            f.image[y][x]=g_field.image[y][x];

    DWORD count=0;

    for(y=0;y<FH;y++)
    {
        for(x=0;x<FW;x++)
        {
            BYTE c=f.image[y][x];
            if(c!=NONE && c!=JAMA && c!=KATA)
            {
                DWORD n=0;
                Count(&f,y,x,&n);    // 繋がっている個数を調べる
                if(n>=g_del_min)
                {
                    Vanish(y,x);     // 消滅フラグを立てる
                    count+=n;
                }
            }
        }
    }

    // 消滅フラグを立てた色ブロックの個数(妨害ブロック含まず)
    return count;
}

一定数以上つながったブロックを消す

 消滅フラグを見れば消すブロックは分かりますが、KATAブロックは他と動作が異なります。隣接している色ブロックが1つだけならJAMAブロックに成り、2つ以上なら消えます。ちなみに、消滅フラグが立った時点で一つも隣接していないということはあり得ません。

// 消滅フラグの立っている全てのブロックを消す
static BYTE DeleteBlocks(void)    // TETRA から変更
{
    // KATA ブロック
    for(BYTE y=0;y<FH;y++)
    {
        for(BYTE x=0;x<FW;x++)
        {
            if(g_field.vanish[y][x]==false ||
               g_field.image[y][x]!=KATA) continue;

            BYTE c,colors=0;
            if(x+1<FW && g_field.vanish[y][x+1] &&
               (c=g_field.image[y][x+1])!=JAMA && c!=KATA) colors++;
            if(y+1<FH && g_field.vanish[y+1][x] &&
               (c=g_field.image[y+1][x])!=JAMA && c!=KATA) colors++;
            if(x-1>=0 && g_field.vanish[y][x-1] &&
               (c=g_field.image[y][x-1])!=JAMA && c!=KATA) colors++;
            if(y-1>=0 && g_field.vanish[y-1][x] &&
               (c=g_field.image[y-1][x])!=JAMA && c!=KATA) colors++;

            if(colors==1) g_field.image[y][x]=JAMA;
            else if(colors>=2) g_field.image[y][x]=NONE;
            g_field.vanish[y][x]=false;
        }
    }

    // JAMA と 色ブロック
    BYTE count=0;
    for(BYTE y=0;y<FH;y++)
    {
        for(BYTE x=0;x<FW;x++)
        {
            if(g_field.vanish[y][x]==false) continue;

            if(g_field.image[y][x]!=JAMA) count++;

            g_field.image[y][x]=NONE;
            g_field.vanish[y][x]=false;
        }
    }
    return count;    // 消した色ブロックの個数
}

浮いているブロックを一行だけ落とす

 「TETRA」と違い、ブロックはピース単位ではなく、それ単体で落下します。従って、どこが浮いているのかは消滅フラグから判定できません。というのも、ピースがフィールドに固定されたときにも浮きブロックが発生するからです。さらに、直接フィールドに配置される妨害ブロックも同様です。というわけで、前述のDeleteBlocks関数で消滅フラグを消しています。

 浮き判定は、空セルの上にブロックがあるかを調べればわかります。空セルの開始座標は、詰めるセルにもなるので、保存しておきます。そして、もし浮きブロックがあれば一行だけ落とします。以上のことを全ての列について行います。

// 浮いているブロックを一行だけ落とす
static DWORD FallBlocks(void)    // TETRA から変更
{
    char x,y,yy;
    DWORD count=0;

    for(x=0;x<FW;x++)
    {
        // 空セルを探す
        for(y=FH-1;y>=0 && g_field.image[y][x]!=NONE;y--);
        // ブロックを探す
        for(yy=y;yy>=0 && g_field.image[yy][x]==NONE;yy--);
        if(yy<0) continue;    // 浮いているブロックは無い

        // 一行だけ落とす
        count++;
        for(;y>=0;y--)
        {
            if(y-1>=0) g_field.image[y][x]=g_field.image[y-1][x];
            else g_field.image[y][x]=NONE;
        }
    }

    return count;    // ブロックを落とした列数
}

浮いているブロックがあるかを調べる

 単に浮きブロックの有無を調べるなら次のようになります。この情報は、浮きブロックを落下させるアニメーションを行うか、などの判定で必要になります。

// 浮いているブロックの有無を返す
static bool IsFloat(void)    // TETRA から追加
{
    char x,y;
    for(x=0;x<FW;x++)
    {
        // 空セルを探す
        for(y=FH-1;y>=0 && g_field.image[y][x]!=NONE;y--);
        // ブロックを探す
        for(;y>=0 && g_field.image[y][x]==NONE;y--);
        if(y>=0) return true; // 浮いているブロックがある
    }
    return false;    // 浮いているブロックが無い
}

ゲームオーバーしているかを調べる

 ゲームオーバーの判定にも、浮き判定が組み込まれています。「上から2段目、左から3列目にブロックがあって、さらにそのブロックが落下できない」がゲームオーバーの条件です。最上段は表示しないので、上から二段目です。この関数が呼び出されるときにブロックが浮いているという状況は、妨害ブロックによって起こりえます。後で詳しく解説しますが、妨害ブロックは直接フィールドに配置するからです。

 グローバル変数g_anim_idxはアニメーションの番号です。0以外ならアニメーション中であることを表します。

// ゲームオーバーしているかを調べる
static bool CheckGameOver(void)    // TETRA から変更
{
    if(g_field.image[1][2]!=NONE && g_anim_idx==0)
    {
        // 空セルを探す
        for(BYTE y=2;y<FH && g_field.image[y][2]!=NONE;y++);
        if(y==FH) return true;    // 落下できない
    }
    return false;
}

妨害ブロックを配置する

 妨害ブロックはフィールドの最上段に配置します。作る数はグローバル変数g_dis_numに設定されています。また、妨害ブロックの種類はg_disturbに設定されています。

 まず最上段にブロックが無いセル座標を一時配列に記録して、その個数をカウントします。そして、この一時配列をシャッフルして、先頭要素から参照することで、ランダムな座標に妨害ブロックを配置します。

 置きたい数が置ける数より多い場合は、一行落としてから、再び最上段に配置することを繰り返します。一行落とし、再びこの関数を呼び出す処理は外部から行います。

static BYTE g_dis_num=0;     // 妨害ブロックの残り数
BYTE g_disturb=JAMA_KATA;    // 妨害ブロックを出現させるか

// 妨害ブロックを出現させるか
enum
{
    INVALID,      // 出現させない
    JAMA_ONLY,    // JAMA だけ
    JAMA_KATA,    // JAMA と KATA
    KATA_ONLY     // KATA だけ
};

// 妨害ブロックを配置
static void CreateDisturb(void)    // TETRA から追加
{
    BYTE x,a,b,kind;
    BYTE fx[FW],count=0;

    // 最上段にブロックが無いセル座標と個数を記録
    for(x=0;x<FW;x++)
    {
        if(g_field.image[0][x]==NONE) fx[count++]=x;
    }
    if(count==0) return;    // 置ける場所が無い

    if(g_dis_num<count)     // 置きたい数 < 置ける数
    {
        // シャッフル
        for(x=0;x<count;x++)
        {
            a=fx[x];
            fx[x]=fx[ b=(BYTE)(rand()%count) ];
            fx[b]=a;
        }
    }

    BYTE n=(g_dis_num<count)? g_dis_num:count;    // 置く数

    // 妨害ブロックを配置
    for(x=0;x<n;x++)
    {
        if(g_disturb==JAMA_ONLY) kind=JAMA;
        else if(g_disturb==JAMA_KATA) kind=(BYTE)(rand()%3) ?
            JAMA:KATA;
        else kind=KATA;     // g_disturb==KATA_ONLY
        g_field.image[0][ fx[x] ]=kind;
    }

    g_dis_num-=n;    // 残りの個数を計算
}

ピースが下に移動できなかったら

 ピースが下に移動できなかった場合は、まず浮いているブロックの有無を調べて、次に一定数以上つながっているブロックの有無を調べ、最後に妨害ブロックを出現させるかを判定します。順番が重要です。「TETRA」とは違って、どのアニメーションから行うか決まっていないので、ここで決める必要があります。

static BYTE g_anim_idx=0;      // アニメーションの番号
static DWORD g_anim_wait=0;    // アニメーションの待機時間

BYTE g_dis_max=4;              // 妨害ブロックが一度に出現する最大数
BYTE g_dis_rnd=10;             // 妨害ブロックの出現頻度[%]

// ピースが下に移動できなかった時の処理
static void PieceNext(void)    // TETRA から変更
{
    FixPiece();
    if(CheckGameOver() || g_dis_num>0) return;

    if(IsFloat())
    {
        g_anim_idx=2;    // 落下アニメーションへ移行
        g_anim_wait=500;
        return;
    }
    if(CheckBlocks())
    {
        g_anim_idx=1;    // 消滅アニメーションへ移行
        g_anim_wait=1000;
        return;
    }
    if(g_dis_rnd > rand()%100)
    {
        g_anim_idx=3;    // 妨害ブロックアニメーションへ移行
        g_dis_num=rand()%g_dis_max + 1;    // 妨害ブロックの個数
        g_anim_wait=500;
        return;
    }

    CopyPiece();
    CreatePiece(&g_next);
}

アニメーション

 妨害ブロックアニメーションを追加しました。CreateDisturb関数で妨害ブロックを配置して、FallBlocks関数で浮いているブロック(今は妨害ブロックのみ)を一行だけ落とします。それでも置ききれなかった妨害ブロックがある場合は、いったん関数を抜けて、再び妨害ブロックアニメーションを実行します。全て置き終えて、浮いているブロックがあれば、落下アニメーションに移行します。

 落下アニメーションが終了したら、新たに一定数以上つながったブロックがないかを調べます。もしあれば、連鎖ということになります。

// アニメーション制御
static bool AnimNext(void)    // TETRA から変更
{
    static BYTE chain=0;
    static DWORD progress=0;

    if(g_anim_idx==0)
    {
        chain=0;
        progress=0;
        return false;
    }

    progress+=SPF;
    if(progress<g_anim_wait) return true;
    progress=0;

    BYTE blocks;

    switch(g_anim_idx)
    {
    case 1:    // 消滅アニメーション
        blocks=DeleteBlocks();
        g_score+=(DWORD)pow(100*blocks,++chain);
        g_anim_idx=2;
        g_anim_wait=500;
        return true;
    case 2:    // 落下アニメーション
        FallBlocks();
        g_anim_wait-=100;
        if(g_anim_wait<100) g_anim_wait=100;
        if(IsFloat()) return true; // まだ浮いているブロックがある
        if(CheckBlocks())          // 連鎖判定
        {
            g_anim_idx=1;
            g_anim_wait=1000;
            return true;
        }
        break;
    case 3:    // 妨害ブロックアニメーション
        CreateDisturb();
        FallBlocks();    // 妨害ブロックを落下
        // まだ配置していない妨害ブロックがある
        if(g_dis_num>0) return true;
        if(IsFloat())
        {
            g_anim_idx=2;
            return true;
        }
        break;
    }

    CopyPiece();
    CreatePiece(&g_next);

    g_anim_idx=0;
    chain=0;

    return false;
}

ゲームのルールを変更する

 既に示したこれらの変数をダイアログボックスから変更できるようにすると、プレイヤーがゲームのルールを変えて遊ぶことができます。

// ゲームのルール
BYTE g_colors=6;             // 色ブロックの種類
BYTE g_del_min=4;            // 何個つなげたら消えるか
BYTE g_disturb=JAMA_KATA;    // 妨害ブロックを出現させるか
BYTE g_dis_max=4;            // 妨害ブロックが一度に出現する最大数
BYTE g_dis_rnd=10;           // 妨害ブロックの出現頻度[%]
DWORD g_waitTime=1000;       // 落下するまでの待機時間[ms]

まとめ

 落ち物ゲームの第2回ということで、第1回の「TETRA」を基に、その応用として「聖夜の落とし物」というゲームを作りました。こんな感じで、他の落ち物ゲームも作ることができます。

 しかし、アニメーションの管理手法には限界を感じます。マルチスレッドにすれば、綺麗に書けるのですが、マルチスレッド特有の嫌な問題も発生するので、タスクシステムを採用するという選択もあります。現在のままでは大きなゲームは、とても作れないので、自分にあった手法を確立してください。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
ゲームプログラミング入門連載記事一覧

もっと読む

この記事の著者

ひよこ(ヒヨコ)

職業ゲームプログラマーを志す学生です。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/228 2006/05/26 15:42

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング