一枚絵を背景にしてスクロール

背景用に一枚のビットマップファイルを用意し、スクロールさせるプログラムを紹介する。
一枚絵がスクロールしているように見せるためには、横スクロールなら右側と左側、縦スクロールなら上側と下側が違和感なくくっつくような画像を用意する必要がある。

NKC_BackImage.cppの組み込み

一枚絵を自由にスクロールさせるプログラムをソースファイルで用意したので、それを組み込んで利用する方法を紹介する。

1.ソースファイルの準備

NKC_BackImage.lzhをダウンロードして解凍すると「NKC_BackImage.cpp」「NKC_BackImage.h」が現れる。それをプロジェクトのフォルダに配置し、VCを開いて、プロジェクトに追加する作業を行う。

2.背景画像ファイルの準備

背景として一枚絵の画像を描画するので、背景の画像ファイルを用意し、プロジェクトのフォルダに配置する。サンプルとしてscroll.pngを用意したので、これを利用する。
なお、このプログラムは背景画像の大きさが640×480であることを前提に作成してあるので、注意すること。

3.NKC_BackImage.cppの修正

用意する画像ファイルによってファイル名が変わるため、NKC_BackImage.cppを修正する。修正する場所は次のとおり。

//=============================================================================
//  背景に一枚絵を利用するスクリプト
//  Copyright NKC Game Staff(←自分の名前) 
//-----------------------------------------------------------------------------
#include "NKC_Common.h"

// マクロ

// 構造体

// グローバル変数
/* 他のソースからも利用されるもの */

/* 自ソースでのみ利用するもの */
static LPDIRECT3DTEXTURE8 gl_TXScroll = NULL;   // スクロール背景用テクスチャ
static TLVERTX scroll_v[4];                     // 頂点情報配列
static float scroll_tu, scroll_tv;              // スクロール幅
static char* FileName1 = "scroll.png";          // 背景画像ファイル名

// プロトタイプ宣言
/* 自ソースでのみ使用するもの */
・
・
・

4.NKC_Common.hの修正

NKC_BackImage.cppを利用できるよう、NKC_Common.hからNKC_BackImage.hをインクルードする。

//-----------------------------------------------------------------------------
// 共通ヘッダ・ファイル
//  Copyright NKC Game Staff(←自分の名前) 
//-----------------------------------------------------------------------------
・
・
・
// 自作ソース・ファイルの組み込み
#include "NKC_DGraphics.h"
#include "NKC_Public.h"
#include "NKC_BackImage.h"
#include "Start.h"
#include "Game.h"
#include "Enemy.h"

5.確認

ここまでの修正を行ったら一度ビルドして、エラーが出ないことを確認する。

前準備

ソースファイルの組み込みが終われば、一枚絵の背景を表示し、自由にスクロールさせることができるようになる。スクロール処理を追加する前に、まず、一枚絵が背景に描画されるようにプログラムする。その後、様々なスクロール処理を考える。
Game.cppを開き、次のように修正する。

//=============================================================================
//  ゲーム処理関係の自作関数群
//  Copyright NKC Game Staff(←自分の名前) 
//-----------------------------------------------------------------------------
#include "NKC_Common.h"

// マクロの定義

// グローバル変数
/* 自ソースでのみ利用するもの */
//---- 背景
static float BackMoveX, BackMoveY;  // 背景移動量
//---- 自機
static STATUS MyChara;              // 自キャラステータス情報

// プロトタイプ宣言
/* 自ソース(Game.cpp)内でのみ利用するもの */
static void BackDraw(void);         // 背景描画
static void MyCharaDraw(void);      // 自機描画
static void MyCharaMove(void);      // 自機移動

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: ゲーム画面初期化処理
//-----------------------------------------------------------------------------
void GameInit(void)
{

    //--------------------------------------------------- 各変数の初期化
    // ゲーム画面で共通して使用するテクスチャの作成
    CreateGameTexture();
    // ポリゴンの初期化
    /* 背景 */
    InitBackImage();
    BackMoveX = 0.0f;
    BackMoveY = 0.0f;
    /* 自キャラ */
    InitVertex(MyChara.Vertex, 273.0f, 193.0f, 367.0f, 287.0f, 255);// 表示位置
    SetRect(&MyChara.HitRect, 10, 10, 10, 10);                   // 当たり判定矩形
    MyChara.MoveX = 2.0f;                                        // 移動量(X方向)
    MyChara.MoveY = 2.0f;                                        // 移動量(Y方向)

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

}

//-----------------------------------------------------------------------------
// 関数名 : GameFrame()
// 機能概要: ゲーム画面処理
//-----------------------------------------------------------------------------
void GameFrame(void)
{
    //--------------------------------------------------- 前処理

    //--------------------------------------------------- 描画
    DrawBackImage(BackMoveX, BackMoveY); // 背景
    MyCharaDraw(); // 自機

    //--------------------------------------------------- 移動処理
    MyCharaMove(); // 自機

	//--------------------------------------------------- 当たり判定


#ifdef DEBUG
    //--------------------------------------------------- デバッグ用
    // F10を押したらスタート画面に戻る
    if ( gl_KeyTbl[VK_F10] & 0x80 ) {
        ReleaseBackImage();
        ReleaseGameTexture();
        g_FrameNo = START_INIT;
    }
#endif

}
・
・
・

確認

上記修正を行ってビルドを行い、エラーが出ないことを確認する。また、プログラムを実行し、NKC_BackImage.cppで指定した背景画像がゲーム処理で表示されることを確認する。

自動的にスクロールさせる(縦/横スクロール)

ここでは、シューティングゲームのように、ゲームの進行に関係なく自動的に背景をスクロールさせる方法を紹介する。

現在のプログラムの解説

ゲームループのたびに背景画像を描画する処理は、GameFrame()関数内でDrawBackImage()関数を実行することにより行われている。

//--------------------------------------------------- 描画
DrawBackImage(BackMoveX, BackMoveY); // 背景
MyCharaDraw(); // 自機

DrawBackImage()関数はNKC_BackImage.cpp内に作成されている関数で、2つの引数を持つ。1つ目の引数はX方向、2つ目の引数はY方向の移動量を示している。
前準備で作成したプログラムでは、初期化処理(GameInit()関数内)で、移動量を次のように設定した。

/* 背景 */
InitBackImage();
BackMoveX = 0.0f;
BackMoveY = 0.0f;

この場合、X方向の移動量は0.0、Y方向の移動量も0.0であるため、背景画像は一切のスクロールを行わないことになる。よって、スクロールを行いたい場合、ここの値を変化させればよいことが分かる。

自動的にスクロールするように設定するには?

例えばシューティングゲームのように自動的に背景をスクロールさせる場合、次のように指定する。

  1. 縦スクロール(右から左へ流れる)の場合
    /* 背景 */
    InitBackImage();
    BackMoveX = 0.002f;
    BackMoveY = 0.0f;
    
  2. 横スクロール(上から下へ流れる)の場合
    /* 背景 */
    InitBackImage();
    BackMoveX = 0.0f;
    BackMoveY = -0.002f;
    

横スクロールの場合はBackMoveX、縦スクロールの場合はBackMoveYに対して、スクロールする幅を指定する。指定できる値は-1.0から1.0の間で、例えば0.1を指定すると、10回のループで背景が一周する。よって、値が小さければゆっくりと、値が大きければ早くスクロールすることが分かる。
また、BackMoveX、BackMoveYの値をともに指定すると、斜めにスクロールするようになる。

《確認》

BackMoveX、BackMoveYの初期値をいろいろ変更し、どのようにスクロールが行われるか確かめよう。

キャラクタの動きにあわせてスクロールさせる

背景画像描画処理(DrawBackImage()関数)を実行する際に、移動量を指定することにより背景がスクロールする。であれば、キャラクタの移動、つまりカーソルキーの入力状態によって背景の移動量を調整すれば、キャラクタを中心にして背景が移動するように見せることが可能であることが想像できる。
どのようにでもプログラムできるが、例えば次のようにプログラムすれば、キャラクタを中心にして背景をスクロールできる。

//=============================================================================
//  ゲーム処理関係の自作関数群
//  Copyright NKC Game Staff(←自分の名前) 
//-----------------------------------------------------------------------------
#include "e;NKC_Common.h"e;

// マクロの定義

// グローバル変数
/* 自ソースでのみ利用するもの */
//---- 背景
static float BackMoveX, BackMoveY;  // 背景移動量
static float ScrollSpeed;           // スクロールスピード
//---- 自機
static STATUS MyChara;              // 自キャラステータス情報
//---- 敵キャラ
static ENEMYSTATUS Enemy;           // 敵キャラステータス情報

// プロトタイプ宣言
/* 自ソース(Game.cpp)内でのみ利用するもの */
static void BackDraw(void);         // 背景描画
static void MyCharaDraw(void);      // 自機描画
static void MyCharaMove(void);      // 自機移動
static void EnemyMove(void);        // 敵キャラ移動

//-----------------------------------------------------------------------------
// 関数名 : GameInit()
// 機能概要: ゲーム画面初期化処理
//-----------------------------------------------------------------------------
void GameInit(void)
{

    //--------------------------------------------------- 各変数の初期化
    // ゲーム画面で共通して使用するテクスチャの作成
    CreateGameTexture();
    // ポリゴンの初期化
    /* 背景 */
    InitBackImage();        // 背景用テクスチャの作成
    BackMoveX = 0.0f;       // スクロール幅(X座標)
    BackMoveY = 0.0f;       // スクロール幅(Y座標)
    ScrollSpeed = 0.005f;   // 移動量
    /* 自キャラ */
    InitVertex(MyChara.Vertex, 273.0f, 193.0f, 367.0f, 287.0f, 255);// 表示位置
    SetRect(&MyChara.HitRect, 10, 10, 10, 10);                   // 当たり判定矩形
    MyChara.MoveX = 2.0f;                                        // 移動量(X方向)
    MyChara.MoveY = 2.0f;                                        // 移動量(Y方向)

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

}
・
・
・
//-----------------------------------------------------------------------------
// 関数名 : MyCharaMove()
// 機能概要: 自機移動
//-----------------------------------------------------------------------------
static void MyCharaMove(void)
{
    // 初期化
    BackMoveX = BackMoveY = 0.0f;
    // 背景スクロール処理
    if ( gl_KeyTbl[VK_LEFT] & 0x80 )  BackMoveX = -ScrollSpeed;
    if ( gl_KeyTbl[VK_UP] & 0x80 )    BackMoveY = -ScrollSpeed;
    if ( gl_KeyTbl[VK_RIGHT] & 0x80 ) BackMoveX = ScrollSpeed;
    if ( gl_KeyTbl[VK_DOWN] & 0x80 )  BackMoveY = ScrollSpeed;

}
・
・
・

《確認》

上記修正を行い、キャラクタの動きにあわせて背景がスクロールすることを確かめよう。

プログラム解説

ここでは、NKC_BackImage.cppに作成した関数で、どのようにスクロールを実現しているかを簡単に解説する。

テクスチャ座標について(復習)

このプログラムは、テクスチャ座標(tu、tv)を変化させることによりスクロールを実現している。テクスチャ座標についての基本的な考え方は、04−(8)1つの画像ファイルに複数の画像を載せるには?で解説済みである。

ここでは、1つの画像ファイルに4つ(2×2)のキャラクタ画像を用意している。例えば、左上のキャラクタを表示したい場合、各頂点のtu、tvの値は次のようになる。

このように、tu、tvを0から1の間で変化させ、画像全体のうちどの位置をマッピングするかを指定することにより、画像の一部分を描画できる。
通常、テクスチャ座標は0〜1の間で利用するが、実はこの範囲外の値を使うことで特別な処理を行うことができる

テクスチャ・マッピングの効果

頂点に設定するテクスチャ座標は、先ほどの例にもあるとおり、通常0〜1の範囲で設定するが、範囲外の値を指定すると、同じテクスチャのパターンを繰り返して使ったり、境界の色を引き伸ばしたりといった効果を出すことができる

例えば1つのキャラクタ画像がある場合、各頂点のtu、tvの値を次のように設定する。

このポリゴンにテクスチャを貼り付けると、次のようになる。

この場合、真ん中((0,0)〜(1,1))の左右に同じテクスチャがマッピングされたことがわかる。

《応用》

この技術を使えば、例えばWindowsの壁紙のように、同じ画像を背景に敷き詰めることが可能になる。例えば1つの小さな背景画像を横10枚、縦8枚に敷き詰める場合、背景画像用のポリゴンのtu、tvを次のように指定すればよい。

マップチップを使わなくても、この程度なら簡単にできることがわかるだろうか?

背景スクロールのプログラムについて

NKC_BackImage.cppで行っている背景描画処理の、スクロールの部分は次のようになっている。

//-----------------------------------------------------------------------------
// 関数名 : InitBackImage()
// 機能概要: 初期化処理
//-----------------------------------------------------------------------------
void InitBackImage(void)
{
    HRESULT hr;
    char buff[80];

    // 背景用テクスチャの生成
    hr = D3DXCreateTextureFromFile(gl_lpD3ddev, FileName1, &gl_TXScroll);
    if ( FAILED(hr) ) {
        wsprintf(buff, "%s をテクスチャとして読み込めませんでした", FileName1);
        MessageBox(hWnd, buff, "ERROR", MB_OK);
        return;
    }

    // 初期化
    InitVertex(scroll_v, (float)gl_rcScreen.left, (float)gl_rcScreen.top, (float)gl_rcScreen.right, (float)gl_rcScreen.bottom, 255);
    scroll_tu = 0.0f;
    scroll_tv = 0.0f;

}

//-----------------------------------------------------------------------------
// 関数名 : DrawBackImage()
// 機能概要: 背景描画&スクロール処理
//-----------------------------------------------------------------------------
void DrawBackImage(float move_x, float move_y)
{

    /* 描画処理 */
    gl_lpD3ddev->SetTexture(0, gl_TXScroll);
    gl_lpD3ddev->DrawPrimitiveUP(D3DPT_TRIANGLEFAN, 2, scroll_v, sizeof(TLVERTX));

    /* スクロール処理 */
    // 横
    scroll_tu += move_x;
    if ( scroll_tu <= -1.0f ) scroll_tu += 1.0f;
    else if ( 1.0f <= scroll_tu ) scroll_tu -= 1.0f;
    scroll_v[0].tu = scroll_tu;
    scroll_v[1].tu = 1.0f + scroll_tu;
    scroll_v[2].tu = 1.0f + scroll_tu;
    scroll_v[3].tu = scroll_tu;
    // 縦
    scroll_tv += move_y;
    if ( scroll_tv <= -1.0f ) scroll_tv += 1.0f;
    else if ( 1.0f <= scroll_tv ) scroll_tv -= 1.0f;
    scroll_v[0].tv = scroll_tv;
    scroll_v[1].tv = scroll_tv;
    scroll_v[2].tv = 1.0f + scroll_tv;
    scroll_v[3].tv = 1.0f + scroll_tv;

}

ポリゴンの初期化はInitVertex()関数で行っているが、これはNKC_Public.cppに用意した関数で、頂点情報構造体のX,Y座標をセットするが、tu,tvの値は0〜1にセットされるようにプログラムしてある。

スクロールのたびにスクロール幅(scroll_tu,scroll_tv)を増減し、tu,tvに反映させる。これにより、マッピングする範囲を0〜1を超えて指定している。

このようにプログラムすれば、ポリゴンにマッピングする画像を自由に変化させられるため、背景用ポリゴンの頂点座標を固定((0,0)〜(640,480))したままでスクロールを実現できる

《注意!!》

スクロール幅(scroll_tu, scroll_tv)が1.0(または-1.0)を超えた場合、一画面分スクロールしたことになるため、スクロール幅の値をクリアしている。

// 横
scroll_tu += move_x;
if ( scroll_tu <= -1.0f ) scroll_tu += 1.0f;
else if ( 1.0f <= scroll_tu ) scroll_tu -= 1.0f;
scroll_v[0].tu = scroll_tu;
scroll_v[1].tu = 1.0f + scroll_tu;
scroll_v[2].tu = 1.0f + scroll_tu;
scroll_v[3].tu = scroll_tu;
// 縦
scroll_tv += move_y;
if ( scroll_tv <= -1.0f ) scroll_tv += 1.0f;
else if ( 1.0f <= scroll_tv ) scroll_tv -= 1.0f;
scroll_v[0].tv = scroll_tv;
scroll_v[1].tv = scroll_tv;
scroll_v[2].tv = 1.0f + scroll_tv;
scroll_v[3].tv = 1.0f + scroll_tv;

ここで、スクロール幅を0.0にしていないことが重要である。何故なら、tu,tvはfloat型であるため、そこに加算減算を行うためのスクロール幅であるscroll_tu,scroll_tvも当然float型で宣言しているが、float型は浮動小数点形式であるため、演算に誤差が出てしまうからである。
例えばC言語で次のようなプログラムを作成した場合、思うような結果が得られない。

#include <stdio.h>
void main()
{
    float num = 0.0f;
    while ( num <= 1.0f )
    {
        num += 0.005f;
        printf("%f\n", num);
    }
}

本来ならfloat型変数numの値は0.005ずつ増加し、ぴったり1.0でループが終了するはずだが、どこかで微妙に値がずれてしまうことが分かる。つまり、ぴったり1.0になる保障は無いのである。
よって、scroll_tu,scroll_tvは0.0に戻す(scroll_tu = 0.0f;では駄目)のではなく、ぴったり一画面分元に戻してやる(scroll_tu -= 1.0f)必要がある。

問題点?(想像)

このプログラムは、ポリゴンのtu、tvの値からマッピングする領域を求め、ポリゴンに貼り付けている。ドット単位で移動させているわけではないため、スクロール幅によっては波を打ったように見えてしまう。
また、マッピングのために内部的に演算が発生しているはずなので、ドット単位でスクロールさせる方法と比べて、処理が重くなるような気がする・・・

参考

tu、tvの値に範囲外(0〜1以外)の値を設定した際、表示の効果を決めるのがアドレス・モードであり、デフォルトでは同じ画像が繰り返されるようになっている。

テクスチャのアドレス・モードを設定するには、IDirect3DDevice8::SetTextureStageStateメソッドを使用する。使い方は次のとおり。

  1. テクスチャ座標が範囲外のとき、同じ画像を繰り返し使用して描画(デフォルト)
    gl_D3ddev->SetTextureState(0, D3DTSS_ADDRESSU, D3DTADDRESS_WRAP);
    
  2. テクスチャ座標が範囲外のとき、同じ画像を隣の画像と反転した画像を使用して描画
    gl_D3ddev->SetTextureState(0, D3DTSS_ADDRESSU, D3DTADDRESS_MIRROR);
    
  3. テクスチャ座標が範囲外のとき、範囲外になる部分をテクスチャ画像の端の色がそのまま引き伸ばされて描画
    gl_D3ddev->SetTextureState(0, D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP);
    
  4. テクスチャ座標が範囲外のとき、範囲外になる部分は境界色として定義された色で塗りつぶして描画
    gl_D3ddev->SetTextureState(0, D3DTSS_ADDRESSU, D3DTADDRESS_BORDER);
    

この設定は一度行えばずーっと有効であるため、描画モードの初期化処理(NKC_DGraphics.cppのInitRender()関数内)などで設定するといいだろう。

また、SetTextureStateメソッドの第2引数としてD3DTSS_ADDRESSUを指定しているが、これは、アドレス・モードの設定をtuに反映させるという意味である。この設定を指定しない場合、tu、tv共にD3DTADDRESS_WRAPが指定されたことになる(デフォルト)。tu、tvそれぞれに別のアドレス・モードを設定したい場合は、D3DTSS_ADDRESSUまたはD3DTSS_ADDRESSVをそれぞれ指定し、アドレス・モードを設定すればよい。


BACK(星を降らせて擬似スクロール) NEXT(マップチップを背景にしてスクロール)