スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

ローグライクゲームを作ってみよう!  その6 ~描画システム~

今までソースコード一つのみの、サンプルを公開してきたわけですが、
やっぱり複数のソースコードに分割した方が良いと思ったので、

根本の描画システムの構築方法を解説していきます。
※あくまで自分の実装方法なので、参考程度に。


まず、力技で実装した場合、
下記の様なプログラムになっているかもしれません。
while(//省略 ){
MovePlayer();
MoveEnemy();
DrawPlayer();
DrawEnemy();
}
※whileの中が、ゲームループとする。

これは良くない構造です。

■理由
・敵が居なくても、とりあえずMoveEnemy関数 / DrawEnemy関数が呼ばれる
・主人公が居なくても、とりあえずMovePlayer関数 / DrawPlayer関数が呼ばれる
・描画順を制御できない


3番目が特に致命的ですね。

敵⇒主人公 という描画順から、主人公⇒敵という描画順に変更したい!
という場合、まともな対処方法がありません。


※下記の様な実装でごり押しする事も出来ますが、止めた方が良いです。
(中規模、大規模なゲームでは、間違いなくフラグ管理とかがゴチャゴチャになる。)
while(//省略 ){
if( flag == 0 ){
DrawPlayer();
DrawEnemy();
}else{
DrawEnemy();
DrawPlayer();
}
}



という訳で、描画をまずリスト形式にします。
STLにlistというのがあるので、それを使いましょう。
※STL … スタンダードテンプレートライブラリの略。

プログラム内で、唯一の描画リストを定義しておき、
それに登録された描画オブジェクトを描画
という風にします。

【参考画像】
drawsystem1.jpg


で、描画リストで重要になってくるのが、
下記の3つの処理を、具体的にどう実装するのか?です。
--------------------------------------------------
【1】描画オブジェクトの登録方法
【2】描画オブジェクトの削除方法
【3】描画オブジェクトの描画順変更方法

--------------------------------------------------

まず【1】登録。
リストに登録と言っても、
先頭に追加、末尾に追加、リストの途中に追加の3種類があります。

これは、描画オブジェクトに優先度の変数を設けて対応します。

例えばこんな感じ。
class Drawer {
public:
Drawer();
~Drawer();
int GetPriority();
private:
int m_Priority;
};

int AddDrawer( Drawer& t_Drawer )
{
int result = -1;

for( it = m_DrawerList.begin(); it != m_DrawerList.end(); it++ )
{
if( it->GetPriority() > t_Drawer.GetPriority() ){
m_DrawerList.insert( it, t_Drawer );
result = 0;
break;
}
}
//登録されなかったら末尾に追加
if( result == -1 ){
m_DrawerList.push_back( t_Drawer );
result = 0;
}

return( result );
}

※m_Priorityの値が小さいほど優先度が高い、とします。

何気に重要なのが赤字の部分です。
この処理が無いと、正常に描画オブジェクトが登録されません。

正常に登録されないのは下記の2パターンです。

【A】描画リストに、一つも描画オブジェクトが登録されていない場合。
【B】描画リストに登録されているものよりも、優先度が低いオブジェクトを登録する場合。


【A】について。
描画オブジェクトが登録されてなかったら、優先度の比較もクソも無いので、 
for文の処理自体がすっ飛ばされます。
なので、登録処理が行われません。

【B】について。
例えば、優先度1、2、3のオブジェクトが登録済みとします。
そこへ、優先度4のオブジェクトを登録しようとすると、

下記の様にif文の判定が順番に行われるのですが、
全部 成立しないので、結局 登録処理は行われずにfor文を抜けます。

1 > 4  //成立しない
2 > 4  //成立しない
3 > 4  //成立しない

まぁ、という訳で、
for文の方で登録が成立すればresultに0を代入しておき、
その後に、resultが-1のまま(登録が行われなかった)かどうかを判定して、
-1だったら、末尾に追加という処理をしておく必要があるのです。


次に【2】削除。

削除に関しては、これも
何らかの値を用意しておき、それを判別して削除する必要があります。


最初、下記の様に描画オブジェクトIDを用意して、
それを判別して削除してたのですが、


class Drawer {
public:
Drawer();
~Drawer();
int GetPriority();
int GetObjectID();
private:
int m_Priority;
int m_ObjectID;
};

//削除したい描画オブジェクトIDを渡す。
int DeleteDrawer( int t_ObjectID ) {
int result = -1;

for( it = m_DrawerList.begin(); it != m_DrawerList.end(); it++ )
{
//描画オブジェクトIDが一致すれば削除
if( it->GetObjectID() == t_ObjectID ){
m_DrawerList.erace( it );
result = 0;
break;
}
}
return( result );
}


描画リストにオブジェクトが200個登録されている場合、
最悪200回判定をしないといけない(効率が悪い)ので、
やり方を変えました。

~変えた点~
・描画リストに登録するオブジェクトを、参照数計測ポインタに変更
・描画リストに対応する、描画オブジェクト配列を定義
・描画オブジェクトを登録する際は、配列番号を指定
・描画オブジェクトを削除する際は、配列番号を指定
・描画する際に、描画フラグを調べて、フラグによって描画/削除を決定


文章だと分かりにくいですね。

例えば、優先度が4の描画オブジェクトを、
描画オブジェクト番号0番として登録する
とします。

すると下記の様に、描画リストと描画オブジェクト配列の
参照数計測ポインタが、メモリ上の同じ描画オブジェクトを指す
ようになります。

【参考画像】
drawsystem2.jpg

このようにしておけば、
削除する際は、描画オブジェクト配列を介して描画フラグを変更するだけで、
一発でオブジェクトの削除(描画終了フラグのセット)が出来ます。


//削除したい描画オブジェクト配列の添え字を渡す。
int DeleteObject( int t_VecNum )
{
int result = -1;

//添え字の値が正しいかチェック
if( t_VecNum >= 0 && ( int )m_DrawerVec.size() > t_VecNum ){
//ポインタがNULLではないことを確認
if( m_DrawerVec[ t_VecNum ] != NULL ){
//描画終了フラグをセット
m_DrawerVec[ t_VecNum ]->SetDrawFlag( DRAW_DELETE );
result = 0;
}
}
return( result );
}

void Draw()
{
for( it = m_DrawerList.begin(); it != m_DrawerList.end(); )
{
//削除フラグが設定されていれば削除
if( it->GetObjectFlag() == DRAW_DELETE ){
it = m_DrawerList.erace( it );
}else{
//そうでなければ描画
it->Draw();
++it;
}
}
}




最後に【3】描画順の入れ替え

描画順を入れ替える場合は、下記の手順を踏みます。
--------------------------------------------------
1)既にリストに登録されている該当オブジェクトを削除
2)再度、優先順位を比較しながら登録

--------------------------------------------------

とりあえず、先ほどの削除と同じように、
フラグを設定 → 描画時にフラグ判別を行い、処理実行
という風にします。

具体的にいうと、下記の様に描画順変更フラグ(DRAW_CHANGE)がセットされていれば、
とりあえず描画 ⇒
該当の優先順位の箇所に登録(ChangeDrawObject関数)⇒
リストから削除

とします。
void Draw()
{
for( it = m_DrawerList.begin(); it != m_DrawerList.end(); )
{
switch( ( *it )->GetObjectFlag() ){
case DRAW_DELETE: it = m_DrawerList.erace( it );
break;
case DRAW_CHANGE: ( *it )->Draw();
ChangeDrawObject( *it );
it = m_DrawerList.erace( it );
break;
default: ( *it )->Draw();
++it;
break;
}
}
}



ただ、この方法には欠点があります。
1)優先度を低くした場合、1フレームだけ、オブジェクトが2回描画される。
2)描画順変更フラグをセットして、実際に描画順が変更されるのは1フレーム後。


まず1)についてですが、
優先順位を1から3に変更したとします。
そして、下記の様に登録されている場合、オブジェクトAが2回描画されます。

オブジェクトA … 優先度1
オブジェクトB … 優先度2
オブジェクトC … 優先度4

A描画 → B描画 → A描画 → C描画


次に、2)について。
描画をする瞬間に優先順位を変更する仕様の為、
優先度変更フラグセット⇒
描画⇒
描画後、適切な優先度の位置へ変更

という風に、ラグが生じます。


という訳で、優先順位変更は描画時に行うものではない
という結論に至りました。


最終的に実装は、
・リストに登録されている参照数計測ポインタを指すポインタを定義
・削除する際、ポインタを通じて、参照数計測ポインタにNULLを突っ込む
・描画する際、描画フラグ判定の前に、オブジェクトがNULLでないか確認


これも図で見た方が分かり易いので、下記をご覧ください。

drawsystem3.jpg
描画リストの参照数計測ポインタを指すポインタを定義します。
(赤い点線。)

削除する際は、そのポインタを用いて、
描画リスト上の参照数計測ポインタにNULLを突っ込みます。

描画オブジェクト配列からの参照は残っているので、
メモリ上のオブジェクトは解放されない点がポイントです。
drawsystem4.jpg

NULL突っ込んだ後は、あとは普通に登録するだけです。
※登録する際の大雑把な流れは、本記事上部参照。

void Draw()
{
for( it = m_DrawerList.begin(); it != m_DrawerList.end(); )
{
//NULLならリストから削除
if( ( *it ) == NULL ){
it = m_DrawerList.erace( it );
}else{
//削除フラグが設定されていれば削除
if( ( *it )->GetObjectFlag() == DRAW_DELETE ){
it = m_DrawerList.erace( it );
}else{
//そうでなければ描画
( *it )->Draw();
++it;
}
}
}
}


現状は上記の様な構造になってます。
その内、また改善版を考えるかもしれませんが、
その際は、また記事にして纏めます。

最後に。

上記に記載したソースコードは、
大雑把な流れだけ示したものなので、動作しません。(変数定義や、main関数がない)

なので、動作するソースコードを以下に記載します。
下記のソースコードをベースに、ローグライクゲームの開発を進めてみる事にします。

【ソースコード】 http://rudora3.web.fc2.com/RogueSample1.zip
ソースコード一式をUPしました!(`・ω・´)

main.cpp
CSingleton.h
CDrawerBase.h
CDrawerBase.cpp
CDrawer2D.h
CDrawer2D.cpp
CDrawerManager.h
CDrawerManager.cpp


RogueSample1_1.jpg


※Enterキーで描画順変更、
「→」キーで描画オブジェクト削除 という動作になってます。
※拡大・縮小のフラグが用意してありますが、現状設定しても動作に影響しません。
※とりあえず"grade.jpg"という画像を読み込むようにしてます。
※ソースコードは、適宜修正、改良する可能性があります。
スポンサーサイト

テーマ : ゲーム製作 関連 - ジャンル : ゲーム

コメント

No title

参考になります!!

No title

ありがとうございます
参考になると嬉しいです!v-290

ソースコード一式もアップロードしたので、
参考になればと思います!(`・ω・´)

No title

ソースコードのアップありがとうございます。
早速ダウンロードさせて頂き、理解するために読み込み中です。

勝手ながら、今後も分かりやすい解説を期待しております。

Re: No title

参考になれば幸いです(・・)*

あと、申し訳ないのですが、不具合を見つけました。
DeleteDrawer関数を2回呼ぶとプログラムが強制終了しますOTZ

これは、deleteしようとしている描画オブジェクトが
既にNULLになっているかを確認していない為で、

下記の様に既にNULLなのかどうかを確認する処理を入れれば
正常に動作するようになります。(2回目のdeleteでも強制終了は発生しない)

int DrawerManager::DeleteDrawer( int t_DrawerVecNum )
{
int result = -1;

if( t_DrawerVecNum >= 0 && ( int )m_DrawerVec.size() > t_DrawerVecNum ){
if( m_DrawerVec[ t_DrawerVecNum ].m_Drawer != NULL ){
( *m_DrawerVec[ t_DrawerVecNum ].m_pDrawer ) = NULL;
m_DrawerVec[ t_DrawerVecNum ].m_pDrawer = NULL;
m_DrawerVec[ t_DrawerVecNum ].m_Drawer = NULL;
}
result = 0;
}

return( result );
}
※m_DrawerがNULLでなければ、m_DrawerをNULLにする
m_pDrawerがNULLでなければ、m_pDrawerをNULLにする、
という様にそれぞれチェックを別々に行うべきですが、
m_Drawer、m_pDrawer、およびm_pDrawerが指す参照数計測ポインタは、
全部一括して生成、削除するという仕様
にしている為、

m_DrawerがNULLでなければ、
m_pDrawerが指す参照数計測ポインタも、m_pDrawerも、
m_Drawerと一緒に削除(NULLを突っ込む)
という動作にしています

上記以外にも不具合があれば、適宜修正して行こうと思います(_ _;)
コメントの投稿
管理者にだけ表示を許可する



上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。