課題3(超シンプルアクションゲーム)

 基本プログラムを改造し、単純アクションゲームを作る。

ゲーム概要

前準備

1.基本プログラムの準備

基本プログラムをコピーし、「kadai3」とする。

2.画像データの準備

今回使用する画像は次のとおりである。

画像ファイル名説明リソース名
Title.bmpタイトル画面用画像TITLE
backImg.bmpゲーム画面背景画像BACKIMG
moveChar.bmpキャラクタ画像1(動くもの)MOVECHAR
faulChar.bmpキャラクタ画像2(落ちてくるもの)FAULCHAR
String.bmp文字列画像STRINGS

画像ファイルをダウンロードし、基本プログラムの画像と入れ替える。上記画像が使えるよう「common.h」「myDraw.h」「myDraw.cpp」を修正する

ダウンロードした基本プログラムはキャラクタ画像用サーフェイスを1つしか用意していないが、この課題では上記のとおり2つのキャラクタ画像を使うため、DirectDrawサーフェイスオブジェクト関連の修正を行うこと。

3.common.hの修正

自キャラ、落下物の情報を格納する構造体をcommon.hに宣言する。

//-----------------------------------------------------------------------------
// 構造体宣言
//-----------------------------------------------------------------------------
typedef struct _STATUS {
    int     x;          //X座標
    int     y;          //Y座標
    RECT    rect;       //画像矩形
    RECT    hitRect;    //当たり判定矩形
    int     width;      //幅
    int     height;     //高さ
    int     speed;      //移動量
    int     anime;      //アニメーションパターン数
    int     anime_no;   //現在のコマ数
    int     movePattern;//動作パターン(0:静止、1:右移動、2:左移動)
    int     life;       //ライフ
} STATUS;

4.game.cppの修正

game.cppのローカル変数宣言を次のように修正、追加する。

//-----------------------------------------------------------------------------
// ローカル変数
//-----------------------------------------------------------------------------
static RECT     rcRect = {0, 0, 640, 480};      // 画面全体矩形
static RECT     GameRect = {150, 0, 630, 480};  // ゲーム画面短形
static STATUS   myChar;                         // 自キャラ情報

5.ゲーム初期化処理の修正

ゲーム初期化処理を次のように修正する。

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: ゲーム処理初期化
//-----------------------------------------------------------------------------
void GameInit(HWND hWnd)
{
    //------------------------------------------------------- 各変数の初期化
    SetRect(&myChar.rect, 64, 0, 96, 32);           // 自キャラの矩形
    myChar.width = myChar.rect.right-myChar.rect.left;  // 自キャラの幅
    myChar.height = myChar.rect.bottom-myChar.rect.top; // 自キャラの幅
    myChar.x = 480/2+GameRect.left-4;                   // 自キャラのX座標
    myChar.y = GameRect.bottom-myChar.height;           // 自キャラのY座標
    myChar.speed = 1;                                   // 自キャラの移動量

    //------------------------------------------------------- フレームナンバーセット
    g_FrameNo = GAME_FRAME;

}

設問a

 前準備の通りにプログラムを修正する。

実行結果サンプル

設問b

 課題2では、ゲームメイン処理(GameFrame関数内)がくしゃくしゃになってしまった。開発効率を上げるため、次のようなルールに基づいてプログラムする。
(詳しくは授業ノート参照)

処理フロー

 これらの処理を1つの関数内に記述すると非常に分かりづらくなるので、それぞれの処理を関数化したい。Game.cppを次のように変更する。

1.プロトタイプ宣言に次の行を追加

static HRESULT Haikei_Blt(void);
static void Char_Move(void);
static void Char_Hit(void);
static HRESULT Char_Blt(void);

2.GameFrame関数を次のように修正

//-----------------------------------------------------------------------------
// 関数名 : GameFrame()
// 機能概要: 画面更新処理
//-----------------------------------------------------------------------------
void GameFrame(HWND hWnd)
{
    // 背景描画処理
    if (Haikei_Blt() != DD_OK)
        return;

    // キャラクタ移動処理
    Char_Move();

    // キャラクタ当たり判定処理
    Char_Hit();

    // キャラクタ描画処理
    if (Char_Blt() != DD_OK)
        return;

}

3.背景画像描画処理関数を作成

//-----------------------------------------------------------------------------
// 関数名 : Haikei_Blt()
// 機能概要: 背景画像描画処理
//-----------------------------------------------------------------------------
static HRESULT Haikei_Blt(void)
{
    HRESULT         hRet;

    // バックバッファに背景画像を書き込む
    hRet = myBltFast(g_pDDSBack, 0, 0, g_pDDSBackimg, &rcRect, DDBLTFAST_NOCOLORKEY);
    if (hRet != DD_OK)
        return hRet;

    return DD_OK;

}

4.キャラクタ移動処理関数を作成

//-----------------------------------------------------------------------------
// 関数名 : Char_Move()
// 機能概要: キャラクタ移動処理
//-----------------------------------------------------------------------------
static void Char_Move(void)
{
    // マイキャラ移動処理
    if (keyTbl[VK_LEFT] &0x80)
        myChar.x -= myChar.speed;
    if (keyTbl[VK_RIGHT] &0x80)
        myChar.x += myChar.speed;

}

5.キャラクタ当たり判定処理関数を作成

//-----------------------------------------------------------------------------
// 関数名 : Char_Hit()
// 機能概要: キャラクタ当たり判定処理
//-----------------------------------------------------------------------------
static void Char_Hit(void)
{
    /* 左右の壁にぶつかったらゲームオーバー */
    if (myChar.x <= GameRect.left || myChar.x >= GameRect.right-(myChar.rect.right-myChar.rect.left))
    {
        g_FrameNo = START_INIT;
    }

}

6.キャラクタ描画関数を作成

//-----------------------------------------------------------------------------
// 関数名 : Char_Blt()
// 機能概要: キャラクタ描画処理
//-----------------------------------------------------------------------------
static HRESULT Char_Blt(void)
{
    HRESULT         hRet;

    // バックバッファに自キャラを書き込む
    hRet = myBltFast(g_pDDSBack, myChar.x, myChar.y, g_pDDSMoveChar, &myChar.rect, DDBLTFAST_SRCCOLORKEY);
    if (hRet != DD_OK)
        return hRet;

    return DD_OK;

}

[実行結果は設問aと同じ]

設問c

 自キャラのアニメーションを行う。アニメーションは「止まっている時」「右に移動しているとき」「左に移動しているとき」の3パターンあり、それぞれ2枚の画像がある。

 アニメーション画像は0.2秒毎に切り替える。

[HINT]

  1. アニメーションパターン数(anime)、現在のこま数(animeNo)はGameInit関数内で初期化する。
  2. コマ送り処理は、自キャラ時間制御用変数(g_charThisTime)をローカル変数定義で宣言し、 Char_Move関数内でこまを進めるかどうかの処理を行う。
  3. 自キャラ描画時、どの画像を使うかを計算する。

実行結果サンプル

設問d

 障害物を1つ落とす。障害物の現れる位置はランダムで、一番下まで落ちたら、また別の場所から落ちてくる。
 障害物は落下物画像内のブロックを使い、障害物情報はSTATUS構造体で宣言した変数に格納する。

[乱数発生方法]

 C言語の教科書及びVCのヘルプをまとめると、次のようになる。

/* 0から10までの乱数を無限に発生させる */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main(void)
{
    int     suu;

    /* 実行するたびに違う値が得られるように、現在の時刻値を使って
       乱数ジェネレータを初期化する。 */
    srand( (unsigned)time( NULL ) );

    while(1)
    {
        suu = (int)(10.0 * ((double)rand() / (double)RAND_MAX));
        printf("%d\n", suu);
    }

}

このプログラムを応用し、ブロックの出現位置をランダムに変える。ブロックはGameRect内に出現しなければならないので、X座標は150〜630の間ということである。今後のことを考え、出現時のブロックの座標を設定する関数を次のように作成する。

//-----------------------------------------------------------------------------
// 関数名 : SetBlock()
// 機能概要: ブロックの座標設定
//-----------------------------------------------------------------------------
void SetBlock(int *x, int *y)
{
	static double	getMax = GameRect.right-GameRect.left-Block.width;	// 求める乱数の最大

	*x = (int)(getMax * ((double)rand() / (double)RAND_MAX)) + GameRect.left;
	*y = 0;

}

この関数をGame.cpp内に作成し、ゲーム開始前初期化時と、ブロックが一番下まで落ちたときの2箇所で使用する。

[使用例]

SetBlock(&Block.x, &Block.y); // ブロックの座標設定

なお、乱数ジェネレータの初期化はプログラム開始時に一度行えばいいので、例えばstart.cppのStart_Init関数内で次のように行う。

/* 実行するたびに違う値が得られるように、現在の時刻値を使って乱数ジェネレータを初期化する */
srand(g_thisTickCount);

※g_thisTickCount変数は現在の時刻値を取得しているので、それを使った。

実行結果サンプル

設問e

 自キャラと障害物との当たり判定を行う。当たったら「GameOver」と一定時間表示し、スタート画面に戻る。

[当たり判定の考え方]

2つの矩形Rect1、Rect2の交差判定は次の式で求められる。
(詳しくは授業ノート参照)

if (Rect1.left <= Rect2.right && Rect1.top <= Rect2.bottom && Rect1.right >= Rect2.left && Rect1.bottom >= Rect2.top)

当たり判定の処理は頻繁に使うので、関数化しておくと便利である。

//-----------------------------------------------------------------------------
// 関数名 : HitCheck()
// 機能概要: あたり判定関数
//-----------------------------------------------------------------------------
static bool HitCheck(RECT Rect1, RECT Rect2)
{
    if (Rect1.left <= Rect2.right &&
        Rect1.top <= Rect2.bottom &&
        Rect1.right >= Rect2.left &&
        Rect1.bottom >= Rect2.top)
        return true;
    else
        return false;

}

[GameOverの出し方]

STATUS構造体にはlifeという変数があらかじめ用意されているので、それを使う。

  1. GameInit関数で、lifeに1をセット
  2. ブロックに当たったらlifeを0、自キャラコマ送り制御用変数をクリア
  3. lifeが0なら当たり判定を行わない
  4. lifeが0ならGameOver文字を出す。0でなければ自キャラを描画
  5. GameOver文字を一定時間以上(例えば5秒)出したら、g_FrameNoにSTART_INITをセット

実行結果サンプル

設問f

 一定時間ごとに、障害物の数を1つづつ増やす。最大10個とする。

[考え方の例]

  1. ブロックを一定時間ごとに増やすため、ブロック用の時間制御変数を用意する。
  2. ブロックの各種情報を持つ構造体変数を配列にする(要素数は10)。
  3. ブロック情報の初期化で、lifeに0をセットする。
  4. 一定時間がたったらブロックのlifeを調べ、 0のものが見つかったら(1つだけ)1に変える。
  5. ブロックの描画や当たり判定は、lifeが1のものだけ行う。

[POINT]

ブロックの要素数を#define BLOCK_MAX 10と定義しておくと、ブロックを増やしたいときでも、このdefine文の数値を変えるだけで対応できる。

実行結果サンプル

設問g

 ゲームスタートからの経過時間をスコアとして表示する。0.1秒で10点とし、0.1秒毎にスコアを更新する。また、ゲームオーバー時、スコアを画面中央に表示する。

実行結果サンプル

設問h

 スペースキーを押すと、一発だけ弾を打てるようにする。ブロックは弾に当たると破壊され、得点が100加算される。
(ブロックの破壊アニメーションは自由。あれば平常点UP)

実行結果サンプル

設問i

 当たり判定を甘くする。現在の当たり判定ではキャラクタ画像の大きさをそのまま利用しているため、キャラクタが描かれていないところでも当たってしまう。

例えば左のような32×32の画像の場合、中に書かれている画像よりも大きい範囲で当たり判定が行われてしまう。
そこで、当たり判定用に内側の矩形を用意する。

 ステータス構造体には当たり判定用のRect構造体としてhitRectが用意されているので、それを利用する。

[追加・修正するコードの例]

  1. GameInit関数内
    SetRect(&myChar.hitRect, 8, 0, 24, 32); // 当たり判定矩形
  2. 当たり判定を行っている関数内
    /* 自キャラの位置情報設定 */
    Rect1.left = myChar.x + myChar.hitRect.left;
    Rect1.right = Rect1.left + (myChar.hitRect.right - myChar.hitRect.left);
    Rect1.top = myChar.y + myChar.hitRect.top;
    Rect1.bottom = Rect1.top + (myChar.hitRect.bottom - myChar.hitRect.top);

実行結果サンプル

設問j(自由問題)

 自キャラ死亡時にアニメーションを行う。3パターンのアニメーション表示後、GameOverが出るように変更する。

実行結果サンプル


[ TOP ]