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

ゲーム開発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;
}

ゲーム開発3:システム構成を確認する

5f7d4ba8.jpgいきなりDirectXまわりを組んでいこうと思ったのですが、考えも無しに組んでいくと後々破綻しそうなので、とりあえず過去に組んだシステム構成を確認してみました。

画像のツリーがそれで、クラス毎に書いてます。WinMainからはじまり、CSystemで全体の初期化をします。その時にCGraphics等でDirectX関連の初期化も行います。またCSystemはフレームレート等の基底システムを司ります。

CGameModeは各モードの基底クラスで、タイトル画面だとか、エンディングとか、その時どきのシーンを派生します。

CMainModeがゲームプレイ時のシーンです。この中にキャラクタや移動するフィールドなんかを詰め込んでます。

図では分かりづらいですが、CMainMode等はCGameModeを継承、CMyChara等はCCharaを継承しています。

基本はこの構成で良さそうですね。ただこの過去のシステムでは、DirectXに依存する部分の隠蔽化が不十分だったため、今回ゼロから組み直すハメになったので、そこは気をつけてみます。

図を眺めてまず最初にやるのは、やはりCSystemクラスですね。その後DirectX関連を進めていきますか。

ゲーム開発2:ウィンドウを表示する

久しぶりにゲームを作ろうと思います。6年ぶりなので久しぶりどころじゃないけど。この6年で、覚えたことは忘れちゃったし、そもそも時代が変わってるので、新たな気持ちでゼロから勉強です。

せっかくなので、開発日誌をつけていこうと思います。ちなみに開発環境は、
WindowsXP SP3
DirectX9.0c
Visual Studio 2008 Express Edition with SP1
C++

で行きます。

まずはウィンドウを表示する処理を作ります。WindowsのプログラムはWinMain()からですね。最近はUnicode対応の関係から_tWinMain()とかいうのが定義されてるみたいだけど、とりあえず今まで通りで。

ここの処理はもう定型文みたいなものですね。描画はDirectXにやらせるので、以下のソースコードではウィンドウ内には何も表示されません。

次はフレームレートかな、それともDirectX Graphicsに進んだ方がいいかな。

//---------------------------------------------------------
// メイン

#include "windows.h"

#define WINDOW_NAME "開発テスト"
#define APP_NAME "devtest"

// システムメッセージの処理
LRESULT WINAPI WndProc(HWND, UINT, WPARAM, LPARAM);

//---------------------------------------------------------
// アプリケーション開始

INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, INT)
{
 RECT rect={0, 0, 640, 480};
 MSG  msg;
 
 // ウィンドウクラスを登録する
 WNDCLASSEX wc={sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L,
       GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
       APP_NAME, NULL};
 RegisterClassEx(&wc);
 
 // アプリケーションのウィンドウを作成する
 AdjustWindowRectEx(&rect, WS_OVERLAPPEDWINDOW, FALSE, NULL);
 HWND hWnd=CreateWindow(APP_NAME, WINDOW_NAME, 
         WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, (rect.right-rect.left), (rect.bottom-rect.top),
         GetDesktopWindow(), NULL, wc.hInstance, NULL);
 
 // 独自の処理★
 
 // ウィンドウ表示
 ShowWindow(hWnd, SW_SHOWDEFAULT);
 UpdateWindow(hWnd);
 
 // メッセージループ
 ZeroMemory(&msg, sizeof(msg));
 while (msg.message!=WM_QUIT) {
  if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) {
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
  else {
   // 独自の処理★
  }
 }
 
 return 0;
}


//---------------------------------------------------------
// システムメッセージの処理

LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
 switch (msg) {
 case WM_ACTIVATE:
  // ウィンドウがアクティブになった
  // 独自の処理★
  return 0;
 
 case WM_KEYDOWN:
  switch (wParam) {
  case VK_ESCAPE:
   // 強制終了
   DestroyWindow(hWnd);
   return 0;
  }
  break;
 
 case WM_DESTROY:
  // ウィンドウが閉じられた
  // 独自の処理★
  PostQuitMessage(0);
  return 0;
 }
 
 return DefWindowProc(hWnd, msg, wParam, lParam);
}

ゲーム開発1:Visual Studio 2008 Express Edition with SP1を入れる

ネットニュースでVisual Studio 2008 Express Edition with SP1が提供開始したとなっていたので、これを機にインストールしてみました。無料だし。

以下のサイトから
http://www.microsoft.com/japan/msdn/vstudio/express/

Visual StudioのExpress Editionは、無料で使えるプログラムの統合環境です。色々制限がかかってるけど、商用利用もOKなので、同人ゲーム開発なんかには良いね。俺はVisual C++を使うことにしました。

ためしにコンソールアプリケーションで“Hello, world!”を表示させてみました。うん、簡単。次にWindowsアプリのテンプレートプロジェクトを開いてみたら…よくわからん。知らない構造体?クラス?や関数だらけだし。まあネットでちょっと調べてみたら、Win32で組むプログラムの構造は昔と変わってないみたいだったので安心した。

次にDirectXの開発一式を以下からダウンロード
http://www.microsoft.com/japan/msdn/directx/downloads.aspx

今の最新はMicrosoft DirectX SDK (Mar 2008)です。インストールしたら勝手にVisual Studioにパスを通してくれるから便利だね。

これで一通り準備は整いました。ちなみに、試しにDirectX8.0で組んだプログラムをビルドしようとしたら色々足りないってエラー出たw 今はDirectX9.0cの時代だからまぁ仕方ない。

次回はメインループを組んでみようかね。

ゲームつくろう!

知り合いの社長さんと今後の事業計画を練ってる時、なんとなく「ゲーム作りたいなぁ」と言ったところ、「やりましょう!」と同意されてしまいました。マジですか。

まあ、その人はゲーム開発をわかってない人なので、ポシャる可能性の方が高いけどね。

しかし個人的にゲーム作るのも良いかもね。前に同人でゲーム作ってたのは5年位前か。当時と比べると、今は色々変わってるだろうな。まだ主流はWindows&C++&DirectXなんだろうか。グラフィックボードの機能は共通化されただろうか。3Dは扱いやすくなっただろうか。

仕事の合間に調べながらプログラム組んでみようかね。作るゲームは初心に戻ってアクションゲームの予定です。