ADVシステム実験第一回

いろいろ書こうと思ったけど(そう、今回も私が時々起こす「RPG創りたい病」の発作です(^^;)、もはや何を書いても空しくなるだけなので前置きなしで始めます。

今回は、ADVRPG用のイベントシステムを作ってみましょう。感じとしては、ゲーム作成ツールなどでよく見かけるメッセージ表示や状況に応じてゲームの進行を変える汎用的なイベント実行エンジンです。すでにシューティング用に簡単なスクリプトシステムを作っているので、あれと同じように「ゲーム世界」クラスとその世界を駆動・描画するメインプログラムという構成ででやってみることにしました。

今回は、まず世界を生成しそれをキーボードで移動しながら表示する部分を作ってみます。

ゲームの構造

今回のゲームは、ゲームの世界をモデル化したCGameクラス(のインスタンスgame)とそのモデルを駆動・表示するメインプログラムから構成されます。CGameクラスは、メンバとしてキャラクタオブジェクトとマップオブジェクトを保持し、その管理も行うようにしました。これらのクラスの概要は以下のとおりです。

世界(CGameクラスインスタンス)

画面表示用のビットマップを保持し、指定されたデバイスコンテキストにゲームの状態を描画する。また、公開関数action()によりゲームの「時間」を進め、キーボード入力をメッセージとしてとして受け取る。

キャラクタ(CPlayerクラスインスタンス)

キャラクタの状態と描画用ピクセル列を保持し、指定されたDIBピクセル列にアニメーション表示を行う。

マップ(CMapクラスインスタンス)

マップデータとマップチップを保持し、指定された範囲のマップを描画する。また、指定された座標に対する各種の判定などを行う。

今回のプログラムの「主役」であるCGameクラスは、以下のようになっています。

CGameクラス宣言

class CGame {

public:

	CGame();

	bool init(HDC);

	bool setColorTable(LPBYTE);
	bool setMap(LPBYTE);
	bool setMapChips(LPBYTE);
	bool setPlayerBMP(LPBYTE,int);

	int getPlayerX();
	int getPlayerY();

	bool setPlayerPos(int,int);

	bool draw(HDC,int,int);

	bool action();

	bool OnVK_UP();
	bool OnVK_RIGHT();
	bool OnVK_DOWN();
	bool OnVK_LEFT();

	~CGame();

private:

	bool scroll();

	LPBYTE lpScreen,lpScroll;
	LPBITMAPINFO lpbiScreen,lpbiScroll;
	HBITMAP hScreen,hScroll;
	HDC hdcScreen,hdcScroll;
	RGBQUAD *lpRGB;

	CPlayer player;
	CMap map;
	int iX,iY,iDx,iDy,iSx,iSy,iScStep,iCount;
	bool fOnScroll;

};

ゲームの流れとしては、メインプログラムからゲームの時間を進めるaction()が呼び出されるとそこでスクロールや主人公の移動、あるいは表示される主人公パターンの切り替えなどが行われ、(ゲームの状態が変化し必要であれば)その結果が8bitDIBSectionピクセル列lpScreenに描画されます。そして、lpScreenに描画が行われるとaction()trueを返すので、メインプログラムはメインウインドウのデバイスコンテキストを引数にgame->draw()を呼び出してdraw()が渡されたデバイスコンテキストにDIBSectionのデバイスコンテキストhdcScreenBitBlt()することで画面上にゲームが描画されます。

 移動はキーボードで行いますが、キーボード入力はメインプログラムで監視し、イベントとしてgameに通知するようにしました。具体的には、入力があるとgameのイベントハンドラ

  bool OnVK_UP();
  bool OnVK_RIGHT();
  bool OnVK_DOWN();
  bool OnVK_LEFT();

を呼び出すようになっています。

以上のことから、今回のメインプログラム内では以下のようなメインループでgameaction()とキーボード入力イベントハンドラを呼び出しながらゲームを進めるようにしました。

  while (1) { /* メインループ */

      if (PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) {

          if (!GetMessage(&msg,NULL,0,0)) /* メッセージ処理 */
              return msg.wParam ;

          TranslateMessage(&msg);
          DispatchMessage(&msg);

      } else if (getTime()>dwTime+16) {

          dwTime=getTime();

          if (GetForegroundWindow()==hwMain) { /* キー入力監視 */

              if (GetAsyncKeyState(VK_UP)<0)
                  game->OnVK_UP();

              if (GetAsyncKeyState(VK_RIGHT)<0)
                  game->OnVK_RIGHT();

              if (GetAsyncKeyState(VK_DOWN)<0)
                  game->OnVK_DOWN();

              if (GetAsyncKeyState(VK_LEFT)<0)
                  game->OnVK_LEFT();

          }

          if (game->action()) { /* ゲームを進める */

              InvalidateRect(hwMain,&recGame,FALSE);
              UpdateWindow(hwMain);

          }

      } else
          Sleep(0);

  }

ゲーム初期化

メインプログラムでは、最初にCGameクラスのインスタンスgameを生成します。この段階でgameの内部でマップやキャラクタオブジェクトが生成されているので、以降はCGameクラスが公開しているアクセス用の関数を通してgame内部のCMapオブジェクトやCPlayerオブジェクトにアクセスできます。

メインプログラムでは、game生成後にキャラクタやマップチップのビットマップを読み込んでそれらのビットマップをgameのアクセス用関数でマップパーツやキャラクタパターン、さらにカラーテーブルとして設定するようにしました。

  game=new CGame();

  lpWrk=(LPBYTE)GlobalAlloc(GPTR,256*256);

  for (i=10;i<246;i++) /* 乱数でマップ作成 */
      for (j=10;j<246;j++)
          lpWrk[j+i*256]=8+rand() % 4;

  game->setMap(lpWrk);

  loadColorTable("chr.bmp",lpWrk);
  game->setColorTable(lpWrk);

  loadPixels("chr.bmp",lpWrk);

  lpTmp=(LPBYTE)GlobalAlloc(GPTR,1024);

  for (i=0;i<32;i++) /* キャラパターン0を作成 */
      CopyMemory(lpTmp+i*32,lpWrk+i*256,32);

  game->setPlayerBMP(lpTmp,0);

  for (i=0;i<32;i++) /* キャラパターン1を作成 */
      CopyMemory(lpTmp+i*32,lpWrk+i*256+32,32);

  game->setPlayerBMP(lpTmp,1);

  GlobalFree(lpTmp);

  /* マップチップ設定 */
  loadPixels("parts.bmp",lpWrk);
  game->setMapChips(lpWrk);

まずloadColorTable()は、指定した8ビットBMPファイルからカラーテーブルを読み込み、そのカラーテーブルを指定された領域にコピーする関数です。

  bool loadColorTable(LPCTSTR lpszFn,LPBYTE lpColors) { /* カラーテーブル */

      DWORD dummy;
      LPBYTE lpBuf;
      HANDLE fh;

      fh=CreateFile(lpszFn,GENERIC_READ,0,NULL,OPEN_EXISTING,
          FILE_ATTRIBUTE_NORMAL,NULL); /* ファイルオープン */

      lpBuf=(LPBYTE)GlobalAlloc
            (GPTR,GetFileSize(fh,NULL)); /* バッファ確保 */

      ReadFile(fh,lpBuf,GetFileSize(fh,NULL),&dummy,NULL);

      CloseHandle(fh);

      CopyMemory(lpColors,
      lpBuf+sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER),1024);

      GlobalFree(lpBuf);

      return true;

  }

読み込んだカラーテーブルは、gamesetColorTable()に渡してそこでゲーム中の描画形式を指定するBITMAPINFOにコピーされると、ゲーム全体のカラーテーブルとなります。

  bool CGame::setColorTable(LPBYTE lpArg) {

      if (lpArg==NULL)
          return false;

      CopyMemory((LPBYTE)lpbiScreen+sizeof(BITMAPINFOHEADER),lpArg,1024);
      CopyMemory((LPBYTE)lpbiScroll+sizeof(BITMAPINFOHEADER),lpArg,1024);

      return true;

  }

カラーテーブルの初期化に続いて、マップチップやキャラクタのピクセル列を読み込みます。今回は、マップチップ・キャラクタとも32×32ピクセルのパターンを8×8個(256×256ピクセル)並べたビットマップとして定義する形にしました。

カラーテーブルとピクセル列を設定したら、ウインドウのデバイスコンテキストをgameinit()に渡してgameを初期化します。

  /* ゲームを初期化 */
  game->setPlayerPos(12,12);
  game->init(GetDC(hwnd));

  GlobalFree(lpWrk);

init()では、メインプログラムから渡されたカラーテーブルとデバイスコンテキストを元にスクロール領域やゲーム状態などを描画する8bit DIBSectionを作成し、マップの状態を初期化します。

  bool CGame::init(HDC hdc) {

      hScreen=CreateDIBSection(hdc,lpbiScreen,DIB_RGB_COLORS,(LPVOID*)&lpScreen,NULL,0);
      hdcScreen=CreateCompatibleDC(hdc);
      SelectObject(hdcScreen,hScreen);

      hScroll=CreateDIBSection(hdc,lpbiScroll,DIB_RGB_COLORS,(LPVOID*)&lpScroll,NULL,0);
      hdcScroll=CreateCompatibleDC(hdc);
      SelectObject(hdcScroll,hScroll);

      map.draw(lpScreen,player.getX()-8,player.getY()-8,17,17);

      iDx=0;
      iDy=0;
      iSx=0;
      iSy=0;
      fOnScroll=false;
      iScStep=2;
      iCount=0;

      return true;

  }

ゲームの描画

次に、CGameクラスで行われるゲームの描画を見て行きましょう。今回は、スクロール表示の時にスクロール領域のマップがDIBSectionピクセル列lpScrollに描かれ、そのlpScrollがゲーム内の時間と共に表示用ピクセル列lpScreenにコピーされていきます。そして、ゲームを描く段階でそのlpScreenに主人公のピクセル列がCPlayerクラスのdraw()で描画されるようにしました。キャラクタやマップチップの大きさは32×32ピクセル。1画面は、マップチップが17×17個で544×544ピクセルで構成されます。

まずスクロールですが、スクロールはキーボードイベントで開始されます。例えば、上方向の移動は上方向キー入力のイベントハンドラOnVK_UP()で以下のように開始されます。

  bool CGame::OnVK_UP() {

      if (fOnScroll || !map.enter(player.getX(),player.getY()-1))
          return false;

      iDy=-iScStep;
      fOnScroll=true;

      return true;

  }

最初の判定で、すでにスクロール中でなく移動先に進入できるのであれば、iDyにスクロール方向を設定してスクロールフラグ変数fOnScrolltrueにします。これで、次のaction()実行時にscroll()が呼び出されスクロールが始まります。

scroll()では、スクロールの位置を保持する変数iSx, iSyを更新しながらスクロール処理を行います。これらの変数は、スクロール開始時に0に設定され、以降は1ステップごとにスクロール方向に応じて加算/減算されていきます。そして、その絶対値がマップ1パーツ分である31を超えるとスクロール終了です。スクロール終了時には、主人公の位置を更新してスクロール終了時の画面をlpScreenに描きスクロールフラグをクリアするようにしました。

  bool CGame::scroll() {

      int iVx=0,iVy=0;

      /* スクロール開始時にスクロール領域をlpScrollに描画 */
      if (iSx==0 && iSy==0) {

          if (iDx<0)
              map.draw(lpScroll,player.getX()-9,player.getY()-8,18,18);
          if (iDx>0)
              map.draw(lpScroll,player.getX()-8,player.getY()-8,18,18);
          if (iDy<0)
              map.draw(lpScroll,player.getX()-8,player.getY()-9,18,18);
          if (iDy>0)
              map.draw(lpScroll,player.getX()-8,player.getY()-8,18,18);

      }

      iSx+=iDx;
      iSy+=iDy;

      if (iDx<0)
        iVx=31+iSx;
      if (iDx>0)
          iVx=iSx;
      if (iDy<0)
          iVy=31+iSy;
      if (iDy>0)
          iVy=iSy;

      if (iSx<-31) {

          player.setX(player.getX()-1);
          StretchBlt(hdcScreen,0,0,544,544,hdcScroll,0,0,544,544,SRCCOPY);

          fOnScroll=false;
          iDx=0;
          iSx=0;

      }

      if (iSx>31) {

          player.setX(player.getX()+1);
          StretchBlt(hdcScreen,0,0,544,544,hdcScroll,32,0,544,544,SRCCOPY);

          fOnScroll=false;
          iDx=0;
          iSx=0;

      }

      if (iSy<-31) {

          player.setY(player.getY()-1);
          StretchBlt(hdcScreen,0,0,544,544,hdcScroll,0,0,544,544,SRCCOPY);

          fOnScroll=false;
          iDy=0;
          iSy=0;

      }

      if (iSy>31) {

          player.setY(player.getY()+1);
          StretchBlt(hdcScreen,0,0,544,544,hdcScroll,0,32,544,544,SRCCOPY);

          fOnScroll=false;
          iDy=0;
          iSy=0;

      }

      /* スクロール中なら、スクロール領域をずらしながらlpScreenに描画 */
      if (fOnScroll)
          StretchBlt(hdcScreen,0,0,544,544,hdcScroll,iVx,iVy,544,544,SRCCOPY);

      return true;

  }

以上の処理が行われることでゲーム中には、常にlpScreenに現在のマップが描かれています。あとは、このlpScreenに主人公を描けばゲームの描画を行うことができますね。ゲームの描画を行うCGame::draw()は、以下のようになっています。

  bool CGame::draw(HDC hdc,int iArg1,int iArg2) {

      player.draw(lpScreen);

      BitBlt(hdc,iArg1,iArg2,544,544,hdcScreen,0,0,SRCCOPY);

      return true;

  }

主人公の描画を行うCPlayer::draw()は以下のように単純な透過描画を行います。今回は、主人公のパターンを2つ用意してアニメショーン表示を行うのですが、dwIndexが「何番目のキャラクタパターンを描くか」示す変数になっています。

  bool CPlayer::draw(LPBYTE lpArg) {

      for (int i=0;i<32;i++)
          for (int j=0;j<32;j++)
              if (lpPixels[dwIndex][j+i*32]!=0)
                  lpArg[j+8*32+(8*32+i)*544]=lpPixels[dwIndex][j+i*32];

      return true;

  }

プログラム

プログラムダウンロード(advsys1.zip/23KB)

実行ファイル、またはプロジェクトと同じディレクトリにparts.bmp, chr.bmpを置いて実行してみてください。カーソルキーで上下左右に移動できます。


プログラミング資料庫 > ゲーム開発室