クラスまみれのゲームプログラミング入門

24:スプライトのアニメーション

 歩く、ジャンプ、しゃがむ、攻撃する……ゲームにおいてキャラクターは様々なアクションを起こすことがあります。パラパラ漫画の要領で複数の画像を順繰りで描画していけばいいのですが、1枚(以下、フレーム)のイラストに1つのファイルを使っていては、フォルダの中があっという間に画像ファイルでいっぱいになってしまいます。そこで今回は、1枚の画像ファイル(テクスチャ)でアニメーションを実現する方法をご紹介します。

 Direct3Dのスプライト描画では、特定の範囲だけを転送することができます。この転送処理をクラス化することで、簡単な数値の指定だけでアニメーションができるようにしましょう。

 一枚の画像(テクスチャ)の中に、アニメーションパターンを描き込み、プログラムで指定したフレームに対応した範囲をテクスチャから抜き出して、ウィンドウに転送。これが今回紹介するアニメーションのメカニズムです。アニメーションパターンは左から右へ、ここでは上から下へ進むにつれてフレームを進めるようにします。

 ちなみに、今回のアニメパターン作成には3Dモデリングツール「六角大王Super 5.5」を利用しています。六角大王には再利用可能サンプルが豊富に用意されているので、このソフトなら絵心のない人でも、様々な角度からとらえた戦闘機や、そのアニメーションなどを手軽に作成することができます。Direct3D専用のポリゴンファイル(x形式)の出力にも対応しているので、3Dプログラミングにチャレンジしたい人も必携です。

 指定範囲を転送するには、その範囲がどれだけあるかをプログラムが知っている必要があります。さすがに自動で取得させるのは無理なので、範囲を指定するための関数を用意することにします。

 指定したフレームに対応する取得範囲を計算する関数を作ります。フレームとフレーム画像サイズに対応した、矩形範囲の取得は以下のようにして行えますが、ここでひとつ注意しておきたいことがあります。Direct3Dのテクスチャは64x64、128x128、256x256……、つまり、2nサイズ四方にすることが定められています。それ以外のサイズの画像をテクスチャとして読み込むと、自動的にサイズの変更が行われるため、以下の式では計算が合わなくなってきますので、複数のイメージをひとつのテクスチャに格納するときは、画像のサイズを2nサイズに統一しておくよう心がけましょう。

 取得した転送範囲はID3DXSprite::Draw()の第2引数にて指定します。今回はSetCenterPosition()をはじめとして、CSpriteクラスに大幅な変更が加えられていますので、CSpriteクラスの全ソースコードを掲載します。

sprite.h
#pragma once

#include "Texture.h"

#define CP_MIDDLECENTER 0
#define CP_LEFT 1
#define CP_CENTER 2
#define CP_RIGHT 4
#define CP_TOP 8
#define CP_MIDDLE 16
#define CP_BOTTOM 32

class DECLSPEC CSprite : public CGameObject
{
private:
	CTexture *texture;

	BYTE cpos;	// 基点の位置
	float orig_x, orig_y;	// 原点の位置
	int texwidth;	// テクスチャの幅(縦横同一サイズが前提)

	int swidth, sheight;	// スプライトのサイズ
	RECT drawrect;			// 転送範囲

	void Reset();

public:
	CSprite();
	CSprite(CTexture *source);

	void SetTexture(CTexture *source);

	void SetCenterPosition(BYTE pos);
	BYTE GetCenterPosition();

	// アニメーションのフレーム当たりのキャラクターサイズの設定・取得
	void GetSpriteSize(int *width, int *height);
	void SetSpriteSize(int width, int height);

	// フレーム単位ではなく特定の範囲のグラフィックを転送したときには
	// SetSpriteRect()で直接範囲を指定できるようにする
	void SetSpriteRect(int left, int top, int right, int bottom);

	// アニメーションのフレームより描画範囲を計算する
	void SetFrame(int frame);

	void Draw(float x, float y, int alpha = 255);
	void Draw(float x, float y, float r, int alpha = 255);
	void Draw(float x, float y, float ex, float ey, int alpha = 255);
	void Draw(float x, float y, float ex, float ey, float r, int alpha = 255);
};

sprite.cpp
#include "Sprite.h"

CSprite::CSprite()
{
	Reset();
}

CSprite::CSprite(CTexture *source)
{
	Reset();
	SetTexture(source);
}

void CSprite::Reset()
{
	texture = NULL;
	cpos = CP_MIDDLECENTER;
	orig_x = orig_y = 0.0f;
	texwidth = 0;
	swidth = sheight = 0;
	SetRect(&drawrect, 0, 0, 0, 0);
}

void CSprite::SetTexture(CTexture *source)
{
	if(source == NULL) return;
	texture = source;

	int h;
	texture->GetTextureSize(&texwidth, &h);
	SetSpriteSize(texwidth, h);
}

void CSprite::SetSpriteSize(int width, int height)
{
	// スプライトのサイズを変更
	swidth = width;
	sheight = height;

	// 原点位置の更新
	SetCenterPosition(cpos);

	// フレームのリセット
	SetFrame(0);
}

void CSprite::GetSpriteSize(int *width, int *height)
{
	*width = swidth;
	*height = sheight;
}

void CSprite::SetSpriteRect(int left, int top, int right, int bottom)
{
	swidth = right - left;
	sheight = bottom - top;

	drawrect.left = left;
	drawrect.right = right;
	drawrect.top = top;
	drawrect.bottom = bottom;

	SetCenterPosition(cpos);
}

void CSprite::SetCenterPosition(BYTE pos)
{
	// 原点の位置を計算
	cpos = pos;

	if(pos & CP_LEFT){
		orig_x = 0.0f;
	}else if(pos & CP_RIGHT){
		orig_x = (float)swidth;
	}else{
		orig_x = (float)swidth / 2.0f;
	}

	if(pos & CP_TOP){
		orig_y = 0.0f;
	}else if(pos & CP_BOTTOM){
		orig_y = (float)sheight;
	}else{
		orig_y = (float)sheight / 2.0f;
	}
}

BYTE CSprite::GetCenterPosition()
{
	return cpos;
}

void CSprite::SetFrame(int frame)
{
	// 指定したフレーム値より転送範囲を計算
	int d = texwidth / swidth;
	if(d == 0) return;

	int xpos = frame % d;
	int ypos = frame / d;

	drawrect.left = xpos * swidth;
	drawrect.right = drawrect.left + swidth;
	drawrect.top = ypos * sheight;
	drawrect.bottom = drawrect.top + sheight;
}

void CSprite::Draw(float x, float y, int alpha)
{
	Draw(x, y, 1.0f, 1.0f, 0.0f, alpha);
}

void CSprite::Draw(float x, float y, float ex, float ey, int alpha)
{
	Draw(x, y, ex, ey, 0.0f, alpha);
}

void CSprite::Draw(float x, float y, float r, int alpha)
{
	Draw(x, y, 1.0f, 1.0f, r, alpha);
}

void CSprite::Draw(float x, float y, float ex, float ey, float r, int alpha)
{
	if(texture == NULL){
		DXTRACE_MSG(_T("テクスチャが読み込まれていません"));
		return;
	}

	D3DXMATRIX mtrx1, mtrx2;

	// 原点を重ね合わせる平行移動
	D3DXMatrixTranslation(&mtrx1, -orig_x, -orig_y, 0.0f);

	// 拡大行列と合成
	if(ex != 1.0f || ey != 1.0f){
		D3DXMatrixScaling(&mtrx2, ex, ey, 1.0f);
		D3DXMatrixMultiply(&mtrx1, &mtrx1, &mtrx2);
	}

	// 回転行列との合成
	if(r != 0.0f){
		D3DXMatrixRotationZ(&mtrx2, r);
		D3DXMatrixMultiply(&mtrx1, &mtrx1, &mtrx2);
	}

	// 指定の場所へ移動する行列との合成
	D3DXMatrixTranslation(&mtrx2, x, y, 0.0f);
	D3DXMatrixMultiply(&mtrx1, &mtrx1, &mtrx2);


	pSprite->Begin(NULL);

	pSprite->SetTransform(&mtrx1);
	pSprite->Draw(texture->GetTexture(), &drawrect, NULL, NULL, 0x00FFFFFF | ((BYTE)alpha << 24));

	pSprite->End();
}

 サンプルプログラムと画像を用意してみました。コンパイルして実行すると、画面中央で人が走るアニメーションが表示されます。

animtest.cpp
class CAnimationTest : public CGameObject
{
private:
	CTexture tex;
	CSprite sprite;
protected:
	virtual void Init();
	virtual void Exec();
};

void CAnimationTest::Init()
{
	tex.Load(_T("anime.png"));
	sprite.SetTexture(&tex);
	sprite.SetSpriteSize(128, 128);
}

void CAnimation::Exec()
{
	static int frame = 0, count = 0;
	if(++count >= 12){
		count = 0;
		if(++frame >= 8) frame = 0;
		sprite.SetFrame(frame);
	}

	sprite.Draw(320.0f, 240.0f);
}


anime.png