タスク管理システムを作ってみる

※この記事は2016年8月1日に投稿した記事をソースコード等が見やすいよう修正し、投稿し直した物です。

タスク管理システムとは。

タスク管理システムとはその名の通り、オブジェクトごとのタスクを管理するクラスです。

例えば、2Dのシューティングゲームを作る際には「自機」「敵」「双方の弾」「背景の動き」など、ゲームを構成するオブジェクトがありますが、そのオブジェクトの更新の順番や、描画の順番を動的に変更することにより、列挙型などで処理順番が決められて更新、描画し忘れの心配もなくなり、作業の効率を上げることが出来ます。(そもそも忘れなければいいd)

ちなみに、タスクの処理順の管理に「連結リスト」を使い、今回はその中で一番簡単な片方向リストを紹介します。

連結リスト - Wikipedia

とりあえず実装してみます。

まず最初に次のタスクと仕事の中身を呼び出すクラスを作ります

Task.h

class CTask
{
 //仕事のポインタ
 CToDo *node;

 //次のタスクのポインタ
 CTask *next;

 //この仕事の認識番号
 int id;

public:
 //コンストラクタ
 CTask();
 //タスクを前に追加する
 void SetTask(CToDo *pNode, int a_ID);

 //仕事の更新
 void Update()
 {
  node->Update();
 }
 //仕事の描画
 void Render()
 {
  node->Render();
 }
 //仕事を取得
 void SetNode(CToDo *pNode){ node = pNode; }
 //IDを取得
 void SetId(int tmp){ id = tmp; }
 //IDを返す
 int GetId(){ return id; }
 //次のタスクのポインタを返す
 CTask *GetNext(){ return next; }
 //次のタスクのポインタを取得
 void SetNext(CTask *tmp){ next = tmp; }

};

この「CToDo *node」が仕事内容のクラスです。

ではついでに仕事内容も作ります。
(CTaskクラスでCToDoのインスタンスを呼び出している部分があるため、CToDoはCTaskの上で定義しましょう。)


//仕事の中身を管理するクラス
Task.h

class CToDo
{
 //継承先でも使えるようにする
protected:
 //仮のデータ(ここでゲームオブジェクトを入れる)
 int num;

public:
 //コンストラクタ
 CToDo();
 //デストラクタ
 virtual ~CToDo(){};
 //更新
 virtual void Update();
 //描画
 virtual void Render();
 void SetNum(int tmp){ num = tmp; }

};

次にクラスで定義した関数の中身を作っていきます。

Task.cpp

#include"Task.h"
#include<iostream>
using namespace std;

//コンストラクタ
CToDo::CToDo()
{
 //番号初期化
 num = 0;
}

//更新
void CToDo::Update()
{
 cout << num << "番更新" << endl;
}
//描画
void CToDo::Render()
{
 cout << num << "番描画" << endl;
}
//コンストラクタ
CTask::CTask()
{
 //初期化
 node = NULL;
 next = NULL;
 id = 0;
}

//仕事を前に追加する
void CTask::SetTask(CToDo *pNode, int a_ID)
{
 //CTaskを動的確保
 CTask *task = new CTask();
 //IDを取得
 task->SetId(a_ID);
 //仕事を取得
 task->SetNode(pNode);
 /*元々次にあった仕事を生成した仕事の次にセットして生成したものを次の仕事にする*/
 task->SetNext(this->GetNext());
 this->SetNext(task);
}

これでタスク管理システムが完成しました。

ではちゃんと動くかの確認のため、実際に使用してみます。



main.cpp

 #define TODO_NUM 10
int main()
{
 //仕事の始まり
 CTask loopStart;
 //仕事の終わり
 CTask loopEnd;
 //終わりのIDを0にする
 loopEnd.SetId(0);
 //終わりを最初の次の仕事にセットする
 loopStart.SetNext(&loopEnd);
 //1~10の仕事を生成
 CToDo todo[TODO_NUM+1];
 for (int num = 1; num <= TODO_NUM; num++)
 {
  todo[num].SetNum(num);
  //仕事を入れていく
  loopStart.SetTask(&todo[num], num);
 }
 //更新
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  task->Update();
 }
 //描画
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  task->Render();
 }
 //削除
 //削除するためのポインタ宣言
 CTask *deleteTask = NULL;
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  delete deleteTask;
  deleteTask = task;
 }
 delete deleteTask;
 return 0;
}


実行すると10~1番の更新描画処理がされます。

これをループさせるとゲームループの完成です。

IDを取得する際に一緒に優先度も決めて後でソートすることにより動的な処理順番の管理が出来ます。

オブジェクト指向を意識してC++でシューティングゲームを作る(3)

前回の「オブジェクト指向を意識してC++でシューティングゲームを作る(2)」で自機の描画と動かす所までやりました。
今回は弾を発射する所をやりたいと思います。

やること

  1. Bulletクラスを作る
  2. 基底クラスを作って共通部分をまとめる
  3. 弾を決まった方向に飛ばす
  4. BulletManagerクラスで全ての弾の管理をできるようにする
  5. 弾の発射


Bulletクラスを作る

弾の描画と更新を管理するクラスを作りたいと思います。
ではcppとhファイルを作成します。
f:id:haina817:20160930134523p:plain

cppファイルのほうにDxlibとBullet.hをインクルードします。

Bullet.hの方でBulletクラスを作ります。

Bullet.h

#pragma once
//弾の管理
class CBullet
{
private:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	CBullet();
	~CBullet();
};

Bullet.cpp

CBullet::CBullet()
{
}
CBullet::~CBullet()
{
}

と、ここで前回のCPlayerクラスでも同じ部分があることにお気づきでしょうか?

CPlayer.h

#pragma once
class CManager;
class CPlayer
{
	CManager *manager;
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

この中のメンバー変数posとgraphicは自機と弾には共通の用途なので、今から1つにまとめたいと思います。


基底クラスを作って共通部分をまとめる

新しくObjectクラスを作るため、cppとhファイルを作成します。
f:id:haina817:20160930135248p:plain

Bulletと同じようにcppファイルのほうにDxlibとObject.hをインクルードします。

Objectクラスを作成します。
Object.h

#pragma once
//座標と画像を持つオブジェクトの基底クラス
class CObject
{
protected:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CObject(){};
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	virtual ~CObject(){};
	//描画(純粋仮想関数)
	virtual void Render() = 0;
	//更新(純粋仮想関数)
	virtual void Update() = 0;
};

ここで「純粋仮想関数」というものが出てきました。
これは簡単に言うと継承で書き換えるのを前提として作る関数です。
座標と画像データが同一な為にCObjectにまとめましたが、各オブジェクト毎に描画の仕様や更新の内容が違ってきます。なので、「更新と描画はするけど、内容は各クラスで違うよ」と明記するためにこれを書きます。派生先の子クラスでUpdateとRender関数を書かないとエラーが出るので書き忘れ防止にもなります。

これをCPlayerとCBulletに派生させていきます。
Player.hとBullet.hがインクルードされている全てのファイルにObject.hをインクルードします。
f:id:haina817:20160930142558p:plain



次にCPlayerクラスとCBulletクラスにCObjectクラスを継承します。
Player.h

class CPlayer:public CObject
{
	CManager *manager;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

Bulletはついでに更新描画も作成しておきます。
Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
public:
	CBullet();
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

Bullet.cpp

void CBullet::Update()
{
}
void CBullet::Render()
{
}


これで座標と画像データをCObjectクラスにまとめるとこができました。
次に弾を飛ばす処理の部分を制作します。

弾を決まった方向に飛ばす
今度は弾を指定した方向に飛ばす処理を書いていきます。
弾を飛ばす処理には色々ありますが、今回は簡単に方向ベクトルを設定し、その方向に毎フレーム移動していきます。

最初に方向ベクトルを取得する変数を作成します。

Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet();
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

そしてUpdate関数内で自分の今の座標を足せば移動処理は完成です。
Bullet.cpp

void CBullet::Update()
{
	pos = VAdd(pos, vPos);
}

次に進む方向ベクトルを入れる構造を作ります。
方向ベクトルを取得する時は基本的にインスタンス生成時なので、
コンストラクタに弾の発生位置と方向ベクトルを引数に持たせたいと思います。

Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet(VECTOR &position, VECTOR &vPosition);
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

Bullet.cpp

CBullet::CBullet(VECTOR &position, VECTOR &vPosition)
{
	pos = position;
	vPos = vPosition;
}

これで弾の動きの処理は完成です。
それではこの弾を複数出す時に一括で管理出来るように弾の管理クラスを作ります。

BulletManagerクラスで全ての弾の管理をできるようにする
では弾を一括で管理するクラスBulletManagerクラスを作ります。
まずcpp、hファイルを作ってください。

f:id:haina817:20161005155710p:plain

BulletManagerクラスを作り、その中にBulletのポインタ配列を作ります。
Bulletのポインタを定義するため、上にBulletクラスをプロトタイプ宣言しておきます。
BulletManager.h

class CBullet;
#pragma once
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
};

このままだとエラーが出るのでcppファイルでインクルードしてください。

f:id:haina817:20161005183800p:plain

そしてコンストラクタの中でポインタ配列を全てNULLにし、デストラクタで全て解放します。

BulletManager.cpp

CBulletManager::CBulletManager()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		bullet[num] = NULL;
	}

}
CBulletManager::~CBulletManager()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		delete bullet[num];
	}
}

NULLを入れることにより一斉にdeleteする際、NULLが入ったポインタは無視されるためエラーが出なくなります。

では最後に弾の発射部分を作ります。
BulletManagerクラスにShot関数を作ります。
BulletManager.h

class CBullet;
#pragma once
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
	void Shot(VECTOR &pos, VECTOR &vPos);
};


Shotの中身はNULLのbulletを探してそのポインタを使って動的確保するだけ。

BulletManager,cpp

void CBulletManager::Shot(VECTOR &pos, VECTOR &vPos)
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		if (bullet[num] == NULL)
		{
			bullet[num] = new CBullet(pos,vPos);
			break;
		}
	}
}

これで後はbulletのNULLでないポインタ変数を全て更新、描画すれば完成です。

BulletManager.h

class CBullet;
#pragma once
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];
public:
	CBulletManager();
	~CBulletManager();
	void Shot(VECTOR &pos, VECTOR &vPos);
	void Update();
	void Render();
};

BulletManager.cpp

void CBulletManager::Update()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		//NULLでない場合
		if (bullet[num] != NULL)
		{
			bullet[num]->Update();
		}
	}
}
void CBulletManager::Render()
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		//NULLでない場合
		if (bullet[num] != NULL)
		{
			bullet[num]->Render();
		}
	}
}


そういえば弾のグラフィックをまだ読み込んでいなかったので今から読み込もうと思います。
ですか、数がかなり多いのでここは一つ一つ読み込まず、読み込む回数は一回にして、弾は全て読み込んだ画像データのアドレスを参照して読み込むことにします。
Objectクラスのgraphicをポインタ変数にします。
Object.h

#pragma once

class CObject
{
protected:
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int *graphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CObject(){};
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	virtual ~CObject(){};
	//描画
	virtual void Render() = 0;
	//更新
	virtual void Update() = 0;
};


するとPlayerの画像を格納している部分と描画している部分がエラーを吐くので、最初にgraphicを動的確保し、実態を得たgraphic変数に画像を格納します。
動的確保したため、デストラクタを使って解放し、描画のところのgraphic変数をポインタ先にします。
ついでに弾の画像をPlayer自身に持たせたいため、弾の画像の格納も一緒にやります。
Player.h

#pragma once
class CManager;
class CPlayer:public CObject
{
	CManager *manager;
	//弾の画像
	int bulletGraphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

Player.cpp

CPlayer::CPlayer(CManager *pManager)
{
	//CManagerのアドレス格納
	manager = pManager;
	//画像データ格納
	graphic = new int;
	*graphic = LoadGraph("graphic/Player/player.png");
	bulletGraphic = LoadGraph("graphic/Bullet/playerBullet.png");
	//位置を初期化
	pos = VGet(320,240,0);
}
CPlayer::~CPlayer()
{
	delete graphic;
}
void CPlayer::Render()
{
	// 読みこんだグラフィックを回転描画
	DrawRotaGraph(pos.x, pos.y, 1.0, 0, *graphic, TRUE);
}

これでエラーが消えたと思います。
次にBulletクラスのコンストラクタの引数にint型のポインタ変数を定義します
Bullet.h

#pragma once
//弾の管理
class CBullet:public CObject
{
	//進む方向ベクトル
	VECTOR vPos;
public:
	CBullet();
	CBullet(int *tex,VECTOR &position, VECTOR &vPosition);
	~CBullet();
	//描画
	void Render();
	//更新
	void Update();
};

そしてgraphicに代入し、Render関数で描画します。

CBullet::CBullet(int *tex,VECTOR &position, VECTOR &vPosition)
{
	graphic = tex;
	pos = position;
	vPos = vPosition;
}
void CBullet::Render()
{
	// 読みこんだグラフィックを回転描画
	DrawRotaGraph(pos.x, pos.y, 1.0, 0, *graphic, TRUE);
}

BulletManagerのShot関数で弾を生成するためこちらにも引数にint型のポインタ変数を入れてBulletのコンストラクタの引数に渡します。
BulletManager.h

#pragma once
class CBullet;
//弾の数
#define BULLET_NUM 100
class CBulletManager
{
	CBullet *bullet[BULLET_NUM];

public:
	CBulletManager();
	~CBulletManager();
	void Shot(int *tex, VECTOR &pos, VECTOR &vPos);
	void Update();
	void Render();
};

BulletManager.cpp

void CBulletManager::Shot(int *tex,VECTOR &pos, VECTOR &vPos)
{
	for (int num = 0; num < BULLET_NUM; num++)
	{
		if (bullet[num] == NULL)
		{
			bullet[num] = new CBullet(tex,pos,vPos);
			break;
		}
	}
}

では最後に弾の発射をして終わりたいと思います。

弾の発射

弾の発射はPlayerで行います。

PlayerクラスにBulletManagerクラスのポインタ変数と、アドレスを取得する関数を作ります。

Player.h

#pragma once
class CManager;
//これがないとエラーが出る
class CBulletManager;
class CPlayer:public CObject
{
	CManager *manager;
	//弾の画像
	int bulletGraphic;
	CBulletManager *bulletManager;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
	//bulletManagerのアドレスを取得
	void SetBulletManager(CBulletManager *bullet){ bulletManager = bullet; }
};

cppファイルの上にBulletManagerをインクルードします。

f:id:haina817:20161005221809p:plain

更新の関数の所で発射部分を作ります。
Player.cpp

void CPlayer::Update()
{
	//移動する向き
	VECTOR vPos = VGet(0, 0, 0);
	//弾の発射を抑制するトリガー
	static int cnt = 0;
	//移動していないときは正規化する必要がないのでこれで移動しているか確認する
	bool moveFlag = false;
	if (manager->GetKey()[KEY_INPUT_LEFT] > 0)
	{
		vPos.x -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_RIGHT] > 0)
	{
		vPos.x += 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_UP] > 0)
	{
		vPos.y -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_DOWN] > 0)
	{
		vPos.y += 1.0f;
		moveFlag = true;
	}
	if (moveFlag)
	{
		vPos = VNorm(vPos);
		vPos = VScale(vPos, PLAYER_SPEED);
		//現在の座標に移動量を加算する
		pos = VAdd(pos, vPos);
	}
	//弾の発射
	if (manager->GetKey()[KEY_INPUT_SPACE] > 0)
	{
		cnt++;
	}
	else
	{
		cnt = 0;
	}
	if ((cnt % 10) == 1)
	{
		//上に弾を飛ばす
		bulletManager->Shot(&bulletGraphic, pos, VGet(0, -1, 0));
	}
}

最後にGameクラスでBulletManagerを動的確保と更新描画、Playerクラスにアドレスを取得させれば完成です。
Game.h

#pragma once
class CPlayer;
class CBulletManager;
class CGame:public CScene
{
	//自機のポインタ変数
	CPlayer *player;
	//弾の全体管理しているクラスのポインタ変数
	CBulletManager *bulletManage;
public:
	CGame(CManager *pManager);
	~CGame();
	void Update();
	void Render();
};

BulletManagerのインクルードを忘れずに。

Game.cpp

#include"DxLib.h"
#include"Manager.h"
#include"Game.h"
#include"Object.h"
#include"Player.h"
#include"BulletManager.h"

CGame::CGame(CManager *pManager) : CScene(pManager)
{
	//プレイヤーを動的確保
	player = new CPlayer(pManager);
	//弾の全体管理しているクラスを動的確保
	bulletManage = new CBulletManager();
	//CBulletManagerクラスのアドレス取得
	player->SetBulletManager(bulletManage);
}
CGame::~CGame()
{
	//動的確保したものを解放する
	delete player;
	delete bulletManage;
}
void CGame::Update()
{
	player->Update();
	bulletManage->Update();
	
}
void CGame::Render()
{
	player->Render();
	bulletManage->Render();
}

それでは実行してみましょう。


f:id:haina817:20161005223558p:plain

こんな感じで弾を飛ばすことに成功しました。

今回はスクショするために遅くしましたが

//弾の発射
	if ((cnt % 10) == 1)
	{
		//上に弾を飛ばす
		bulletManager->Shot(&bulletGraphic, pos, VGet(0, -1, 0));
	}

のVGet(0,-1,0)の部分をもう少し大きくすればもっと速い球が出せるようになります。

今回はここまでにします。

このままでは弾が100発しか出せないようになっているので、次はいらなくなった弾の解放からやりたいと思います。

次回(オブジェクト指向を意識してC++でシューティングゲームを作る(4) - hainaのゲーム制作日記

※追記。
キー取得の関数gpUpdateKeyに不具合があったので修正いたしました

//キーの入力状態を更新する
int gpUpdateKey(char *key){
	//現在のキーの入力状態を格納する
	char tmpKey[KEY_NUM];
	//全てのキーの入力状態を得る
	GetHitKeyStateAll(tmpKey);
	for (int i = 0; i<KEY_NUM; i++){
		if (tmpKey[i] != 0)
		{//i番のキーコードに対応するキーが押されていたら加算
			key[i]++;
		}
		else 
		{//押されていなければ0にする
			key[i] = 0;
		}
	}
	return 0;
}

//キーの入力状態を更新する
int gpUpdateKey(char *key){
	//現在のキーの入力状態を格納する
	char tmpKey[KEY_NUM];
	//全てのキーの入力状態を得る
	GetHitKeyStateAll(tmpKey);
	for (int i = 0; i<KEY_NUM; i++){
		if (tmpKey[i] != 0)
		{//i番のキーコードに対応するキーが押されていたら加算
			if (key[i] == 120)
			{
				key[i] = 0;
			}
			key[i]++;
			
		}
		else 
		{//押されていなければ0にする
			key[i] = 0;
		}
	}
	return 0;
}

オブジェクト指向を意識してC++でシューティングゲームを作る(2)

前の投稿でゲームループとシーン切り替えクラスを作ったので、今回はいよいよ自機を描画する所までやりたいと思います。

前回はこちらオブジェクト指向を意識してC++でシューティングゲームを作る(1)

やること

  1. Playerクラスを作る
  2. 自機の描画
  3. 自機を動かす

Playerクラスを作る


最初にPlayerクラスを作るためにcppとhファイルを作成します
f:id:haina817:20160924215441p:plain

playerクラスでdxライブラリを使用するので、先にcppファイル内でDxlibとplayerをincludeしてください。
(そういえばDXライブラリの環境構築に全く触れていなかったので、もしまだの方はこちらから環境構築を行ってください。)

f:id:haina817:20160924215926p:plain

次にPlayer.cppにプレイヤークラスを作り、ここでプレイヤーの描画には欠かせない位置情報とプレイヤーの画像を格納する変数をそれぞれ宣言します。
Player.h

#pragma once

class CPlayer
{
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer();
//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
};

cppファイルではCPlayerのコンストラクタを使ってgraphicとposの初期化を行います。

Player.cpp

CPlayer::CPlayer()
{
	//画像データ格納
	graphic = LoadGraph("画像のパス");
	//位置を初期化(座標はウインドウの中心)
	pos = VGet(320,240,0);
}
CPlayer::~CPlayer()
{
}

使用した画像はこちら(手抜きではない)
f:id:haina817:20160924230400p:plain
これで自機の画像データが読み込まれ、描画する準備が整いました。
では実際に描画してみましょう。

自機の描画

CPlayerクラスにRender関数を作ってそこで描画します。

CPlayer.h

#pragma once

class CPlayer
{
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer();
//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
//更新
	void Render();
};

Player.cpp

void CPlayer::Render()
{
	//読みこんだグラフィックを回転描画
	DrawRotaGraph(pos.x, pos.y, 1.0, 0, graphic, TRUE);
}

DrawRotaGraph関数説明
CPlayerのインスタンスはCGameクラスで動的確保します。
どのようにするかと言うと、まずCGameクラスの上にCPlayerクラスのプロトタイプ宣言をしてCPlayerクラスのポインタ変数を作り、
CGameクラス内のコンストラクタとデストラクタを、cppファイルに移動します。

CGame.h

#pragma once
class CPlayer;
class CGame:public CScene
{
	//自機のポインタ変数
	CPlayer *player;
public:
	CGame(CManager *pManager);
	~CGame();
	void Update();
	void Render();
};

Game.cpp

CGame::CGame(CManager *pManager) : CScene(pManager)
{
}
CGame::~CGame()
{
}

そしてPlayer.hをincludeした後、CGameのコンストラクタの中でCPlayerを動的確保(new)し、デストラクタの中でそれを解放して、最後にRender関数内でplayerのRenderを呼べば自機の描画が完成です。

Game.cpp

#include"Player.h"
CGame::CGame(CManager *pManager) : CScene(pManager)
{
	//プレイヤーを動的確保
	player = new CPlayer();
}
CGame::~CGame()
{
	//動的確保したものを解放する
	delete player;
}
void CGame::Render()
{
	//DrawFormatString(0, 0, GetColor(255, 255, 255), "ゲーム");
	player->Render();
}

起動してみるとこんな感じ

f:id:haina817:20160925002433p:plain

自機を動かす
最後に自機を動かす処理をして今回は終わります。
やることは、オブジェクト指向を意識してC++でシューティングゲームを作る(1)でCManagerクラスの中で作ったキー取得関数「GetKey」をCPlayer内でも使えるようにして、CPlayerの更新関数内で自機を動かす処理をします。
まず最初に、CManagerクラスのポインタを格納する変数をCPlayerクラスに作成してそれをコンストラクタの引数で取得できるようにします。
あとついでに更新関数も作っておきましょう。

Player.h

#pragma once
class CManager;
class CPlayer
{
	CManager *manager;
	//DXライブラリで定義されている構造体(中身はfloat型のx,y,z)
	VECTOR pos;
	//画像データ格納
	int graphic;
public:
	//コンストラクタ(インスタンス生成時に最初に呼ばれる関数)
	CPlayer(CManager *);
	//デストラクタ(インスタンス削除時に呼ばれる関数)
	~CPlayer();
	//描画
	void Render();
	//更新
	void Update();
};

Player.cpp

CPlayer::CPlayer(CManager *pManager)
{
	//CManagerのアドレス格納
	manager = pManager;
	//画像データ格納
	graphic = LoadGraph("graphic/Player/player.png");
	//位置を初期化
	pos = VGet(320,240,0);
}


では、いよいよキー入力で自機を動かす部分を作ります。
まずCManagerクラスを使用するので、Managerをincludeするのと、自機の移動量を決めて上に定義(define)しておきましょう。
次に上下左右キーが押されているか判定する条件文を作成します。
そしてベクトル取得する変数を定義して、4方向の移動量を取得してから現在の座標に加算します。

Player.cpp

#include"DxLib.h"
#include"Player.h"
#include"Manager.h"
#define PLAYER_SPEED 10.0f


void CPlayer::Update()
{
	//移動する向き
	VECTOR vPos = VGet(0,0,0);
	//移動していないときは座標を動かす必要がないのでこれで移動しているか確認する
	bool moveFlag = false;
	if (manager->GetKey()[KEY_INPUT_LEFT] > 0)
	{
		vPos.x -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_RIGHT] > 0)
	{
		vPos.x += 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_UP] > 0)
	{
		vPos.y -= 1.0f;
		moveFlag = true;
	}
	if (manager->GetKey()[KEY_INPUT_DOWN] > 0)
	{
		vPos.y += 1.0f;
		moveFlag = true;
	}
	if (moveFlag)
	{
		vPos = VNorm(vPos);
		vPos = VScale(vPos, PLAYER_SPEED);
		//現在の座標に移動量を加算する
		pos = VAdd(pos, vPos);
	}
	
}

これで後はこのUpdate関数をCGameのUpdateで呼び出せば完成です。

CGame.cpp

CGame::CGame(CManager *pManager) : CScene(pManager)
{
	//プレイヤーを動的確保
	player = new CPlayer(pManager);
}
void CGame::Update()
{
	player->Update();
}

では実行して移動できるか試してみましょう。
f:id:haina817:20160925002647p:plain
f:id:haina817:20160925002753p:plain

しっかり移動できました。


一応参考になるかと思い私自身がプログラムを書く流れをそのまま書いているのですが少し長くなってしまいますね...
本当は弾の発射まで行きたかったのですがそれはオブジェクト指向を意識してC++でシューティングゲームを作る(3)で説明することにします。

オブジェクト指向を意識してC++でシューティングゲームを作る(1)

使うのはVS2013とDXライブラリ、使用言語はC++です。

 今回は次のことをします。

  1. ゲームループ作成。
  2. タイトル、ゲームのシーン切り替え部分作成。

※Manager.hの所のコンストラクタとデストラクタの記述が抜けていたので追記しました。2016/10/5

ゲームループ作成。

まず最初にゲームループを作ります。

#include"DxLib.h"

#define KEY_NUM 256
//キーの入力状態を更新する
int gpUpdateKey(char *key){
 //現在のキーの入力状態を格納する
 char tmpKey[KEY_NUM];
 //全てのキーの入力状態を得る
 GetHitKeyStateAll(tmpKey);
 for (int i = 0; i<KEY_NUM; i++){
  if (tmpKey[i] != 0)
  {//i番のキーコードに対応するキーが押されていたら加算
  key[i]++;
  }
  else
  {//押されていなければ0にする
  key[i] = 0;
  }
 }
 return 0;
}
bool Process(char *key) {
 if (ProcessMessage() != 0) return false;
 //画面の裏ページの内容を表ページに反映する
 if (ScreenFlip() != 0) return false;
 //画面を初期化
 if (ClearDrawScreen() != 0) return false;
 //キー取得
 if (gpUpdateKey(key) != 0) return false;
 //エスケープで終了
 if (key[KEY_INPUT_ESCAPE] >= 1) return false;
 return true;
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
 //ウインドウモードに変更
 ChangeWindowMode(TRUE);
 //DXライブラリを初期化する。
 if (DxLib_Init() == -1) return -1;
 //キー取得
 char key[KEY_NUM];
 //描画先画面を裏にする
 SetDrawScreen(DX_SCREEN_BACK);
 while (Process(key))
 {
  //ゲームループ

 }
 //DXライブラリを終了する。
 DxLib_End();

 return 0;
}

参考サイト(新・C言語 ~ゲームプログラミングの館~ [DXライブラリ]


ここまではどんなゲームを作るときも同じ流れになると思うので説明は割愛します。

ここのゲームループのところでゲームの更新描画をします。

 

シーン切り替え。

次に全体のシーンの管理クラスを作ります。

Manager.h

#pragma once

class CManager;
//シーンの基底クラス
class CScene
{
protected:
CManager *manager;
public:
CScene(CManager *pManager){ manager = pManager; };
virtual ~CScene(){};
//描画
virtual void Update(){};
//更新
virtual void Render(){};
};
//シーン管理クラス
class CManager
{
//キー
char *key;
public:
//今のシーンのポインタ
CScene *scene;
CManager(char *pKey){ key = pKey; };
~CManager(){ delete scene; };
char *GetKey(){ return key; }
//描画
void Update(){ scene->Update(); };
//更新
void Render(){ scene->Render(); };

};

これがシーン管理クラスです。

使い方は、CManagerクラスの中の「CScene *scene」をタイトルやゲームシーンの切り替わりの際に解放(delete)動的確保(new)してシーンを切り替えます。

では試しにタイトルからゲームのシーン切り替えをしてみます。

Title.h

#pragma once

//タイトル全体の管理
class CTitle:public CScene
{
 public:
 CTitle(CManager *pManager):CScene(pManager){};
 ~CTitle(){};
 void Update();
 void Render();
};

Title.cpp

#include"DxLib.h"
#include"Manager.h"
#include"Title.h"
#include"Game.h"
void CTitle::Update()
{
 if (manager->GetKey()[KEY_INPUT_X] >= 1)
 {
  //ゲームにシーン移行
  manager->scene = new CGame(manager);
  delete this;
 }

}

void CTitle::Render()
{
 DrawFormatString(0, 0, GetColor(255, 255, 255), "タイトル");
}

Game.h

#pragma once

class CGame:public CScene
{
 public:
 CGame(CManager *pManager) :CScene(pManager){};
 ~CGame(){};
 void Update();
 void Render();
};

Game.cpp

#include"DxLib.h"
#include"Manager.h"
#include"Game.h"

void CGame::Update(){
}
void CGame::Render()
{
 DrawFormatString(0, 0, GetColor(255, 255, 255), "ゲーム");
}

main.cpp

#include"Manager.h"
#include"Title.h"

 

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
//ウインドウモードに変更
ChangeWindowMode(TRUE);
//DXライブラリを初期化する。
if (DxLib_Init() == -1) return -1;
//キー取得
char key[KEY_NUM];
//描画先画面を裏にする
SetDrawScreen(DX_SCREEN_BACK);
//管理システムを動的確保
CManager *manager;
manager = new CManager(key);
//タイトルを動的確保
manager->scene = new CTitle(manager);
while (Process(key))
{
 //ゲームループ
 manager->Render();
 manager->Update();
}
//後始末
delete manager;
//DXライブラリを終了する。
DxLib_End();

return 0;
}

 これで実行すると

f:id:haina817:20160920011038p:plain

最初にタイトル。

そしてXを押すと。

f:id:haina817:20160920011110p:plain

 このようにシーンを切り替えることに成功しました。

本日はここまでにします。

この続きはオブジェクト指向を意識してC++でシューティングゲームを作る(2)に書きます。

 

 

 

 

Unityでandroidのビルド

Unityを使いだしてふと、自分のスマホに自作ゲームを入れてみたいと思いやってみたがかなり苦戦したので自分用にメモ。

 

Unity以外で必要なものは。

Android SDK

(Download Android Studio and SDK Tools | Android Studio)

JDK

(Java SE Development Kit 8 - Downloads)

のこの二つ。

 

インストールが出来たらUnityのメニューバーから[Edit]→[Preference]を開いて、External Toolに切り替え、SDKJDKのパスを指定します。

 

次にUnityのメニューバーから[File]→[Build Settings]を開いて、PlatformをAndroidに変更します。

f:id:haina817:20160912220457p:plain

そして[Player Settings...]のボタンをクリックしてこのウインドウを出します。

f:id:haina817:20160912215717p:plain

このOther Sttingsという欄を開くと、Bundle Identifierという項目がるので、ここを書き換えてからBuild Settingsの[Build]ボタンを押すとapkが作成されます。

(私のプロジェクトの場合Bundle Identifierは「com.test.game」になります)

 

タスク管理システムの初歩

 

タスク管理システムとは。

タスク管理システムとはその名の通り、オブジェクトごとのタスクを管理するクラスです。

例えば、2Dのシューティングゲームを作る際には「自機」「敵」「双方の弾」「背景の動き」など、ゲームを構成するオブジェクトがありますが、そのオブジェクトの更新の順番や、描画の順番を動的に変更することにより、列挙型などで処理順番が決められて更新、描画し忘れの心配もなくなり、作業の効率を上げることが出来ます。そもそも忘れなければいいd

ちなみに、タスクの処理順の管理に「連結リスト」を使い、今回はその中で一番簡単な片方向リストを紹介します。

連結リスト - Wikipedia

とりあえず実装してみます。

まず最初に次のタスクと仕事の中身を呼び出すクラスを作ります

class CTask
{
 //仕事のポインタ
 CToDo *node;

 //次のタスクのポインタ
 CTask *next;

 //この仕事の認識番号
 int id;

public:
 //コンストラクタ
 CTask();
 //タスクを前に追加する
 void SetTask(CToDo *pNode, int a_ID);

 //仕事の更新
 void Update()
 {
  node->Update();
 }
 //仕事の描画
 void Render()
 {
  node->Render();
 }
 //仕事を取得
 void SetNode(CToDo *pNode){ node = pNode; }
 //IDを取得
 void SetId(int tmp){ id = tmp; }
 //IDを返す
 int GetId(){ return id; }
 //次のタスクのポインタを返す
 CTask *GetNext(){ return next; }
 //次のタスクのポインタを取得
 void SetNext(CTask *tmp){ next = tmp; }

};

 この「CToDo *node」が仕事内容のクラスです。

ではついでに仕事内容も作ります。

//仕事の中身を管理するクラス

class CToDo
{
 //継承先でも使えるようにする
protected:
 //仮のデータ(ここでゲームオブジェクトを入れる)
 int num;

public:
 //コンストラクタ
 CToDo();
 //デストラクタ
 virtual ~CToDo(){};
 //更新
 virtual void Update();
 //描画
 virtual void Render();
 void SetNum(int tmp){ num = tmp; }

};

 次にクラスで定義した関数の中身を作っていきます。

#include"Task.h"
#include<iostream>
using namespace std;

//コンストラクタ
CToDo::CToDo()
{
 //番号初期化
 num = 0;
}

//更新
void CToDo::Update()
{
 cout << num << "番更新" << endl;
}
//描画
void CToDo::Render()
{
 cout << num << "番描画" << endl;
}
//コンストラクタ
CTask::CTask()
{
 //初期化
 node = NULL;
 next = NULL;
 id = 0;
}

//仕事を前に追加する
void CTask::SetTask(CToDo *pNode, int a_ID)
{
 //CTaskを動的確保
 CTask *task = new CTask();
 //IDを取得
 task->SetId(a_ID);
 //仕事を取得
 task->SetNode(pNode);
 /*元々次にあった仕事を生成した仕事の次にセットして生成したものを次の仕事にする*/
 task->SetNext(this->GetNext());
 this->SetNext(task);
}

 これでタスク管理システムが完成しました。

ではメインを書いていきましょう。

 #define TODO_NUM 10
int main()
{
 //仕事の始まり
 CTask loopStart;
 //仕事の終わり
 CTask loopEnd;
 //終わりのIDを0にする
 loopEnd.SetId(0);
 //終わりを最初の次の仕事にセットする
 loopStart.SetNext(&loopEnd);
 //1~10の仕事を生成
 CToDo todo[TODO_NUM+1];
 for (int num = 1; num <= TODO_NUM; num++)
 {
  todo[num].SetNum(num);
  //仕事を入れていく
  loopStart.SetTask(&todo[num], num);
 }
 //更新
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  task->Update();
 }
 //描画
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  task->Render();
 }
 //削除
 //削除するためのポインタ宣言
 CTask *deleteTask = NULL;
 for (//スタートの次の仕事のポインタを取得
  CTask *task = loopStart.GetNext();
  //loopEndと同じIDで無いかチェック
  task->GetId() != loopEnd.GetId();
  //次の仕事のポインタ取得
  task = task->GetNext())
 {
  delete deleteTask;
  deleteTask = task;
 }
 delete deleteTask;
 return 0;
}

 これで実装完了です。

実行すると10~1番の更新描画処理がされます。

これをループさせるとゲームループの完成です。

IDを取得する際に一緒に優先度も決めて後でソートすることにより動的な処理順番の管理が出来ます。

 

 

ポインタ解説

 

今回はC言語の鬼門と言われている「ポインタ」について書きます。

C言語を勉強していると必ずと言っていいほどつまずいてしまうポインタ。
それを自分なりにわかりやすい教え方を思いついたのでメモ。

まず、ポインタ変数と変数の宣言はこう。

//変数 

a   = 10;

b   = 20;

c   = 30; 

//ポインタ変数
int *pA;

int *pB;

int *pC;

//各変数のアドレスをポインタ変数に格納

pA = &a;

pB = &b;

pC = &c;

 その実行結果はこれ

 < 変数 >

そのまま     : a = 10 , b = 20 , , c = 30

&つけた場合    : &a = 00C9FB64 , &b = 00C9FB58 , &c = 00C9FB4C

< ポインタ >

*ある場合   : *pA = 10 , *pB = 20 , *pC = 30

*抜いた場合  : pA = 00C9FB64, pB = 00C9FB58, pC = 00C9FB4C

&つけた場合 : &pA = 00C9FA4C, &pB = 00C9FA50, &pC = 00C9FA54

 ここで注目してほしいのはここ。
「変数の&をつけた場合とポインタの*を抜いた場合の出力。」
同じ値になっていますよね。
これはアドレスと言って実態があるオブジェクトの位置情報。
(私はこれを電話番号みたいな物と解釈していますが)
これでわかるのは、

変数の場合。

&をつけるとアドレス。

何もつけないと実態が呼ばれる。

 

ポインタ変数の場合。

何もつけないとアドレスが呼ばれる。

*をつけるとそのアドレスの先の実態が呼ばれる。

 と言うこと。←ここ重要

 

ではこれがどのような意味を持つか見てみましょう。

a += 100; b += 100; c += 100;

< 変数 >

そのまま   : a = 110 , b = 120 , c = 130

&つけた場合  : &a = 0033FA24 , &b = 0033FA18 , &c = 0033FA0C

< ポインタ >

*ある場合   : *pA = 110 , *pB = 120 , *pC = 130

*抜いた場合  : pA = 0033FA24, pB = 0033FA18, pC = 0033FA0C

&つけた場合 : &pA = 0033F90C, &pB = 0033F910, &pC = 0033F914

 各変数を+100した後の出力です。

この時、変数の値だけでなくポインタの*ある場合も値が変わっています。

次にポインタ変数で弄った場合。

*pA -= 100; *pB -= 100; *pC -= 100;

< 変数 >

そのまま      : a = 10 , b = 20 , c = 30

&つけた場合  : &a = 0033FA24 , &b = 0033FA18 , &c = 0033FA0C

< ポインタ >

*ある場合     : *pA = 10 , *pB = 20 , *pC = 30

*抜いた場合    : pA = 0033FA24, pB = 0033FA18, pC = 0033FA0C

&つけた場合   : &pA = 0033F90C, &pB = 0033F910, &pC = 0033F914

 各ポインタ変数のアドレス先にー100した後の出力です。

この時も各変数の値が変わっています。

つまり、ポインタとは格納したアドレス先の変数を自由に参照できるもの。

無茶をすればこうやっても値を出せます「 *&*&a = 10」

ポインタを使うとこんなことをするのも楽にできます。

int num[10];
int *pNum = num;
for (int i = 0; i < 10; i++)
{
*pNum = i;

//アドレスの参照先をpNumの大きさ分移動する
pNum++;
}
for (int i = 0; i < 10; i++)
{
cout << num[i] << endl;

}

<実行結果>

0

1

2

3

4

5

6

7

8

9

 

ちなみに、以前友人に聞いた話でこんなことも出来るらしいです。

int a = 0x11111111;

short test[2];

test[0] = ((short *)&a)[0];
test[1] = ((short *)&a)[1];

<実行結果>

4369

4369

4369は16進数で0x1111です。

最初何言ってんだこいつ...って思いました(笑)
まぁ実際は4バイトのデータをメモリ上に確保して、それを2バイトのポインタ変数で参照して格納しているってだけなんだけどね。
(こんなのどこで使うんだろ...)

ポインタは最初こそ躓きやすいですが、基礎をしっかり勉強して数をこなして慣れていけばすごく便利なものだと思います。

 

それではまた。
最後までお付き合いいただき、ありがとうございました。