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

27:キーボード入力の改良

 前回の結果をふまえて、テストプログラムを作成してみることにしましょう。用意するクラスはこの3つです。

  • CInputTestStart : プログラム自体の初期化を行う
  • CShip     : プレイヤーの動かすキャラクター
  • CShoot    : プレイヤーの発射したミサイル

 CInputTestStartクラスでは、入力クラスと戦闘機クラスの登録を行います。

inputstart.h
// データ初期化クラス
class CInputTestStart : public CGameObject
{
protected:
	void Init();
};

inputstart.cpp
void CInputTestStart::Init()
{
	// テクスチャデータを作成する
	CTexture *texdata = new CTexture(_T("ship.png"));
	AppendObject(texdata, -1, "texture", true);
	// hash_mapに登録
	AppendItemBox("texture", texdata);

	// 入力クラスを追加
	AppendObject(new CInput(), -1, "input", true);

	// 戦闘機クラスを作成(後述)
	CShip *ship = new CShip();
	AppendObject(ship, 1000, "ship", true);
	// hash_mapに登録
	AppendItemBox("ship", ship);

	RemoveObject(this);
}

 戦闘機を管理するCShipクラスでは、テクスチャクラスと入力クラスの関連づけと初期位置指定をInit()で行い、Exec()では、CInputクラスで取得されたキー入力情報から十字キーとスペースキーの押下状態を取得し、それに基づき、描画グラフィックの更新とミサイルの発射を行います。なお、座標位置とテクスチャ情報は発射されたミサイルを管理するCShootクラスでも用いりますので、共通のクラス「CCharactor」を作成し、CShipおよびCShootはその派生クラスとすることにします。

 CShootクラスではミサイルキャラクターの制御を行います。ミサイルは1フレームごとに上へ5ピクセル移動し、画面外まで移動したら自分自身を破棄しています。

 DirectInputにおける配列に対応するキーボードキーはDIK_から始まる定数で定義されています。例えば「A」キーならDIK_A、F2キーならDIK_F2という具合です。

 今回サンプルプログラムに使用したテクスチャはこれです。好みの画像に差し替えるのもよいでしょう。ちなみにこのCGも「六角大王Super 5.5」のサンプルデータを使って作成されています。


ship.png

ship.h
// 戦闘機・ミサイル共通クラス
class CCharactor : public CGameObject
{
protected:
	CSprite sprite;
	CTexture *tex;
public:
	float x, y;
};

// 戦闘機クラス
class CShip : public CCharactor
{
private:
	CInput *input;
protected:
	void Init();
	void Exec();
};

// ミサイルクラス
class CShoot : public CCharactor
{
protected:
	void Init();
	void Exec();
};

ship.cpp
void CShip::Init()
{
	input = (CInput*)FindObject("input");

	sprite.SetTexture((CTexture*)FindItemBox("texture"));
	sprite.SetSpriteSize(64, 64);

	x = 320.0f;
	y = 240.0f;
}

void CShip::Exec()
{
	// VK_LEFTにあらず
	if(input->IsKeyDown(DIK_LEFT)){
		x -= 1.0f;
		sprite.SetFrame(0);
	}else if(input->IsKeyDown(DIK_RIGHT)){
		x += 1.0f;
		sprite.SetFrame(2);
	}else{
		sprite.SetFrame(1);
	}

	if(input->IsKeyDown(DIK_UP)) y -= 1.0f;
	if(input->IsKeyDown(DIK_DOWN)) y += 1.0f;

	if(input->IsKeyDown(DIK_SPACE)){
		// ミサイル発射
		AppendObject(new CShoot(), 900, true);
	}

	sprite.Draw(x, y);
}


void CShoot::Init()
{
	sprite.SetTexture((CTexture*)FindItemBox("texture"));
	sprite.SetSpriteSize(64, 64);
	sprite.SetFrame(3);

	// 戦闘機と同じ位置にミサイルを配置
	CShip* its = (CShip*)FindItemBox("ship");
	x = its->x;
	y = its->y;
}

void CShoot::Exec()
{
	y -= 5.0f;

	// 画面外へ出たら削除する
	if(y < -32.0f){
		RemoveObject(this);
		return;
	}

	sprite.Draw(x, y);
}

 ではCInputTestStartクラスをタスクリストに追加して実行してみましょう。中にはタスクリストをどこへ追加したらいいか忘れた人がいるかもしれませんので、ここにWinMain関数を含めたファイルをおいておきます。

 で、実行結果がこれです。

 ……移動は問題ないとして、ミサイルがスペースキーを押し続けている限り発射されるため、なんだかレーザーみたいです。そこでキーを押し続けても連射されないように修正することにしましょう。

 「今も直前も押されている状態だった」のであればキーを押し続けていることになるのですから、「直前が押されていない状態だった」のであれば、押し続けていないと判断できます(逆に離した瞬間は「直前まで押されていたが、今は押されていない」ことになる)。そこで、直前に押されたキーデータを格納する変数と、押した瞬間であるかどうかを取得する関数をCInputに追加することにします。

input.h
class CInput : public CGameObject
{
private:
	LPDIRECTINPUT8 pInput;

	LPDIRECTINPUTDEVICE8 pKeyDevice;
	BYTE keydata[256], lastkeydata[256];

public:
	/* 省略 */

	BOOL IsKeyDown(int key);
	BOOL IsKeyPressed(int key);
	BOOL IsKeyReleased(int key);

	/* 省略 */
};

input.cpp
void CInput::Exec()
{
	if(pKeyDevice){
		// 入力の受け付け開始
		pKeyDevice->Acquire();

		memcpy(lastkeydata, keydata, 256);
		pKeyDevice->GetDeviceState(256, keydata);
	}
}

BOOL CInput::IsKeyPressed(int key)
{
	if(pKeyDevice == NULL) return FALSE;

	// 現在押されていてかつ直前に押されていないときTRUEを返す
	if((keydata[key] & 0x80) && !(lastkeydata[key] & 0x80)) return TRUE;
	return FALSE;
}

BOOL CInput::IsKeyReleased(int key)
{
	if(pKeyDevice == NULL) return FALSE;

	// 現在押されておらずかつ直前には押されていたときTRUEを返す
	if(!(keydata[key] & 0x80) && (lastkeydata[key] & 0x80)) return TRUE;
	return FALSE;
}

 プログラムの追加ができたら、CITShip::Exec()の「if(input->IsKeyDown(DIK_SPACE))」部分を「if(input->IsKeyPressed(DIK_SPACE))」に変えた上で実行してみましょう。これで連射はできなくなっているはずです。