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

ゲーム開発18:画面スクロール

画面スクロールです。これがあれば一気に世界が広がります。

でも実装は簡単です。表示する全てのオブジェクトの位置を、自キャラの位置を中心にして変換すれば良いだけです。

具体的には、オブジェクトの座標値から自キャラChara[0]の座標値を引くと自キャラからの相対座標が出るので、それを画面の中心(今回はX320,Y240)だけ移動させればおしまいです。

以下のプログラムでは、さらに画面の端ではスクロールしないようにしています。

スクロール範囲に固定値を入れてるので、後で修正が必要ですね。他にはスクロールして見えないところの描画のクリッピングや、当たり判定の省略なんかも考えないと。でも今時のPCならマシンパワーで押し切れるかなw

次回はやり忘れてたゲームモードクラスCGameModeの設計です。

//-----------------------------------------------------------------------------
// 座標をスクロール後に変換
void CCharaManage::Scroll(float before_x, float before_y, float *after_x, float *after_y)
{
 float my_x, my_y;

 // 自キャラの位置
 my_x=Chara[0]->mx;
 if (my_x<320) {
  my_x=320;
 }
 if (my_x>(6400-320)) {
  my_x=6400-320;
 }
 my_y=240;

 // 相対位置に変換
 *after_x=before_x-my_x+320;
 *after_y=before_y-my_y+240;
}

ゲーム開発17:キャラクタ情報をファイルから読み込む

今回はキャラクタの位置情報をファイルから読み込みます。別段難しい処理は行いません。

キャラクタ管理クラスCCharaManage
初期化関数Init()で処理します。まず引数にあるファイルを開いて、中に入っているデータを一行ずつ読み込んでいきます。データ形式は以下のような感じです。

0,3,5,-1

これは、キャラクタ種類、X座標、Y座標、親IDという順番です。座標はピクセルで入力すると面倒なので、1キャラクタ単位である48ピクセル毎に配置します。つまり上のデータ形式ではX=3、Y=5になってますが、実際はX=144、Y=240に配置されます。あと親IDが-1なのは親がいないという意味です。

読み込んだ行が空の場合、もしくは#が入ってる場合はコメント行と判断して、処理は行いません。これでデータファイル内にコメントが書き込めますね。

strtok_s()でカンマ区切りの数値を取り出し、CreateChara()でキャラクタを生成します。

これでゲームステージ内の任意の場所に、キャラクタを配置できるようになりました。次回は画面スクロールかな。

//-----------------------------------------------------------------------------
// 初期化
void CCharaManage::Init(char *filename)
{
 int  ikind, ix, iy, iparent;
 char str[256], *nexttoken;
 FILE *fp;

 // キャラクタ情報読み込み
 if((fopen_s(&fp, filename, "r"))!=0) {
  sprintf_s(str, 256, "%s が見つかりません", filename);
  MessageBox(NULL, str, "CharaManage::Init", MB_OK);
  return;
 }
 while (fscanf_s(fp, "%s", &str, 256)!=EOF) {
  if (sizeof(str)!=0 && strstr(str, "#")==NULL) {
   // 分解する
   ikind=atoi(strtok_s(str, ",", &nexttoken));
   ix=atoi(strtok_s(NULL, ",", &nexttoken));
   iy=atoi(strtok_s(NULL, ",", &nexttoken));
   iparent=atoi(strtok_s(NULL, ",", &nexttoken));
   
   if (iparent==-1) {
    // 親がいない場合
    CreateChara(ikind, (ix*48), (iy*48), NULL);
   }
   else {
    // 親がいる場合
    CreateChara(ikind, (ix*48), (iy*48), Chara[iparent-1]);
   }
  }
 }
 fclose(fp);
}

ゲーム開発16:キャラクタ同士の当たり判定2

今回はキャラクタに当たり判定を実装してみます。

自キャラはアクションが多くてややこしいので、まずは単純な動きの敵キャラを作ります。敵01キャラクラスCEnemy01Move()内にて、前回作ったHit()を呼び出します。呼び出す際、移動後の座標を引数に入れ、当たっていたら移動させません。

ちなみにこの敵01はスーパーマリオで言うところのクリボーと同じ動きをします。つまり地面を歩いて、何かにぶつかったら方向転換し、地面が無ければそのまま落っこちる動作です。

重力を実現するために、常に下向きに移動させています。そしてdirectionというメンバ変数に、移動方向(LEFTとかRIGHTという列挙型定数)を保持させて、それの通りに歩かせます。これで上記の動きをしてくれます。

ちなみに作業中の自キャラクラスでは、もう少しリアリティを出すために擬似的ですが重力加速度を導入しています。

次回はキャラクタの位置情報をファイルから読み込む処理をやります。

//-----------------------------------------------------------------------------
// 移動
void CEnemy01::Move()
{
 int  move;
 RECT retRect;

 // 重力
 if (CharaManage->Hit(my_no, mx, my+4, &retRect)==-1) {
  my+=4;
 }

 // 移動方向
 move=-2;
 if (direction==RIGHT) {
  move=2;
 }
 if (CharaManage->Hit(my_no, mx+move, my, &retRect)==-1) {
  // 当たってなければ移動
  mx+=move;
 }
 else {
  // 当たってたら反転
  if (direction==RIGHT) {
   direction=LEFT;
  }
  else {
   direction=RIGHT;
  }
 }
}

ゲーム開発15:キャラクタ同士の当たり判定1

間髪入れずに当たり判定です。方法は色々ありますが、今回は矩形での当たり判定を行います。矩形とは四角形の事で、キャラクタを四角形として考えて、四角形同士が重なったかどうかで判定します。

判定はとりあえず存在するキャラクタ同士で総当たりさせます。キャラクタ管理クラスCCharaManageHit()という関数を作って、その中で行います。

まずは判定したいキャラクタからHit()が呼び出されます。内部では全キャラ分ループさせて、他のキャラと当たったかどうか判定します。キャラの矩形は、あらかじめキャラクタクラスCCharaに当たり判定用のRECT構造体を作っておき、そのRECTと座標mx,myから実際の座標系での矩形を生成します。重なるキャラが見つかったら、その番号と実座標の矩形を返します。

シンプルですね。ただこれだと複数のキャラとぶつかった場合、判定がおかしくなる可能性があります。あと高速で移動する小さなキャラの当たり判定も行えません。すり抜けちゃうから。

でも欲張ったら先に進めなくなるので、このまま行きます。

次回はこのHit()を使って、実際に当たり判定を行います。

//---------------------------------------------------------
// 当たり判定
int CCharaManage::Hit(int chara_no, float x, float y, RECT *retRect)
{
 int  i, ret;
 RECT myRect, yourRect;

 // 自分の矩形
 myRect=Chara[chara_no]->GetHitArea();
 myRect.top+=(LONG)y;
 myRect.bottom+=(LONG)y;
 myRect.left+=(LONG)x;
 myRect.right+=(LONG)x;

 ret=-1;
 for (i=0; i<MAX_CHARA; i++) {
  if (i!=chara_no && Chara[i]!=NULL) {
   // 相手の矩形
   yourRect=Chara[i]->GetHitArea();
   if (yourRect.top!=-1) {
    yourRect.top+=(LONG)Chara[i]->my;
    yourRect.bottom+=(LONG)Chara[i]->my;
    yourRect.left+=(LONG)Chara[i]->mx;
    yourRect.right+=(LONG)Chara[i]->mx;

    if (myRect.bottom>yourRect.top && myRect.top<yourRect.bottom 
     && myRect.left<yourRect.right && myRect.right>yourRect.left) {
      // 当たった
      ret=i;
      *retRect=yourRect;
    }
   }
  }
 }
 return ret;
}

ゲーム開発14:キャラクタクラス作成

0307a462.jpg「みずほ銀の著作権投資、ゲーム開発にシフト」らしい>挨拶
---

キャラクタクラスCCharaを作るため試行錯誤してたのですが、あれやこれやと色んなモノを詰め込もうとして、訳わからん状態となってしまいました。

これじゃいつまで経っても完成しないという事で、複雑な処理は排除、外部ファイルとして持たせるデータもソースにベタ打ちさせて、とにかく動くもの優先で作ることにしました。

とりあえず以下の値だけ持たせました。

座標mx,my,mz
キャラクタ番号my_no
スプライト番号spr_no
キャラクタ管理クラスへのポインタCCharaManage*

ソースですが、前回のキャラクタ管理クラスCCharaManageからCCharaを継承した自キャラクラスCMyCharaを呼び出します。呼び出し時に即値でスプライトを生成して、spr_noに格納します。今はあまり意味無いけど、CCharaManageへのポインタもCommonInit()の中で格納しています。

Move()ではCInputクラスからキー入力を受け取り、座標mx,myに移動量を加算します。これで移動できます。

最後にDraw()からCommonDraw()を呼び出してスプライト表示させます。今は無駄に見えますが、キャラクタ毎に描画方法は変わりますので、こう書いてます。

一応これで任意の場所にキャラクタを表示し、キーボード等で移動させる事が出来ます。欲張らずに、最初からこうやって組んどけばよかったよ。

次回は当たり判定を行います。

//---------------------------------------------------------
// 初期化
CMyChara::CMyChara(int no, int x, int y, CCharaManage *manage)
{
 RECT workRect;

 my_no=no;
 CommonInit(x, y, 1, manage);

 // スプライト作成
 SetRect(&workRect, 0, 0, 48, 96);
 spr_no[0]=System->Graphics->CreateSprite("data\test.jpg", workRect);
}

//---------------------------------------------------------
// 移動
void CMyChara::Move()
{
 float work_x, work_y;
 KEYCODE key;

 // キーコード取得
 System->Input->PressKey(&key);

 work_x=(float)key.x*6;
 work_y=(float)key.y*6;

 mx+=work_x;
 my+=work_y;
}

//---------------------------------------------------------
// 描画
void CMyChara::Draw()
{
 CommonDraw();
}

//---------------------------------------------------------
// 共通描画
void CChara::CommonDraw()
{
 System->Graphics->DrawSprite(spr_no[0], mx, my, mz, D3DXCOLOR(1,1,1,1), 0, 1, 1);
}

//---------------------------------------------------------
// 共通初期化
void CChara::CommonInit(int x, int y, int type, CCharaManage *manage)
{
 CharaManage=manage;

 mx=(float)x;
 my=(float)y;
 mz=1.0f;
}

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