「ゲーム開発」カテゴリーアーカイブ

ゲーム開発13:キャラクタ管理クラス作成

キャラクタクラスCCharaを作ろうと思いましたが、その前にキャラクタ管理クラスCCharaManageを作成します。

キャラクタ管理クラスとは、CCharaをまとめて処理するクラスといった感じでしょうか。キャラクタの移動や描画はひとつひとつ別個に処理させず、この管理クラスに全部やってもらいます。

キャラクタの最大数MAX_CHARA分だけCCharaポインタを持ち、CreateChara()にて空いているCCharaポインタがあったら、そこに任意のキャラクタを生成します。キャラクタの種類kindを引数に持ち、ベタですがswitch~caseで生成キャラを選択します。ちなみに

Chara[i]= new CMyChara(i, x, y);
Chara[i]= new CEnemy01(i, x, y);

としているのは、CMyCharaCEnemy01がCCharaを継承したクラスだから可能にしている芸当です。キャラクタを増やす際、CEnemy02:public CCharaみたいに継承クラスを作って追加するだけで、中身が敵だろうとアイテムだろうと関係なく、汎用的に使いまわすことができます。

あとはMove()で移動、Draw()で描画を全キャラ分ループで回しています。キャラクタの寿命が来たらDeleteChara()で、これまた中身を気にせずに削除する事ができます。

次回は今度こそキャラクタクラスを作ります。

//-----------------------------------------------------------------------------
// 生成
CCharaManage::CCharaManage()
{
 int  i;

 // キャラクタ初期化(念のため)
 for (i=0; i<MAX_CHARA; i++) {
  Chara[i]=NULL;
 }
}

//-----------------------------------------------------------------------------
// 解放
CCharaManage::~CCharaManage()
{
 int  i;

 // キャラクタ解放
 for (i=0; i<MAX_CHARA; i++) {
  DeleteChara(i);
 }
}

//-----------------------------------------------------------------------------
// キャラクタ生成
CChara *CCharaManage::CreateChara(int kind, int x, int y, CChara *parent)
{
 int  i;

 // 空いているキャラクタがあるか検索
 for (i=0; i<MAX_CHARA; i++) {
  if (Chara[i]==NULL) {
   break;
  }
  i++;
 }
 if (i>=MAX_CHARA) {
  return NULL;
 }

 switch (kind) {
 case KIND_MYCHARA: // 自キャラ
  Chara[i]= new CMyChara(i, x, y);
  break;
 case KIND_ENEMY01: // 敵キャラ01
  Chara[i]= new CEnemy01(i, x, y);
  break;
 }

 // 自分の親を設定
 if (parent!=NULL) {
  Chara[i]->SetParent(parent);
 }

 return Chara[i];
}

//-----------------------------------------------------------------------------
// キャラクタ解放
void CCharaManage::DeleteChara(int no)
{
 if (Chara[no]!=NULL) {
  delete Chara[no];
  Chara[no]=NULL;
 }
}

//-----------------------------------------------------------------------------
// 移動
int CCharaManage::Move()
{
 int  i, ret;

 for (i=0; i<MAX_CHARA; i++) {
  if (Chara[i]!=NULL) {
   Chara[i]->Move();
  }
 }

 return ret;
}

//-----------------------------------------------------------------------------
// 描画
void CCharaManage::Draw()
{
 int  i;

 for (i=0; i<MAX_CHARA; i++) {
  if (Chara[i]!=NULL) {
   Chara[i]->Draw();
  }
 }
}

ゲーム開発12:キャラクタクラスの設計

キャラクタクラスキャラクタクラスCCharaを作るため、あれこれ試行錯誤してました。

キャラクタを構成するのは初期化・移動・描画・解放の処理と、スプライトや位置情報等のデータです。

ゲーム内にはキャラクタがたくさん出てくるので、このキャラクタクラスは生成・解放を頻繁に繰り返し、そのため無駄が出てきます。なので再利用できるデータは再利用して無駄を省きたい。その辺でデータの持ち方を色々と考えてましたよ。

画像の表はとりあえず現時点でのクラス案です。定型部分をパーツクラスCCharaPartsとして分けてみました。

これで大丈夫かな? とりあえず次回から実装に移ります。

ゲーム開発11:方向性を決める

前回までに、基底・グラフィック・入力のクラスが完成しました。今回はどんなゲームにするかを決めます。

まずは基本システムを考えてみました。

・横スクロールアクションゲーム
・敵を倒したり、障害物を乗り越えていく
・ステージクリア型でボスを倒したらクリア
・自キャラは左右移動、ジャンプ、近接および遠距離攻撃

ありがちなシステムですね。そしてやってみたい事を列挙します。

・ある条件でパワーアップさせる
・フィールドに高低差を付ける
・スピード感を出す
・自キャラの他にサブキャラを出す
・敵の頭に乗れるようにする
・演出に凝ってみる

どこまで出来るかわかりませんが、とりあえずこんな方向性で行ってみます。あと、このままだと地味なので、他と差別化するアイデアを引き続き考えてみますか。

次回はキャラクタークラスを作ります。いちばん難しく、かつ何度も作り直すことになるであろうクラスです。

ゲーム開発10:スプライト処理修正

以前作ったスプライト処理ですが、間違いを見つけました。今のままだとZ方向の移動が出来ず、奥行きが実現できません。

そんな訳でまず、CGraphicsBase::InitD3D()に以下のパラメータを追加します。これは奥行きを使うための宣言です。

d3dpp.EnableAutoDepthStencil=TRUE;
d3dpp.AutoDepthStencilFormat=D3DFMT_D16;

D3Device->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);

次にCGraphicsBase::DrawSpriteBase()の中の計算とかを変更します。これでZ方向にも移動できるようになりました。

なぜ2Dなのに奥行きが必要かというと、背景の手前にキャラクタを表示し、キャラクタの手前に文字や設定画面を表示させたいからです。前のままでも、描画の順番を奥から順に並べれば同じ事はできます。でも、描画命令はあっちこっちのクラスから投げられてくるので、管理がややこしくなりそうです。そのため、今のうちに厳密に設定させるという訳です。

//---------------------------------------------------------
// スプライト描画
void CGraphicsBase::DrawSpriteBase(int tex_no, RECT drawRect, float x, float y, float z, DWORD color, float angle, float scale_x, float scale_y)
{
 D3DXMATRIX mat, mat1, mat2, mat3;
 float  center_x, center_y;
 D3DXVECTOR3 Trans, Center;

 // マトリクス初期化
 D3DXMatrixIdentity(&mat1);
 D3DXMatrixIdentity(&mat2);
 D3DXMatrixIdentity(&mat3);

 // マトリクスで拡大縮小回転移動
 D3DXMatrixScaling(&mat1, scale_x, scale_y, 1);
 D3DXMatrixRotationZ(&mat2, D3DXToRadian(angle));
 D3DXMatrixTranslation(&mat3, x, y, 0);
 mat=mat1*mat2*mat3;
 D3Sprite->SetTransform(&mat);

 // 移動
 Trans=D3DXVECTOR3(0, 0, z);

 // 画像の中心
 center_x=(float)(drawRect.right-drawRect.left)/2;
 center_y=(float)(drawRect.bottom-drawRect.top)/2;
 Center=D3DXVECTOR3(center_x, center_y, 0);

 // 描画
 D3Sprite->Draw(Texture[tex_no]->Tex, &drawRect, &Center, &Trans, color);
}

ゲーム開発9:入力処理2

90353511.jpg入力処理のCInputクラスです。

前回CInputBaseクラスにて、キーボードとゲームパッドから情報を取得する設定を行いました。今回は取得した情報をゲームで使える値に変換します。

ゲームで使う値は移動のxとy、そして決定キーa、キャンセルキーb、選択キーcとします。アクションゲームだったら、決定キーが攻撃になったりキャンセルキーがジャンプになったりします。ともかくそれをKEYCODE構造体としてまとめておきます。

ボタンが押された際、キーボードからは配列が、ゲームパッドからは構造体が返されます。押されたボタンを確認するには、dinput.hに書かれた定義を参照して、ビットが立っているかどうかで判断します。

ところでゲームパッドの十字キーは、アナログで最大0~65535まで値を取ったりするらしいので、閾値を10にして、ちょっとでも押されたら反応するようにしました。ホントはゲームパッドが返した情報を元に、ちゃんと決めないといけないんだけどね。持ってるゲームパッドは0か1000かの2択だったし。

押されたボタンがわかったら、それをKEYCODE構造体に格納して返せばOKです。ちなみにPressKey()では過去に押されたボタンの履歴を参考に、同時押しを抑制しています。

これでスプライトを動かす事が出来るようになりました。音楽以外は一通り基底部分が完成しましたので、次回はゲームの仕様を考えますか。

//---------------------------------------------------------
typedef struct {
 int x; // X軸
 int y; // Y軸
 int a; // 決定
 int b; // キャンセル
 int c; // 選択
} KEYCODE;

#define KEYDOWN(name, key) (name[key] & 0x80)

//---------------------------------------------------------
// キーボード入力取得
void CInput::GetKeyboard(KEYCODE *keycode)
{
 char buffer[256];
 HRESULT hr;

 // キーボードステータス取得
 hr=ProcessKeyboard(buffer);
 if FAILED(hr) {
  return;
 }

 // 6
 if (KEYDOWN(buffer, DIK_NUMPAD6)) {
  keycode->x=1;
 }
 // 4
 else if(KEYDOWN(buffer, DIK_NUMPAD4)) {
  keycode->x=-1;
 }

 // 8
 if (KEYDOWN(buffer, DIK_NUMPAD8)) {
  keycode->y=-1;
 }
 // 2
 else if(KEYDOWN(buffer, DIK_NUMPAD2)) {
  keycode->y=1;
 }

 // 右
 if (KEYDOWN(buffer, DIK_RIGHT)) {
  keycode->x=1;
 }
 // 左
 else if(KEYDOWN(buffer, DIK_LEFT)) {
  keycode->x=-1;
 }

 // 上
 if (KEYDOWN(buffer, DIK_UP)) {
  keycode->y=-1;
 }
 // 下
 else if (KEYDOWN(buffer, DIK_DOWN)) {
  keycode->y=1;
 }

 // Z
 keycode->a=0;
 if (KEYDOWN(buffer, DIK_Z)) {
  keycode->a=1;
 }

 // X
 keycode->b=0;
 if (KEYDOWN(buffer, DIK_X)) {
  keycode->b=1;
 }

 // C
 keycode->c=0;
 if (KEYDOWN(buffer, DIK_C)) {
  keycode->c=1;
 }

 // エンター
 if (KEYDOWN(buffer, DIK_RETURN) || KEYDOWN(buffer, DIK_NUMPADENTER)) {
  keycode->a=1;
 }

 // スペース
 if (KEYDOWN(buffer, DIK_SPACE)) {
  keycode->b=1;
 }
}

//---------------------------------------------------------
// ジョイスティック入力取得
void CInput::GetJoystick(KEYCODE *keycode)
{
 DIJOYSTATE js;
 HRESULT  hr;

 // ジョイスティックステータス取得
 ZeroMemory(&js, sizeof(DIJOYSTATE));
 hr=ProcessJoystick(&js);
 if FAILED(hr) {
  return;
 }

 // X軸
 if (js.lX>10) {
  keycode->x=1;
 }
 if (js.lX<-10) {
  keycode->x=-1;
 }

 // Y軸
 if (js.lY>10) {
  keycode->y=1;
 }
 if (js.lY<-10) {
  keycode->y=-1;
 }

 // 1ボタン
 if (KEYDOWN(js.rgbButtons, 0)) {
  keycode->a=1;
 }

 // 2ボタン
 if (KEYDOWN(js.rgbButtons, 1)) {
  keycode->b=1;
 }

 // 3ボタン
 if (KEYDOWN(js.rgbButtons, 2)) {
  keycode->c=1;
 }
}

//---------------------------------------------------------
// キー入力取得
void CInput::PressKey(KEYCODE *keycode)
{
 static int old_xy=0;  // 前回の移動キー
 KEYCODE  di_key, di_joy;
 int   new_x, new_y;

 ZeroMemory(keycode, sizeof(KEYCODE));

 // キーボード&ジョイスティック取得
 ZeroMemory(&di_key, sizeof(KEYCODE));
 GetKeyboard(&di_key);
 ZeroMemory(&di_joy, sizeof(KEYCODE));
 GetJoystick(&di_joy);

 // X軸
 new_x=di_key.x;
 if (di_joy.x!=0) {
  new_x=di_joy.x;
 }

 // Y軸
 new_y=di_key.y;
 if (di_joy.y!=0) {
  new_y=di_joy.y;
 }

 // 前回の軸を見て移動軸を決定
 if (new_x!=0 && new_y!=0) {
  if (old_xy==0) {
   keycode->y+=new_y;
  }
  else {
   keycode->x+=new_x;
  }
 }
 else {
  // 片方だけ移動
  keycode->x+=new_x;
  keycode->y+=new_y;
  if (new_y==0) {
   old_xy=0;
  }
  else {
   old_xy=1;
  }
 }

 // 決定ボタン
 keycode->a=di_key.a;
 if (di_joy.a!=0) {
  keycode->a=di_joy.a;
 }

 // キャンセルボタン
 keycode->b=di_key.b;
 if (di_joy.b!=0) {
  keycode->b=di_joy.b;
 }

 // 選択ボタン
 keycode->c=di_key.c;
 if (di_joy.c!=0) {
  keycode->c=di_joy.c;
 }
}

ゲーム開発8:入力処理1

予告どおり入力の処理です。

入力はDirectInputを使います。なんかクラス見たらDirectX8のまま変わってなかったんで楽勝、と思ったらやっぱり微妙に違う(汗) まぁほぼ手直し無しで済んだけどね。

今回もDirectXに依存する部分をCInputBaseクラス、入力を処理する部分をCInputクラスに分けます。使うデバイスはキーボードゲームパッドです。使うボタンはキーボードはテンキーと十字キー、あとZとXとCのボタンにします。ゲームパッドの方は十字キーと0と1と2のボタンです。

キーボードは簡単に初期化出来るんだけど、ゲームパッドはモノによってボタンや十字キー(もしくはアナログスティック)の数が違うので、コールバックで再帰的に設定します。まあ定型処理だし、最低限のボタンしか使わないから、関係ないけどね。

今回はCInputBaseだけ組んで、次回CInputで実際に入力キーを取り出します。

//---------------------------------------------------------
// 初期化
BOOL CInputBase::Init(HWND hWnd)
{
 HRESULT hr;

 // DirectInputオブジェクトを作成する
 hr=DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&DInput, NULL);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputオブジェクトの生成に失敗しました", "CInputBase::Init", MB_OK);
  return E_FAIL;
 }

 // キーボードの初期化
 hr=InitKeyboard(hWnd);
 if FAILED(hr) {
  return E_FAIL;
 }

 // ジョイスティックの初期化
 hr=InitJoystick(hWnd);
 if FAILED(hr) {
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// キーボードの初期化
BOOL CInputBase::InitKeyboard(HWND hWnd)
{
 HRESULT hr;

 // キーボードデバイスを取得する
 hr=DInput->CreateDevice(GUID_SysKeyboard, &DIKeyboard, NULL);
 if FAILED(hr) { 
  MessageBox(NULL, "DirectInputキーボードデバイスの生成に失敗しました", "CInputBase::InitKeyboard", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // キーボードのデータ形式を設定する
 hr=DIKeyboard->SetDataFormat(&c_dfDIKeyboard);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputキーボードのデータ形式の取得に失敗しました", "CInputBase::InitKeyboard", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // キーボードの協調レベルを設定する
 hr=DIKeyboard->SetCooperativeLevel(hWnd, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputキーボードの強調レベルの取得に失敗しました", "CInputBase::InitKeyboard", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // キーボードの入力デバイスにアクセスする
 hr=DIKeyboard->Acquire();
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputキーボードへのアクセスに失敗しました", "CInputBase::InitKeyboard", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// ジョイスティックの初期化
BOOL CInputBase::InitJoystick(HWND hWnd)
{
 HRESULT  hr;
 DIDEVCAPS g_DevCaps;

 //ジョイスティックのコールバック設定
 hr=DInput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback, NULL, DIEDFL_ATTACHEDONLY);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックコールバックの生成に失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // ジョイスティックが存在しているか?
 if (DIJoystick==NULL) {
  return S_OK;
 }

 // ジョイスティックのデータ形式を設定する
 hr=DIJoystick->SetDataFormat(&c_dfDIJoystick);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックのデータ形式の取得に失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // ジョイスティックの協調レベルを設定する
 hr=DIJoystick->SetCooperativeLevel(hWnd, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックの強調レベルの取得に失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // ジョイスティックの能力を取得する
 g_DevCaps.dwSize = sizeof(DIDEVCAPS);
 hr=DIJoystick->GetCapabilities(&g_DevCaps);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックの能力の取得に失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // 全ての軸を列挙する
 hr=DIJoystick->EnumObjects(EnumAxesCallback, (void*)hWnd, DIDFT_AXIS);
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックの軸の列挙に失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 // ジョイスティックの入力デバイスにアクセスする
 hr=DIJoystick->Acquire();
 if FAILED(hr) {
  MessageBox(NULL, "DirectInputジョイスティックへのアクセスに失敗しました", "CInputBase::InitJoystick", MB_OK);
  TermDInput();
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// ジョイスティック列挙コールバック
BOOL CALLBACK EnumJoysticksCallback(const DIDEVICEINSTANCE* pdidInstance, VOID* pContext)
{
 HRESULT hr;

 // ジョイスティックデバイスを取得する
 hr=System->Input->DInput->CreateDevice(pdidInstance->guidInstance, &System->Input->DIJoystick, NULL );
 if FAILED(hr) {
  return DIENUM_CONTINUE;
 }

 // ジョイスティックが取得できたらこの関数は呼び出されなくなる
 return DIENUM_STOP;
}

//---------------------------------------------------------
// ジョイスティック軸列挙コールバック
BOOL CALLBACK EnumAxesCallback(const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext)
{
 HWND hDlg=(HWND)pContext;

 DIPROPRANGE diprg;
 diprg.diph.dwSize  = sizeof(DIPROPRANGE);
 diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER);
 diprg.diph.dwHow  = DIPH_BYID;
 diprg.diph.dwObj  = pdidoi->dwType; // Specify the enumerated axis
 diprg.lMin    = -1000;
 diprg.lMax    = +1000;

 // ジョイスティックデバイスを設定する
 if (FAILED(System->Input->DIJoystick->SetProperty(DIPROP_RANGE, &diprg.diph))) {
  return DIENUM_STOP;
 }

 return DIENUM_CONTINUE;
}

//---------------------------------------------------------
// DirectInput解放
void CInputBase::TermDInput()
{ 
 if (DInput) {
  if (DIKeyboard) {
   // Release()を呼び出す前は常にデバイスを解放する
   DIKeyboard->Unacquire();
   DIKeyboard->Release();
   DIKeyboard=NULL;
  }
  if (DIJoystick) {
   // Release()を呼び出す前は常にデバイスを解放する
   DIJoystick->Unacquire();
   DIJoystick->Release();
   DIJoystick=NULL;
  }
  DInput->Release();
  DInput=NULL;
 }
}

//---------------------------------------------------------
// キーボード入力取得
BOOL CInputBase::ProcessKeyboard(char *buffer)
{
 HRESULT hr;

 if (DIKeyboard==NULL) {
  return E_FAIL;
 }

 // キーボードステータス取得
 hr=DIKeyboard->GetDeviceState(256/*sizeof(buffer)*/,(LPVOID)buffer);
 if FAILED(hr) {
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// ジョイスティック入力取得
BOOL CInputBase::ProcessJoystick(DIJOYSTATE *js)
{
 HRESULT  hr;

 if (DIJoystick==NULL) {
  return E_FAIL;
 }

 // ジョイスティックポーリング
 hr=DIJoystick->Poll();
 if FAILED(hr) {
  hr=DIJoystick->Acquire();
  while(hr==DIERR_INPUTLOST) 
  hr=DIJoystick->Acquire();
  return E_FAIL;
 }

 // ジョイスティックステータス取得
 hr=DIJoystick->GetDeviceState(sizeof(DIJOYSTATE), js);
 if FAILED(hr) {
  return E_FAIL;
 }

 return S_OK;
}

ゲーム開発7:スプライト処理

ba1523d5.gifグラフィックまわりをやる事にしました。

まずスプライト描画処理を修正しました。DrawSpriteBase()です。スプライトに拡大縮小回転と色味の引数を渡して、それを計算しています。

次にスプライトそのもののデータ管理ですが、スプライト管理構造体SPRITE_MANAGEMENTテクスチャ管理構造体TEXTURE_MANAGEMENTの2つに分けてみました。なぜかというと、1枚の画像(テクスチャ)から複数のスプライトを取り出すため、別々に扱った方がよさそうだからです。

まずテクスチャの生成CreateTexture()ですが、上に書いた通り1枚のテクスチャを複数のスプライトが参照するため、既に生成しているテクスチャなのかをTEXTURE_MANAGEMENTのファイル名で確認し、その参照している数をreferenceでカウントします。参照数のカウントは、テクスチャを破棄する際に、誰も参照していない事を確認するためです。誰かが参照してるのに破棄したら困るからね。

次にスプライトの生成CreateSprite()は、どのテクスチャを使うか、またテクスチャのどの領域から切り取って使うかをSPRITE_MANAGEMENTに格納します。

これで生成されたスプライトは、最低限スプライト番号spr_noと座標x,yだけで画面に表示できます。ちなみにDrawSprite()には引数がたくさんあるけど、spr_no,x,y以外はディフォルト引数にしているため、未入力でもOKです。

グラフィックに関しては、画面スクロールを実現するため、ワールド座標系ビューポート座標系の導入が必要になりますが、とりあえず置いといて、次回は入力まわりです。

//---------------------------------------------------------
// スプライト描画

void CGraphicsBase::DrawSpriteBase(int tex_no, RECT drawRect, float x, float y, float z, DWORD color, float angle, float scale_x, float scale_y)
{
 D3DXMATRIX mat, mat1, mat2, mat3;
 float  center_x, center_y;
 D3DXVECTOR3 Center;

 // マトリクス初期化
 D3DXMatrixIdentity(&mat1);
 D3DXMatrixIdentity(&mat2);
 D3DXMatrixIdentity(&mat3);

 // マトリクスで拡大縮小回転移動
 D3DXMatrixScaling(&mat1, scale_x, scale_y, 1);
 D3DXMatrixRotationZ(&mat2, D3DXToRadian(angle));
 D3DXMatrixTranslation(&mat3, x, y, z);
 mat=mat1*mat2*mat3;
 D3Sprite->SetTransform(&mat);

 // 画像の中心
 center_x=(float)(drawRect.right-drawRect.left)/2;
 center_y=(float)(drawRect.bottom-drawRect.top)/2;
 Center=D3DXVECTOR3(center_x, center_y, 0);

 // 描画
 D3Sprite->Draw(Texture[tex_no]->Tex, &drawRect, &Center, NULL, color);
}

//---------------------------------------------------------
// テクスチャ生成
int CGraphics::CreateTexture(char *filename)
{
 int  i, tex_no;

 // テクスチャが既に登録されていないか判断
 tex_no=-1;
 i=0;
 for (i=0; i<max_TEX; i++) {
  if (Texture[i]!=NULL) {
   if (strcmp(Texture[i]->filename, filename)==0) {
    tex_no=i;
   }
  }
 }
 if (tex_no!=-1) {
  Texture[tex_no]->reference++;
  return tex_no;
 }

 // 空いているテクスチャを探す
 tex_no=0;
 while (Texture[tex_no]!=NULL || tex_no>=MAX_TEX) {
  tex_no++;
 }
 if (tex_no>=MAX_TEX) {
  return -1;
 }

 // テクスチャ生成
 Texture[tex_no]= new TEXTURE_MANAGEMENT;
 Texture[tex_no]->filename= new char[strlen(filename)+1];
 Texture[tex_no]->reference=1;
 strcpy_s(Texture[tex_no]->filename, (strlen(filename)+1), filename);

 // テクスチャ読み込み
 CreateTextureBase(Texture[tex_no]->filename, &Texture[tex_no]->Tex);

 return tex_no;
}

//---------------------------------------------------------
// テクスチャ解放
void CGraphics::ReleaseTexture(int tex_no)
{
 // 解放済み?
 if (Texture[tex_no]==NULL) {
  return;
 }

 // どのオブジェクトからも参照されていなければ解放
 Texture[tex_no]->reference--;
 if (Texture[tex_no]->reference<=0) {
  delete[] Texture[tex_no]->filename;
  Texture[tex_no]->Tex->Release();
  delete Texture[tex_no];
  Texture[tex_no]=NULL;
 }
}

//---------------------------------------------------------
// スプライト生成
int CGraphics::CreateSprite(char *filename, RECT Range)
{
 int  i, tex_no, spr_no;

 // 空いているスプライトを探す
 spr_no=-1;
 for (i=0; i<max_SPR; i++) {
  if (Sprite[i]==NULL) {
   spr_no=i;
  }
 }
 if (spr_no==-1) {
  return -1;
 }

 // テクスチャ生成
 tex_no=CreateTexture(filename);

 // スプライト生成
 Sprite[spr_no]= new SPRITE_MANAGEMENT;
 Sprite[spr_no]->tex_no=tex_no;
 Sprite[spr_no]->Range=Range;

 return spr_no;
}

//---------------------------------------------------------
// スプライト解放
void CGraphics::ReleaseSprite(int spr_no)
{
 // 解放済み?
 if (Sprite[spr_no]==NULL) {
  return;
 }

 // テクスチャ解放
 ReleaseTexture(Sprite[spr_no]->tex_no);

 // 解放
 delete Sprite[spr_no];
 Sprite[spr_no]=NULL;
}

//---------------------------------------------------------
// スプライト描画
void CGraphics::DrawSprite(int spr_no, float x, float y, float z, DWORD color, float angle, float scale_x, float scale_y)
{
 DrawSpriteBase(Sprite[spr_no]->tex_no, Sprite[spr_no]->Range, x, y, z, color, angle, scale_x, scale_y);
}

ゲーム開発6:文字と画像を表示する

36e2abd7.gif引き続きDirect3Dでグラフィックスします。

まず文字ですが、前回LPD3DXFONT(D3DebugFont)に必要なパラメータを入れて初期化してるので、あとはDrawTextA()で表示するだけです。ちなみにこの関数、DirectX8と同名で別物だったんで、ちょっと苦労した(汗) ネットで検索しても、新旧両方のバージョンが入り混じってるからなぁ。

次はスプライト(画像)を表示させます。DirectX8以降、スプライトは3Dテクスチャとして扱われてるので、テクスチャ関連のクラスを使います。ファイルからD3DXCreateTextureFromFile()でテクスチャとして読み込み、オブジェクトLPDIRECT3DTEXTURE9に格納します。そしてDraw()で描画します。今回のスプライトまわりは直打ちで汚いので、後ほど作り直します。

それらを画面に出力します。背景をClear()で青に塗りつぶし、描画部分をBeginScene()EndScene()で囲います。最後にPresent()で出力。

ようやく画面が表示されました。そういえばフレームレートは何となく50FPSにしてみた。次回はグラフィックまわりを突き詰めるか、入力系のどちらかにします。

//---------------------------------------------------------
//  テクスチャ読み込み
BOOL CGraphicsBase::CreateTextureBase(LPCTSTR filename, LPDIRECT3DTEXTURE9 *tex)
{
 if(D3DXCreateTextureFromFile(D3Device, filename, tex)!=S_OK) {
  MessageBox(NULL, "テクスチャの読み込みに失敗しました", "CGraphicsBase::CreateTextureBase", MB_OK);
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// スプライト描画
void CGraphicsBase::DrawSpriteBase(int tex_no, RECT drawRect, int x, int y, int damage)
{
 RECT srcRect;
 SetRect(&srcRect, 0, 0, 128, 128); // テクスチャ切り出し
 D3DXVECTOR3 Center=D3DXVECTOR3(0, 0, 0); // センター
 D3DXVECTOR3 Position=D3DXVECTOR3(100, 100, 0); // 位置

 // 描画
 D3Sprite->Draw(Texture[tex_no]->Tex, &srcRect, &Center, &Position, D3DXCOLOR(1,1,1,1));
}

//---------------------------------------------------------
// レンダリング
void CGraphicsBase::Render()
{
 RECT drawRect;
 char text[100];

 if (D3Device==NULL) {
  return;
 }

 // バックバッファを青でクリアする
 D3Device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0);

 // シーン開始
 D3Device->BeginScene();

 // スプライト描画
 D3Sprite->Begin(D3DXSPRITE_ALPHABLEND);
 SetRect(&drawRect, 0, 0, 128, 128);
 DrawSpriteBase(0, drawRect, 100, 100);
 D3Sprite->End();

 // デバッグ文字表示
 if (System->DebugFlag==1) {
  SetRect(&drawRect, 0, 0, strlen(text)*32, 32);
  System->GetFPS(text);
  D3DebugFont->DrawTextA(NULL, (char *)text, -1, &drawRect, (DT_SINGLELINE | DT_LEFT | DT_TOP | DT_NOCLIP), 0xFFFFFFFF);
 }

 // シーン終了
 D3Device->EndScene();

 // シーンを表示する
 D3Device->Present(NULL, NULL, NULL, NULL);
}

ゲーム開発5:DirectX Graphics(Direct3D)を初期化する

ちょっと間が空いたけど、今回はDirect3Dの初期化と解放です。ここも定型処理だね。今回使うのはDirect3Dのデバイスとオブジェクト、Direct3DXのスプライトとフォントです。ちなみにDirect3DXはDirect3Dのラッパーです。

将来的にDirectXのバージョンが変わる可能性を考慮して(もう新しいの出てるけど)、クラスは2つに分けます。CGraphicisBaseとCGraphicsです。CGraphicisBaseにはDirectXに依存する処理を入れておき、CGraphicisに継承させます。いざ環境が変わったらCGraphicisBaseだけ差し替えれば動く、という感じにします。クラス名はCGraphicisBase90cみたいにバージョン入れた方がわかりやすいかもね。

しかし今回もDirectX8時代のソースを基に組んでるけど、細かい所が色々変わってるね。同じ名前なのにクラスがオーバーロードする訳でもなく、別物になってたりするし。ゲイツめ…

//---------------------------------------------------------
// 初期化

BOOL CGraphicsBase::Init(HWND hWnd)
{
 // 表示系初期化
 if (InitD3D(hWnd)==E_FAIL) {
  MessageBox(NULL, "Direct3Dの初期化に失敗しました", "CGraphicsBase::Init", MB_OK);
  return E_FAIL;
 }

 // スプライトオブジェクトの作成
 D3DXCreateSprite(D3Device, &D3Sprite);

 // フォント初期化
 InitFont(&D3DebugFont);

 return S_OK;
}

//---------------------------------------------------------
// Direct3D初期化

BOOL CGraphicsBase::InitD3D(HWND hWnd)
{
 D3DPRESENT_PARAMETERS d3dpp; 
 D3DDISPLAYMODE   d3ddm;

 // Direct3Dオブジェクトの作成
 if (NULL==(D3D=Direct3DCreate9(D3D_SDK_VERSION))) {
  MessageBox(NULL, "Direct3Dオブジェクトの生成に失敗しました", "CGraphicsBase::InitD3D", MB_OK);
  return E_FAIL;
 }

 // 現在のディスプレイモードを検出する
 if (FAILED(D3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm))) {
  MessageBox(NULL, "ディスプレイモードの検出に失敗しました", "CGraphicsBase::InitD3D", MB_OK);
  return E_FAIL;
 }

 // 3Dアプリケーションの動作を指定する
 ZeroMemory(&d3dpp, sizeof(d3dpp));
 d3dpp.BackBufferWidth=640;
 d3dpp.BackBufferHeight=480;
 d3dpp.Windowed=System->ScreenMode; // フルスクリーン:FALSE
 d3dpp.SwapEffect=D3DSWAPEFFECT_DISCARD;
 d3dpp.BackBufferFormat=d3ddm.Format;

 // Direct3Dデバイスの作成
 if (FAILED(D3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
         D3DCREATE_SOFTWARE_VERTEXPROCESSING,
         &d3dpp, &D3Device))) {
  MessageBox(NULL, "Direct3Dデバイスの生成に失敗しました", "CGraphicsBase::InitD3D", MB_OK);
  return E_FAIL;
 }

 return S_OK;
}

//---------------------------------------------------------
// フォント初期化

void CGraphicsBase::InitFont(LPD3DXFONT *D3Font)
{
 // テキスト設定
 D3DXFONT_DESCA FontDesc={
  32,     // 高さ
  0,     // 幅
  0,     // 太さ
  0,     // ミップレベル
  FALSE,    // イタリック
  SHIFTJIS_CHARSET,
  OUT_DEFAULT_PRECIS,
  DEFAULT_QUALITY,
  DEFAULT_PITCH | FF_DONTCARE,
  NULL,
 };

 // フォントオブジェクト作成
 D3DXCreateFontIndirect(D3Device, &FontDesc, D3Font);
}

//---------------------------------------------------------
// Direct3D解放

void CGraphicsBase::TermD3D()
{
 // フォント解放
 if (D3DebugFont!=NULL) {
  D3DebugFont->Release();
 }

 // スプライト解放
 if (D3Sprite!=NULL) {
  D3Sprite->Release();
 }

 // Direct3Dデバイス解放
 if (D3Device!=NULL) {
  D3Device->Release();
 }

 // Direct3Dオブジェクト解放
 if (D3D!=NULL) {
  D3D->Release();
 }
}

ゲーム開発4:システムクラス作成

ちょっと間が空いてしまいましたが、システムクラスCSystemを作成しています。もともと58.8fpsで動くように作ってあったのをそのまま流用です。そのうちちゃんと60fpsにしよう。

ProcessNextFrame()メインルーチンWndProc()から呼び出して、システムをフレーム単位で動くようにしています。ProcessNextFrame()内にあるWaitTime()で、動作が一定になるようにウェイトを入れてます。

昔作ったシステムでは場合分けやエラー処理をたくさん入れてたけど、最近はPCの性能も上がったし、OSもWindowsXP以上で動かすこと前提で組んでいるので、ばっさり切ってます。

いつまでも画面が表示されないと寂しいので、次はグラフィックスクラスCGraphicsを作って何か表示させようかね。

//---------------------------------------------------------
#define SPF 17 // 58.8フレーム

//---------------------------------------------------------
// 生成
CSystem::CSystem(HWND hWnd)
{
 // パラメータ設定
 MyhWnd=hWnd;

 // 乱数初期化
 srand((unsigned)timeGetTime());

 // タイマの高精度化開始
 timeBeginPeriod(1); 
}

//---------------------------------------------------------
// 解放
CSystem::~CSystem()
{
 // タイマの高精度化終了
 timeEndPeriod(1);
}

//---------------------------------------------------------
// Name: フレーム処理
HRESULT CSystem::ProcessNextFrame()
{
 // 計算

 // 時間待ち
 WaitTime();

 // レンダリング

 return S_OK;
}

//---------------------------------------------------------
// 時間待ち
void CSystem::WaitTime()
{
 static DWORD dwLastTick=0;
 static LONG  lErrTick=0;
 DWORD   dwCurrTick, dwNowTick;
 LONG   lWaitTick;

 // 待ち時間を計算
 dwCurrTick=timeGetTime();
 lWaitTick=SPF-(dwCurrTick-dwLastTick)+lErrTick;
 lErrTick=0;

 // 処理落ちしている?
 if (lWaitTick<0) {
  lErrTick=lWaitTick;     // lErrTickはマイナスになる
  if ((SPF+lErrTick)<0) lErrTick=0;
  dwLastTick=dwCurrTick;
  return;
 }

 // 処理が早ければスリープする
 if (lWaitTick>=4) {
  Sleep(lWaitTick-3);
 }

 // ループで細かい時間待ち
 while (SPF>((dwNowTick=timeGetTime())-dwLastTick)) {
  ;
 }

 dwLastTick=dwNowTick;
}