はじめに
前回はビットマップの重ね合わせや、一部を切り出すスプライトの描画について取り扱いました。
しかし、たんに描画するだけでは物足りません。スプライトは、動いてこそ、動かせてこそ、です。
今回はスプライトの動かし方を解説していきます。
スプライトのアニメーション
動きのあるスプライトを描画するため、今回のサンプルでは、新たにスプライト用の情報を定義しました。
CExample.h
// スプライト構造体
typedef struct tag_sprite
{
int nPosX; // 表示座標(X)
int nPosY; // 表示座標(Y)
int nState; // 状態(0:立ち, 1:歩き)
int nCurrentCell; // 表示セル
DWORD dwDelay; // 描画更新カウント
DWORD dwLastTickCount; // 経過時間カウント
} SPRITE;
nPosX
とnPosY
はコメントからも分かるように、スプライトの描画位置として使用します。
前回でも、スプライトがウィンドウ中央に描画されるよう、位置を指定しました。
そして、残りのメンバ変数はアニメーションのために使用されます。
まずは、構造体の初期化処理を見ていきましょう。
CExample.cpp - CMyExample::InitExample()
// スプライトデータ初期化
sprite.nPosX = (pDibOffScreen->GetCDibWidth() - 64) / 2;
sprite.nPosY = (pDibOffScreen->GetCDibHeight() - 64) / 2;
sprite.nState = SPRITE_STATE_STAY;
sprite.nCurrentCell = 0;
sprite.dwDelay = SPRITE_DELAY;
sprite.dwLastTickCount = GetTickCount();
nPosX
とnPosY
は、今回もウィンドウ中央に描画されるよう初期値をセットしています。
nState
はヘッダファイルにて定義された状態値をセットしています。
スプライトの状態として、動きのない「立ち」状態と、歩行アニメーションする「歩き」状態を定義しています。
CExample.h
// スプライトの状態
enum
{
SPRITE_STATE_STAY, // 立ち
SPRITE_STATE_WALK, // 歩き
SPRITE_STATE_MAX
};
nCurrentCell
は表示するアニメーションセル(※)の番号(0オリジン)を指します。
※ここでのセルとは、スプライト画像BMPに並んで格納された1コマ単位の画像を意味します。
サンプルのスプライトBMPは、下記のようなイメージで8コマ分のセルが横並びで構成されています。
Cell [0] | Cell [1] | Cell [2] | Cell [3] | Cell [4] | Cell [5] | Cell [6] | Cell [7] |
nState
が「歩き」状態のときは8つのアニメーションセルから構成され、「立ち」状態のときは動きがないため1つのセルしか表示しません。
なお、状態ごとのセル数もヘッダファイルにて定義しています。
CExample.h
// スプライトのセル
#define SPRITE_DELAY 125 // スプライトのセル更新周期(ミリ秒)
#define SPRITE_STAY_CELL_LEN 1 // スプライト立ち状態のセル数
#define SPRITE_WALK_CELL_LEN 8 // スプライト歩き状態のセル数
dwDelay
はアニメーションの速度です。定義値の125
ミリ秒が経過するたびにnCurrentCell
がインクリメントされ、次のセルが表示されるようになります。
この値を大きくすると、ゆったりとしたアニメーションになります。
dwLastTickCount
はアニメーションの経過時間を測定するための変数です。最後にnCurrentCell
を更新したタイミングを保持し、そこを起点にdwDelay
の閾値経過を判定します。
CExample.cpp - CMyExample::DrawSprite()
// スプライトの更新周期
if(dwTickCount - sprite.dwLastTickCount >= sprite.dwDelay)
{
// 状態に応じて表示セルを更新
switch(sprite.nState)
{
case SPRITE_STATE_STAY:
sprite.nCurrentCell = (sprite.nCurrentCell + 1) % SPRITE_STAY_CELL_LEN;
break;
case SPRITE_STATE_WALK:
sprite.nCurrentCell = (sprite.nCurrentCell + 1) % SPRITE_WALK_CELL_LEN;
break;
}
sprite.dwLastTickCount = dwTickCount;
}
時間経過の測定方法
サンプルでは時間経過を測定するためにGetTickCount関数を使用しています。
この関数は、システム起動からの経過時間をミリ秒値で返します。
この関数には、使用するうえでいくつかの注意点があります。
GetTickCount()
まず、GetTickCountはDWORD型ですので、戻り値の最大は4,294,967,296
となります。
そして、一日は86,400,000
ミリ秒ですので、50日を経過する前にオーバーフローし、0からのカウントに戻ります。
Windowsの電源を落とさず、入れっぱなしの場合、このへんを考慮した作りにすべきでしょう。
もしくは、ULONGLONG型のGetTickCount64関数(型以外はGetTickCount()と等価)を使用する方法も考えられます。
次の注意点は、タイマーの精度です。GetTickCountは10~16ミリ秒の範囲で値が取得できます。
平たく言うと、大雑把に経過時間を取得しています。
「リアルタイム性を重視したアプリを作りたい」とか「フレームレート(FPS)が高いゲームを作りたい」とかいう人には不向きな関数かもしれません。
timeGetTime()
より精度の高いタイマーを使用したい方にはtimeGetTime関数があります。
こちらもWindows起動からの経過時間をミリ秒値で返す関数です。
DWORD型ですので49.7日経過でオーバーフローが発生する点は、GetTickCount関数と同様です。
異なる点は、タイマーとしての精度の高さです。
timeGetTime関数は正確に時間を取得しようとしますが、そのため、システムやデバイスへの負荷が考えられます。
他にも経過時間を測定できる関数はあります。自身の作りたいアプリに応じて、より適切なタイマーを調べてみるのもいいかもしれません。
サンプルではGetTickCount関数で充分に事足りると判断しました。
フレームの更新
スプライトの情報を持つ構造体を用意し、時間経過でその構造体の情報を変えていく準備ができました。
ここで第1回から用意していながら、使用されなかった部分に手を入れます。
example001_005.cpp - WinMain()
// メッセージループ
MSG msg;
while(TRUE)
{
if(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
{
if(!GetMessage(&msg, NULL, 0, 0))
{
return msg.wParam;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// ここにアプリ固有の処理を記述
myex.UpdateFrame();
}
}
この部分は、これまでOSからのメッセージを待つだけのループでした。
OSとの対話のみで成り立つアプリであれば、それでよいでしょう。
しかし、今回のサンプルは異なります。メッセージがない間は、経過時間を判定し、スプライトをアニメーションさせる処理を継続させたいのです。
CExample.cpp - CMyExample::UpdateFrame()
// フレームの更新
void CMyExample::UpdateFrame()
{
DWORD dwTickCount = GetTickCount();
DrawBackground();
DrawSprite(dwTickCount);
InvalidateRect(hwnd, NULL, FALSE);
}
メッセージがないときは、経過時間を取得し、背景とスプライトを再描画し、表示内容を更新させています。
ユーザーからの入力イベント
時間経過によるアニメーションの次は、ユーザー操作による能動的なスプライトの動きについて解説します。
パソコンを使用している人のほとんどが、入力デバイスとしてマウスとキーボードの両方を利用していることでしょう。
ここでは、マウスのボタン押下とキーボードの入力イベントについて取り扱います。
既にここ数回のサンプルでは、アプリ終了トリガーとしてWM_KEYDOWNメッセージでESCキーの押下イベントを拾っています。
また、C++を用いたWindowsプログラミングの基礎(第2回)では、マウスのダブルクリックを検知するメッセージを扱いました。
そのため、第一感でOSからのメッセージを処理する方法が思い浮かぶかもしれません。その方法を試してみましょう。
マウスのボタン入力
ウィンドウ上でマウスの左ボタンが押下されると、ウィンドウ関数はWM_LBUTTONDOWNメッセージを受信します(右クリックならWM_RBUTTONDOWNメッセージ)。
そのほか、マウスボタンが放されたことを通知するイベントもあります。
// マウスボタン押下
case WM_LBUTTONDOWN:
myex.SetSpriteCoord(LOWORD(lParam), HIWORD(lParam));
break;
case WM_RBUTTONDOWN:
myex.ChangeSpriteState();
break;
サンプルでは、左クリックを受信するとウィンドウ関数の第4引数であるlParam
の上位ビット、下位ビットからそれぞれ値を取得しています。
これはクリックされたときのマウスカーソルの座標です。lParam
の下位16ビットにはX座標の値が、上位16ビットにはY座標がセットされています。
この座標値を使用して、スプライトの描画位置の変更を実現しています。
CExample.cpp - CMyExample::SetSpriteCoord()
// スプライトの座標変更
void CMyExample::SetSpriteCoord(int nPosX, int nPosY)
{
sprite.nPosX = nPosX;
sprite.nPosY = nPosY;
}
右クリックを受信した場合は、スプライトのnState
を切り替えます。
右クリックのたびに、アニメーションしない状態と、歩行アニメーションする状態が交互に変わります。
CExample.cpp - CMyExample::ChangeSpriteState()
// スプライトの状態変更
void CMyExample::ChangeSpriteState()
{
sprite.nState = 1 - sprite.nState;
}
キーボード押下
キーボード押下メッセージではテンキーが押されたときのイベント処理を追加しています。
テンキーを押しても反応がない場合はNumLockキーの状態を確認してください。
// キーボード押下
case WM_KEYDOWN:
switch(wParam)
{
case VK_NUMPAD2:
myex.MoveSpriteCoord(0, 8);
break;
case VK_NUMPAD4:
myex.MoveSpriteCoord(-8, 0);
break;
case VK_NUMPAD5:
myex.ChangeSpriteState();
break;
case VK_NUMPAD6:
myex.MoveSpriteCoord(8, 0);
break;
case VK_NUMPAD8:
myex.MoveSpriteCoord(0, -8);
break;
case VK_ESCAPE:
PostQuitMessage(0);
break;
}
break;
テンキー5
はマウスの右クリックと同じ処理を呼びます。
テンキー2
4
6
8
はそれぞれスプライトの描画座標を上下左右に移動させます。
移動の際にウィンドウのクライアント領域からはみ出す場合の考慮も必要でしょう(例えば、衝突判定を行い、画面外へは移動させない等)。
サンプルでは、画面外に出ると反対側から出現するよう、座標値をラップアラウンドさせています。
CExample.cpp - CMyExample::MoveSpriteCoord()
void CMyExample::MoveSpriteCoord(int nMoveX, int nMoveY)
{
sprite.nPosX = (sprite.nPosX + nMoveX + pDibOffScreen->GetCDibWidth()) % pDibOffScreen->GetCDibWidth();
sprite.nPosY = (sprite.nPosY + nMoveY + pDibOffScreen->GetCDibHeight()) % pDibOffScreen->GetCDibHeight();
}
次回予告
さて、今回はOSからのメッセージ処理で入力イベントを処理しました。
サンプルを実行してみると、マウスのクリックやキーボード押下に応じてスプライトの描画が変化します。
しかし、ソースコードを見るだけで気づいた人がいるでしょうし、実際に動かしてみて気づいた人もいるかもしれません。
メッセージ処理では、一度に一つのボタンしか入力イベントを拾えないのです。
これでは、「敵の弾幕を斜め移動で回避しながらショットを撃つ」とか「長押しでゲージを溜めて、ボタンを放したときに強攻撃を発動」といったユーザー操作に対応ができません。
次回は、メッセージ処理を使用しない方法でマウス/キーボード入力を処理する方法について解説したいと思います。