この辺の作り方はいろいろ議論も白熱する部分です。
Unityを触ったことのある人はゲームオブジェクトという単語をよく耳にすると思うけど日本で言う「タスクシステム」は海外では「ゲームオブジェクト」と呼ぶそうです。
どちらの言葉にしてもゲーム中で利用されるワークのことを指すのだと考えて間違いはないと思います。
ゲームオブジェクトの考え方はなにもUnityに限った事ではなく
コンシューマーゲームが提供しているフレームワークも近い機能のものを提供していることが多い。
Windows上でデータ作成するエディタを提供して、そのエディタで吐き出したリソースをアーカイブ化→ゲームはロードするだけ
といった感じがゲーム開発らしい開発なのではないでしょうか。
なのでプログラマが実際にデータを作ることはなく、ソースからポチポチ座標を打つなんてこともありえない。
そのほとんどはデザイナーがエディタ上で行う作業です。
その辺の仕組みを書いていこうかと思っております。
まず基本となるゲームオブジェクトを書いてみた。
// ---------------------------------------------------------------------
// オブジェクト基底クラス
// ゲーム中で利用されるオブジェクトは基本的にこれを継承する
// (継承しない場合のリソース管理は自己責任)
// ---------------------------------------------------------------------
class GameObject {
public :
// データ型
enum OBJECT_TYPE {
OBJECT_TYPE_NULL = 0x00 ,
DRAW_OBJECT = 0x01 ,
SOUND_OBJECT = 0x02 ,
PICTURE_OBJECT = ( (0x01) m_childObject; // 子オブジェクト
GameObject* m_parent; // 親オブジェクト
OBJECT_TYPE m_objType;
void _setObjType( OBJECT_TYPE type ){
m_objType = type;
}
public :
static bool TypeIs( GameObject* thisObj , OBJECT_TYPE type ){
if( !thisObj ) return false;
if( thisObj->getObjType() == type ) return true;
return false;
}
template
static T* TypeAs( GameObject* thisObj ){
return dynamic_cast( thisObj );
}
OBJECT_TYPE getObjType(){
return m_objType;
}
void addObject( GameObject* newobject ){
if( !newobject ) return;
newobject->m_parent = this;
m_childObject.push_back( newobject );
}
virtual void OnUpdate(){
}
virtual void OnDraw(){
}
std::string getName(){
return m_name;
}
void setName( std::string name ){
m_name = name;
}
int getID(){
return m_id;
}
GameObject(){
m_name = "NoName";
m_id = 0;
m_objType = OBJECT_TYPE_NULL;
m_parent = NULL;
}
virtual ~GameObject(){
}
GameObject* getParent() const {
return m_parent;
}
// 子オブジェクトをリストから外す。
// 外すだけで開放はしない
void RemoveObject( GameObject* obj ){
if( !obj ) return;
for( std::list::iterator iter = m_childObject.begin() ; iter != m_childObject.end() ; iter++ ){
if( (*iter) == obj ){
m_childObject.remove( obj );
return;
}
(*iter)->RemoveObject( obj );
}
}
// オブジェクトの開放
// 再帰的に管理されるオブジェクト全てにReleaseメソッドを呼ぶ
void ReleaseObject(){
while( true ){
std::list::iterator iter = m_childObject.begin();
if( iter == m_childObject.end() ) break;
GameObject* deleteObj = ( *iter );
m_childObject.remove( deleteObj );
deleteObj->ReleaseObject();
delete deleteObj;
}
// 親がいるなら親からの連結解除
if( m_parent ){
m_parent->RemoveObject( this );
}
}
// 名前からオブジェクトを取得
// @param name : オブジェクト名
GameObject* FindObject( std::string name ){
GameObject* result = NULL;
for( std::list::iterator iter = m_childObject.begin() ; iter != m_childObject.end() ; iter++ ){
if( name.compare( (*iter)->getName() ) == 0 ){
return (*iter);
}
result = (*iter)->FindObject( name );
if( result ){
return result;
}
}
return result;
}
// オブジェクト番号からオブジェクトを取得
// 番号が重複した場合、最上階にあるオブジェクトが選ばれるので注意
// @param id : オブジェクトの番号
GameObject* FindObject( int id ){
GameObject* result = NULL;
for( std::list::iterator iter = m_childObject.begin() ; iter != m_childObject.end() ; iter++ ){
if( id == (*iter)->getID() ){
return (*iter);
}
result = (*iter)->FindObject( id );
if( result ){
return result;
}
}
return result;
}
};
なので全てのオブジェクトに階層構造が存在することになる。
OBJECT_TYPEという列挙型が存在するけど、これは派生オブジェクトに持たせるデータ型のパラメータとなる。
JavaやC#でいうところのType型に相当します。
C++にはas演算子やis演算子といった機能が存在せず、安全なキャストは基本的に難しい。
一歩間違えればすぐに不正アクセスが発生するのでデータの扱いには気をつけなければいけない。
外部ライブラリなんかではtypeofを関数で作ってるものもあるのでこのような方法にしてみた。
このゲームオブジェクトには描画情報が入っていない。
描画オブジェクトを別に作成します。
// ---------------------------------------------------------------------
// 描画オブジェクト基底
// ---------------------------------------------------------------------
class DrawObject : public GameObject {
public :
// 画像描画中心位置
enum ANCHOR{
TOP_LEFT ,
TOP_CENTER ,
TOP_RIGHT ,
MIDDLE_LEFT ,
MIDDLE_CENTER ,
MIDDLE_RIGHT ,
BOTTOM_LEFT ,
BOTTOM_CENTER ,
BOTTOM_RIGHT ,
};
float posX; // 座標x
float posY; // 座標y
int sizeWidth; // 画像サイズ:幅
int sizeHeight; // 画像サイズ:高さ
float scale; // 拡大率
int alpha; // ブレンド率
float rot; // 回転角度
bool visible; // 表示フラグ
ANCHOR anchor; // 中心座標
// 中心位置を計算して返す X座標
float getPosAnchorX(){
switch( anchor ){
case TOP_LEFT : return posX + ( sizeWidth / 2 );
case MIDDLE_LEFT : return posX + ( sizeWidth / 2 );
case BOTTOM_LEFT : return posX + ( sizeWidth / 2 );
case TOP_CENTER : return posX;
case MIDDLE_CENTER : return posX;
case BOTTOM_CENTER : return posX;
case TOP_RIGHT : return posX - ( sizeWidth / 2 );
case MIDDLE_RIGHT : return posX - ( sizeWidth / 2 );
case BOTTOM_RIGHT : return posX - ( sizeWidth / 2 );
}
return 0;
}
// 中心位置を計算して返す Y座標
float getPosAnchorY(){
switch( anchor ){
case TOP_LEFT : return posY + ( sizeHeight / 2 );
case TOP_CENTER : return posY + ( sizeHeight / 2 );
case TOP_RIGHT : return posY + ( sizeHeight / 2 );
case MIDDLE_LEFT : return posY;
case MIDDLE_CENTER : return posY;
case MIDDLE_RIGHT : return posY;
case BOTTOM_LEFT : return posY - ( sizeHeight / 2 );
case BOTTOM_RIGHT : return posY - ( sizeHeight / 2 );
case BOTTOM_CENTER : return posY - ( sizeHeight / 2 );
}
return 0;
}
void setPos( float x , float y ){
posX = x;
posY = y;
}
// 下位16ビットにDRAW_OBJECTが含まれているオブジェクトは描画オブジェクトとして扱う
static bool isDrawObject( int type ){
if( ( 0xFF & type ) == DRAW_OBJECT ) return true;
return false;
}
// 階層構造上の座標加算値を返す
float getParentPosX(){
float x = posX;
if( !getParent() ) return x;
DrawObject* drawObject = GameObject::TypeAs( getParent() );
if( drawObject ){
x += drawObject->getParentPosX();
}
return x;
}
// 階層構造上の座標加算値を返す
float getParentPosY(){
float y = posY;
if( !getParent() ) return y;
DrawObject* drawObject = GameObject::TypeAs( getParent() );
if( drawObject ){
y += drawObject->getParentPosY();
}
return y;
}
// コンストラクタ
// 描画情報初期値を入れる
DrawObject(){
posX = 0;
posY = 0;
sizeWidth = 0;
sizeHeight = 0;
scale = 1.0f;
alpha = 255;
rot = 0.0f;
visible = true;
anchor = MIDDLE_CENTER;
}
virtual void OnUpdate(){
GameObject::OnUpdate();
}
// 描画
virtual void OnDraw(){
GameObject::OnDraw();
}
};
余裕があればカラーアニメーションもさせたいのでポリゴンから描画という風に改造してみるといいかもしれません。
DrawRotaGraphの中心座標は画像の中央ですが、左上をアンカーにしたい場合も考えてアンカーパラメータを持たせています。
このクラスになぜ描画処理を入れないかというと、描画オブジェクトは画像に限らず様々な場合が考えられるためです。
なのでこのクラスは「描画処理をするゲームオブジェクトの基底」と考えます。
共通で利用されるであろうパラメータと処理をまとめて定義します。
次に画像情報
// ---------------------------------------------------------------------
// パッケージスプライト
// ---------------------------------------------------------------------
class PackSprite {
private :
std::string m_filename;
int* m_handle;
int m_sizeWidth;
int m_sizeHeight;
int m_xNum;
int m_yNum;
public :
PackSprite(){
m_handle = NULL;
m_sizeWidth = 0;
m_sizeHeight = 0;
m_xNum = 0;
m_yNum = 0;
}
int getWidth(){
return m_sizeWidth;
}
int getHeight(){
return m_sizeHeight;
}
std::string getFileName(){
return m_filename;
}
void unload(){
if( !m_handle ) return;
for( int i = 0 ; i m_filename = src.m_filename;
int texAll = src.m_xNum * src.m_yNum;
this->m_handle = new int[texAll];
for( int i = 0 ; i m_handle[i] = src.m_handle[i];
}
this->m_sizeWidth = src.m_sizeWidth;
this->m_sizeHeight = src.m_sizeHeight;
this->m_xNum = src.m_xNum;
this->m_yNum = src.m_yNum;
}
// 画像のロード処理
void Load( std::string filename , int xNum , int yNum ){
if( m_handle ){
APP_PRINT( "二度読み禁止\n" );
return;
}
// スプライトアトラスの数指定が大丈夫な値かどうか確認
int texAll = xNum * yNum;
if( texAll = ( m_xNum * m_yNum ) ) return 0;
if( !loadSync() ) return 0;
return m_handle[index];
}
};
ゲームオブジェクトとは分離して管理するようにする。
画像リソースに関してはファイル名を記録しておいて静的に管理するマネージャー層を作り、そこにテクスチャ情報をプールしていきます。
プールしたテクスチャはシーン終了時にまとめて開放する。
このリソースの開放はオブジェクトにはやらせないように作るのがポイント。
オブジェクトにはあくまでも利用する画像ハンドル番号しか持たせません。
// ---------------------------------------------------------------------
// 画像オブジェクト
// ---------------------------------------------------------------------
class PictureObject : public DrawObject {
private :
static std::vector gTexPool;
PackSprite m_sprite;
public :
bool transFlag;
bool turnFlag;
int texAnimNo;
PictureObject(){
_setObjType( PICTURE_OBJECT );
transFlag = true;
turnFlag = false;
texAnimNo = 0;
}
static void Unload(){
APP_PRINT( "リソースの開放\n" );
for( size_t i = 0 ; i ().swap( gTexPool );
}
virtual ~PictureObject(){
APP_PRINT( "PictureObject Destroy [Name %s] \n" , m_name.c_str() );
m_sprite.finalize();
}
void load( std::string filename ){
load( filename , 1 , 1 );
}
void load( std::string filename , int xNum , int yNum ){
// 読み込み済みがあればそちらを使う
for( size_t i = 0 ; i ::iterator iter = m_childObject.begin() ; iter != m_childObject.end() ; iter++ ){
(*iter)->OnUpdate();
}
}
virtual void OnDraw(){
if( !visible ) return;
float x = getPosAnchorX();
float y = getPosAnchorY();
float ParentPosX = 0;
float ParentPosY = 0;
GameObject* parent = getParent();
// 親が存在する場合、親の座標を計算する
DrawObject* draw = GameObject::TypeAs( parent );
if( draw ){
ParentPosX = draw->getParentPosX();
ParentPosY = draw->getParentPosY();
}
DrawRotaGraphF( ParentPosX + x , ParentPosY + y , scale , rot , m_sprite.getHandle( texAnimNo ) , transFlag , turnFlag );
for( std::list::iterator iter = m_childObject.begin() ; iter != m_childObject.end() ; iter++ ){
(*iter)->OnDraw();
}
}
};
ロードメソッドを呼べば、まだ読み込んでいなければロード。
読み込み済みならプールされてる情報をコピーします。
このように複数から同じ画像を持つ可能性があるので開放処理はオブジェクトにはやらせられないのです。
描画順序は 親→子 という順番。
子オブジェクトほどプライオリティが上がります。
仮に画像を読み込んで処理を読んでみる
class SceneTitle : public GameScene::Scene {
private :
PictureObject* obj[4];
public :
virtual void OnInit(){
obj[0] = new PictureObject();
obj[1] = new PictureObject();
obj[2] = new PictureObject();
obj[3] = new PictureObject();
obj[0]->load( "resource/title_001.png" );
obj[1]->load( "resource/title_002.png" );
obj[2]->load( "resource/title_003.png" );
obj[3]->load( "resource/title_004.png" );
obj[0]->addObject( obj[1] );
obj[0]->addObject( obj[2] );
obj[0]->addObject( obj[3] );
obj[0]->anchor = DrawObject::TOP_LEFT;
obj[3]->anchor = DrawObject::TOP_LEFT;
obj[0]->setName( "obj0" );
obj[1]->setName( "obj1" );
obj[2]->setName( "obj2" );
obj[3]->setName( "obj3" );
}
virtual void OnUpdate(){
if( obj[0] ){
obj[0]->OnUpdate();
obj[0]->OnDraw();
//obj[0]->posX+= 1;
}
DrawString( 0 , 0 , "タイトル" , GetColor(255,0,255) );
if( CheckHitKey( KEY_INPUT_1 ) ){
if( obj[0] ){
obj[0]->ReleaseObject();
delete obj[0];
obj[0] = NULL;
}
}
}
virtual void OnRelease(){
}
};
実際のところ、こんな感じで生成を呼ぶのは面倒です。
名前設定もソースでやる必要はなく、もっと楽をするにはどうすれば良いか
という話をそのうち書きたいです。
今日は一日中雨だったので久しぶりにこんなふうにコードを書いて頭のリフレッシュが出来て良かったかもしれない。
ではでは
P.S
C++での型の安全なキャストにdynamic_castがあるということを知ったので書き換えてみる