弾幕STGにおける弾の描画順序について

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
アバター
Hiragi(GKUTH)
記事: 167
登録日時: 13年前
住所: 大阪府
連絡を取る:

弾幕STGにおける弾の描画順序について

#1

投稿記事 by Hiragi(GKUTH) » 8年前

いつもお世話になっております。

現在C++ DxLib ver 3.16bを用いて弾幕STGを作成しており、
その中で敵弾の登録、及び描画の際の描画順序に関して悩んでおります。

開発環境
VS Community 2015

具体的に言うと、大量に弾幕が描画され、弾同士が重なる場合、通常あとに発射された弾が後ろにあるように見えるのが普通であり、
ほとんどのSTGにおいてそうなっているかと思いますが、現在の弾の登録の実装方法だと先頭からフラグが立っていない要素を探し
初期化する方法をとっているため、弾が画面外に出て削除された場合に弾の描画順序が前後することがあります。
これらの管理方法として調べた所、Zバッファを用いた実装がなされている場合もあるようですが、アルファブレンド時に相性が悪いとの情報を目にしたりと、何か他の実装方法はないかと思っております。 フレームごとに配列を先頭方向に詰める、弾ごとにカウンタを持たせて
カウンタの値が小さい順に描画する...などいろいろと考えてみたのですがいずれにしても捜索回数が増えて負荷が大きくなってしまいそう(?)ですし、私が思いつく程度の実装方法よりもっと効率的な実装方法が無いものかと悩んでおります。
今までSTGを設計された方はどのように実装したのでしょうか、実例などを教えていただきたく、質問させていただきました。

以下に弾の登録、及び描画部分のソースコードを提示します。

Enemy.cpp Enter_shot関数

コード:

	//パターン2 自機狙い弾
		case 2:
		{
				//2フレームに一回、200フレーム間に
			if (s_cnt % 2 == 0 && s_cnt <= 200)
			{
				for (int i = 0;i < ENEMY_SHOT_MAX;++i)
				{
					if (shot[i].flag == false)
					{
						shot[i].flag = true;
						shot[i].refflag = false;
						shot[i].x = this->x;
						shot[i].y = this->y;
						shot[i].ang = this->ang;
						break;
					}
				}
			}
		}
Enemy.cpp Draw関数

コード:

for(int i = 0;i < ENEMY_SHOT_MAX;++i)
	{
		if (shot[i].flag)
		{
			DrawRotaGraphF(shot[i].x,shot[i].y,1.0f,0.0f,shot[i].shothandle,true,false);
		}
	}

追記:弾は画面外に出たフレームで削除されます。
だいがくせい!

yuni

Re: 弾幕STGにおける弾の描画順序について

#2

投稿記事 by yuni » 8年前

こんばんは。

このままのソースでは結局配列の先頭から順番に描画すると思うので、
Zバッファを用いた実装 というのを使っても解決は難しいのでは…?と思います。
というかZバッファを用いる、という事を誤解してらっしゃるような気もします。

拝見した条件を満たした上で一番簡単そうなのは、
「弾の情報の配列」とは別に、
std::listを利用して(もしくはリンクリストを自作して)「弾の情報のポインタ型のリスト」を作り、
フラグが立つタイミングでリストの末尾に追加、
フラグが下りるタイミングでリストから削除していく方法かな、と思います。

フラグが立ったものだけをポインタで管理するリストなので、
更新も、当たり判定も、描画も、このリストの先頭から末尾まで回せばいいですし、
そうするとそもそもフラグという変数も不要になる(かも)と思います。

※イメージ
[shot型ポインタのリスト]
  • &shot[3]
  • &shot[10]
  • &shot[16]
  • &shot[5]
↑このリストをループで回せばshot配列の 3, 10, 16、5 しか処理しないし、追加した順に描画出来る。

std::listやリンクリストというのがどういうものかは
調べたうえで検討してもらえればと思います。
オフトピック
後から発射したものが後ろにあるように見える(塗りつぶされてしまう?)のなら
描画時には末尾から先頭に向かってループしないといけないとは思いますが…
まぁ、これはループの方向さえ逆にすればいいので些細な事ですね。

アバター
Hiragi(GKUTH)
記事: 167
登録日時: 13年前
住所: 大阪府
連絡を取る:

Re: 弾幕STGにおける弾の描画順序について

#3

投稿記事 by Hiragi(GKUTH) » 8年前

回答ありがとうございます。

 まずZバッファに関しては、弾の登録時間(後から追加)ごとにZ座標を与えてやれば、先頭から描画したとしても、後の方から登録された弾のほうが前面
に描画されるのでは、という意味で書き込みました、が、ご指摘通りおそらくZバッファに関する知識が曖昧なので今回は別の方法を採用したいと思います。

 配列とは別にstd::listを用いた方法、とのことですが「フラグが降りるタイミングでリストから削除」とありますが、その時点で削除された部分は空白になるのでしょうか?
あるいは自動的に先頭、または末端に詰められるのでしょうか...? リストにも順番が存在するとすれば、やはりリストの内容が削除された時点で、そこが空白となり
次の弾の登録に使用した配列へのポインタは空白となった所に登録され、結局先頭から回すので描画順の問題は解決されないのでは...と思うのですが。
国語力が無く説明できていないかもしれませんが一応説明させていただきました。

少し調べた所、任意のノードが削除された時(eraseを実行した時)任意のノードの前後のポインタの情報が書き換わり削除されたノードを飛ばすようになるということなので
上記の問題は発生しない(?)事がわかりました。 そこで、わざわざ弾の情報への配列のポインタをリストにするのではなく、弾の情報自体をリストにするのではいけないのでしょうか?
何かしら理由があるのであれば教えていただきたいです。


 その後自身で考えた方法では、敵が最後の弾を発射し、最後の弾が画面外に出た瞬間に全てのデータを削除する
最後の弾以外が画面外に出た場合は画面外に出たというフラグを立て、計算、描画諸々をせず、弾が存在する(弾の登録時に使用するフラグ)は立てたままにする、という方法です。

大量のものを管理するのでstd::listのほうが高速なのではないか(上記の管理方法だと敵が最後の弾を画面外に出すまでループの回数が変わらない)と思うので、
std::listを用いた設計にしてみます。

 
だいがくせい!

アバター
Dixq (管理人)
管理人
記事: 1661
登録日時: 13年前
住所: 北海道札幌市
連絡を取る:

Re: 弾幕STGにおける弾の描画順序について

#4

投稿記事 by Dixq (管理人) » 8年前

私はstd::listを用いて、大きさ順にソートして描画しています。

コード:

bool comp(Bullet* v1, Bullet* v2) {
	if (BULLET_SORT[v1->dat.type] > BULLET_SORT[v2->dat.type]) {
		return true;
	}
	else {
		return false;
	}
}

bool Barrage::update()
{
... (略) ...
	_list.sort(comp);
	return true;
}
必ずしも大きい方が下に来た方がいいとは限らないので、ソートしたい順序という物を決めています。
Bulletにtypeを指定しておき、そのtypeが何番目に表示されたらいいのかという情報を元にソートします。

しかしよく考えたら毎フレーム全体をソートする必要はなく、
listにpush_backするのではなく、弾をリストに登録時、表示したい階層の位置に挿入してやればソートの必要はないんですよね。
まぁ5万個位毎フレームソートするようなプログラムでも最近のPCではビクともしないです。

配列にすると特定の部分に挿入したり特定の部分を削除したりソートしたりできないので、リストをお勧めします。

アバター
Hiragi(GKUTH)
記事: 167
登録日時: 13年前
住所: 大阪府
連絡を取る:

Re: 弾幕STGにおける弾の描画順序について

#5

投稿記事 by Hiragi(GKUTH) » 8年前

回答ありがとうございます。 std::listを用いて弾を管理してみました。
現在参考にしているサイト様に影響され、bulletはEnemyが持つ構造体となっています。よって登録処理や
移動処理が全てEnemy.cpp内に存在しますが前面云々に関しては一応解決していますので一度解決とさせていただきます。
申し訳ないのですが、Dixqさんが提示してくれたコードが(私の読む技術がなさすぎて)読めないので... 申し訳ないですが、現在悩んでる別クラス間の座標の受け渡しに関してもトピックを建てようと思っているので、その時に合わせてお願いします。

現状当たり判定は実装できていませんが、6万ほど弾を描画しても12-14msの処理なので問題ないと思っております。
また下記のコードで明らかにおかしい点等があれば指摘してもらえると幸いです。

現在のコードを提示します。

Enemy.h

コード:

#pragma once
#include <list>

class Enemy
{
private:
		//座標と自身のサイズ
	float x,y,speed;
	int sizeX,sizeY;
		//カウンタ、画像表示判別変数
	int cnt,num;
		//出現、停止、帰還、発射タイミング
	int in_time,stop_time,out_time,shot_time;
		//敵の種類
	int type;
		//移動パターン
	int m_pattern;
		//プレイヤーとの角度
	float ang;
		//敵消滅フラグ
	bool killflag;
		//敵クラス消滅フラグ
	bool endflag;

	int enemyhandle[7];

		//敵ショットについて
	typedef struct shot_t
	{
		float x;			//座標
		float y;
		float vx;
		float vy;
		float ang;
		int sizeX,sizeY;	//画像サイズ
		int cnt;
		float speed;		//ショット速度
		bool refflag;
		unsigned int index;	//管理番号
	}shot_t;
		//ショットが打てるようになったかどうかのフラグ
	bool s_flag;
		//ショットパターン
	int s_pattern;
		//弾の種類
	int s_type;
		//ショットが打てるようになってからのカウンタ
	int s_cnt;
		//ショットの画像ハンドル 一応一つの敵に対して16種類の弾を持てるようにしておく
	int shothandle[16];
	int s_sizeX[16],s_sizeY[16];

		//双方向リストで管理
	std::list<shot_t> bullet;

public:
	void Update();
		void Move();
		void Anim();
		void Shot();
			void Enter_shot();
	void Draw();
	bool All();
	void GetPos(double *x,double *y);
		//未実装
	//bool GetShotPos(int index,double *x,double *y);
	Enemy(int type, int s_type, int m_pattern, int s_pattern, int in_time, int stop_time, int shot_time, int out_time, float x, float y, float speed, int hp, int item);
	~Enemy();
};
Enemy.cpp Shot,Enter_shot,Draw関数

コード:


void Enemy::Shot()
{
		//ショット開始時間になったらフラグを立てる
	if(shot_time == g_cnt)
		s_flag = true;
		
		//フラグが立ったら弾を登録する
	if (s_flag)
		Enter_shot();


		//フラグが立っている弾の数
	int s = 0;
		//フラグが立っている弾だけ移動計算
	for (std::list<shot_t>::iterator blItr = bullet.begin();blItr != bullet.end();)
	{
		blItr->vx = float(cos(blItr->ang))*blItr->speed;
		blItr->vy = float(sin(blItr->ang))*blItr->speed;
				//実際に移動させる
		blItr->x += blItr->vx;
		blItr->y += blItr->vy;
		s++;
		shot_num++;

			//画面外でノードを削除
		if (blItr->x + blItr->sizeX < FIELD_X || FIELD_X_MAX +blItr->sizeX < blItr->x || blItr->y + blItr->sizeY < FIELD_Y || FIELD_Y_MAX + blItr->sizeY < blItr->y)
		{
			blItr = bullet.erase(blItr);
			s--;
			shot_num--;
			continue;
		}
		blItr++;
	}
	DrawFormatString(x + 40,y+40,GetColor(255,0,0),"%d shot ",s);
			//キルフラグがたてばこのクラスを削除する
	if (killflag && s == 0)
	{
			//敵消滅フラグを立てる
		endflag = true;
	}

}

void Enemy::Enter_shot()
{
	++s_cnt;
	
		//プレイヤーとの角度を求める
	double px,py;
	Manager::Instance()->GetPlayerPos(&px,&py);
	ang = atan2(py-y,px-x);
	DrawFormatString(x+50,y+50,GetColor(255,0,0),"%f Rad",ang);

		//プッシュ用
	shot_t tmp;
		//最低限の初期化 バグ解消に2時間かけたとこ
	tmp.cnt = 0;
	tmp.sizeX = s_sizeX[0];
	tmp.sizeY = s_sizeY[0];

		//複数のパターンを作る
	switch (s_pattern)
	{
		//パターン0
		case 0:
		{
			//10カウントに1回、40カウントまで発射する
			if (s_cnt % 10 == 0 && s_cnt <= 60)
			{
				tmp.x = this->x;
				tmp.y = this->y;
				tmp.ang = PI/2;
				bullet.push_front(tmp);
			}
			break;
		}

		//パターン1
		case 1:
		{
			int cnt_1 = 0;
			if (s_cnt % 4 == 0 && s_cnt <= 240)
			{
				for (int i = 0;i < 8;++i)
				{
					cnt_1++;
					tmp.refflag = true;
					tmp.x = this->x;
					tmp.y = this->y;
					tmp.ang = (PI2 / 256 * cnt_1) + g_cnt * 5;
					tmp.speed = cnt_1*0.05 + 2;
					bullet.push_front(tmp);
				}
			}
			break;
		}

			//パターン2 自機狙い弾
		case 2:
		{
				//2フレームに一回、200フレーム間に
			if (s_cnt % 2 == 0 && s_cnt <= 180)
			{
				tmp.refflag = false;
				tmp.x = this->x;
				tmp.y = this->y;
				tmp.ang = this->ang;
				tmp.speed = this->speed;
				bullet.push_front(tmp);
			}
			break;
		}

			//パターン3 自機狙い全方位弾(360way)
		case 3:
		{
			if (s_cnt % 1 == 0 && s_cnt <= 180)
			{
				for (int i = 0;i < 360;i++)
				{
					tmp.refflag = false;
					tmp.x = this->x;
					tmp.y = this->y;
					tmp.ang = this->ang + (i*1)*PI/180;
					tmp.speed = this->speed;
					bullet.push_front(tmp);
				}
			}
		}
	}
}

void Enemy::Update()
{
	Move();
	Anim();
	Shot();
	++cnt;
}

void Enemy::Draw()
{
	SetDrawMode(DX_DRAWMODE_BILINEAR);
		//弾が前面
	for (std::list<shot_t>::iterator blItr = bullet.begin();blItr != bullet.end();)
	{
		DrawRotaGraphF(blItr->x,blItr->y,1.0f,0.0f,shothandle[0],true,false);
		++blItr;
	}
	SetDrawMode(DX_DRAWMODE_NEAREST);
		//キルフラグが立っていなければ描画
	if(!killflag)
	{ 
		DrawRotaGraphF(x,y,1.0f,0.0f,enemyhandle[num],true,false);
	}
}
オフトピック
あと私のユーザー名の右にあるポイント4分の1ぐらいにしてくれませんか...
だいがくせい!

閉鎖

“C言語何でも質問掲示板” へ戻る