ページ 11

ダウンキャストはやっていいの?

Posted: 2012年10月01日(月) 18:42
by LisetteLander
データフローの中でどうしてもダウンキャストをしてしまう箇所があるのですが、どうしたらいいでしょうか?

Objectクラス

継承

Entityクラス→当たり判定を委託→ContactAreaクラス

継承

Characterクラス

というクラス関係で、「ContactAreaクラス」のポインタを「ContactAreaListクラス」に登録し、
当たり判定リストクラス::Run()という関数が呼び出されると一括ですべてを判定して、当たっていたらフィードバックをEntityクラスに返すというものです。
(ContactAreaListに登録されたContactAreaクラス同士で判定を行い、持ち主(Entity)に結果を伝える)

このような相関図で攻撃判定を行うのですが、ContactAreaクラスは他のContactAreaクラスと衝突した時、
無条件に持ち主(Entity)に「この相手(ContactAreaクラス)と衝突した」と結果を持ってきます。

この時、そのEntity以外がすべて敵なら持ってきたすべてのContactAreaクラスに対しダメージ処理をすれば楽チンなのですが、
Characterクラスが保持している勢力情報(int型ですが)によって分別してダメージ処理したいので必然的にEntity→Characterクラスへダウンキャストして勢力情報を取得する必要があります。

こういった場合ダウンキャストは適切なのでしょうか?
文章だけでわかりにくいかもしれませんがよろしくお願いします。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月01日(月) 20:49
by nullptr
文章だけなのと継承関係の矢印の向きがはっきりしない(想像はつきますが)のでよくわかりませんが、
当たり判定を出した時情報を欲しいから、Character型にキャストするのは良いのか、という話だと解釈します。

結論から言うと、良いとは思えません。
インターフェイスを使ってはどうでしょう。

Entityクラス・・“実体クラス”?そのあたりの関係性がよくわからないのですが。
例を挙げるなら、
まずEnergyInformationクラスとCollisionInformationクラスを用意します。
EnergyInformationクラスに勢力情報を取得する純仮想関数の宣言し、CollisionInformationが更にそれを継承(勢力情報を取得する関数はまだ定義しない)、ここには当たり判定に必要な関数を純仮想関数で宣言します。そしてCharacterクラスより下の具体クラスで全て定義します。
Entityには自分(Character)のthisポインタをCollisionInformationとして渡し(暗黙的なアップキャスト)、当たり判定で当たっていた場合、接触元へ返す際にCollisionInformationのポインタをEnergyInformationとして返し(暗黙的なアップキャスト)、Entityではそれを介して得た情報を元に計算すれば良いのでは?

Re: ダウンキャストはやっていいの?

Posted: 2012年10月01日(月) 23:50
by LisetteLander
回答ありがとうございます。
新月の獅子 さんが書きました: まずEnergyInformationクラスとCollisionInformationクラスを用意します。
EnergyInformationクラスに勢力情報を取得する純仮想関数の宣言し、CollisionInformationが更にそれを継承(勢力情報を取得する関数はまだ定義しない)、ここには当たり判定に必要な関数を純仮想関数で宣言します。そしてCharacterクラスより下の具体クラスで全て定義します。
Entityには自分(Character)のthisポインタをCollisionInformationとして渡し(暗黙的なアップキャスト)、当たり判定で当たっていた場合、接触元へ返す際にCollisionInformationのポインタをEnergyInformationとして返し(暗黙的なアップキャスト)、Entityではそれを介して得た情報を元に計算すれば良いのでは?
えーと、位置的にはEntityクラスとCharacterクラスの間にEnergyInformationクラスとCollisionInformationクラスが入るような形でしょうか。
Entityというのはゲーム中に表現できるすべてのもののベースとして作りました。画像・当たり判定のどちらかを持つものはすべてこれから派生しますが、画像・当たり判定を持つクラスを保持するだけでその機能をもっているわけじゃないです。(壁やキャラクターや武器)
EntityとEntityから派生するクラス諸々は当たり判定に必要な情報をまったくもっておらず、Entityのメンバ変数であるContactAreaクラスが持ってます。(部位破壊や部位別ダメージみたいな表現をしたかった)
わかりづらくてすいません。

コード:

class ContactArea : public Object{};

class Entity : public Object{
 CContactArea ContactArea;
};
役に立たないかもしれませんが、攻撃からダメージ処理までの流れはこんなかんじです。
例では前提として攻撃したいEntityと攻撃を受けたいEntityそれぞれContactAreaを1つだけ持ってます。

攻撃したいEntityが攻撃属性を持ったContactAreaをContactAreaListに登録する。
攻撃を受けたいEntityが被害属性を持ったCotnactAreaをContactAreaListに登録する。
③ContactAreaList::Runを実行し、接触していたら【攻撃したいEntityのContactArea】に【攻撃を受けたいEntityのContactArea】のポインタを渡します。
④【攻撃したいEntityのContactArea】が受け取った【攻撃を受けたいEntityのContactArea】のポインタと、攻撃したいEntityのどのContactAreaが接触したかを自分の親である攻撃したいEntityに渡します。
⑤④で攻撃したいEntityが受け取った【攻撃を受けたいEntityのContactArea】のポインタと、【攻撃したいEntityのContactArea】という情報からダメージ量を計算して【攻撃を受けたいEntityのContactArea】に、ダメージ量と、接触した【攻撃したいEntityのContactArea】のポインタを渡します。
⑥【攻撃を受けたいEntityのContactArea】が受け取ったダメージ量とダメージ量と、接触した【攻撃したいEntityのContactArea】のポインタと、どのContactAreaが接触したかを自分の親である攻撃を受けたいEntityに渡します。
攻撃を受けたいEntityが受け取った【攻撃したいEntityのContactArea】のポインタとダメージ量と【攻撃を受けたいEntityのContactArea】から、最終的に自分が受けるダメージを計算し、その場で適用します。
            ③                   ④          ⑤                    ⑥     ⑦
ContactArea::Run → AttackEntity::ContactArea → AttackEntity → DefenseEntity::ContactArea → DefenseEntity

上記の部位別ダメージのせいもあって、ダメージ処理自体は継承元から受け継いだ仮想関数内でやりたいためEntityクラス内での処理はできません。(純粋仮想関数ではないので継承先で何も定義しなければダメージ処理はできそうですけど)

クラスを活用したゲームの1作目ということもあってゲームの設計自体掻い摘んで学んできたことなので変でしたら設計の方も、もしよろしかったらご指摘お願いします。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月02日(火) 01:05
by nullptr
LisetteLander さんが書きました:
新月の獅子 さんが書きました: まずEnergyInformationクラスとCollisionInformationクラスを用意します。
EnergyInformationクラスに勢力情報を取得する純仮想関数の宣言し、CollisionInformationが更にそれを継承(勢力情報を取得する関数はまだ定義しない)、ここには当たり判定に必要な関数を純仮想関数で宣言します。そしてCharacterクラスより下の具体クラスで全て定義します。
Entityには自分(Character)のthisポインタをCollisionInformationとして渡し(暗黙的なアップキャスト)、当たり判定で当たっていた場合、接触元へ返す際にCollisionInformationのポインタをEnergyInformationとして返し(暗黙的なアップキャスト)、Entityではそれを介して得た情報を元に計算すれば良いのでは?
えーと、位置的にはEntityクラスとCharacterクラスの間にEnergyInformationクラスとCollisionInformationクラスが入るような形でしょうか。
Entityというのはゲーム中に表現できるすべてのもののベースとして作りました。画像・当たり判定のどちらかを持つものはすべてこれから派生しますが、画像・当たり判定を持つクラスを保持するだけでその機能をもっているわけじゃないです。(壁やキャラクターや武器)
EntityとEntityから派生するクラス諸々は当たり判定に必要な情報をまったくもっておらず、Entityのメンバ変数であるContactAreaクラスが持ってます。(部位破壊や部位別ダメージみたいな表現をしたかった)
わかりづらくてすいません。
HUM...
>えーと、位置的にはEntityクラスとCharacterクラスの間にEnergyInformationクラスとCollisionInformationクラスが入るような形でしょうか。
というのは違いますかね。C++では多重継承が許されていますので、間に入れるというより、・・・実際にコードで見せたほうが早いですかね

コード:


/// とりあえず勢力情報が構造体だとして、
struct CharactorEnergy
{
};

class Entity
{

};

class EnergyInformation
{
public:
	virtual	const	CharactorEnergy& getCharactorEnergy() const = 0;	//< 勢力情報を取得するインターフェイス
};

class CollisionInformation
	:public	EnergyInformation
{
public:
	virtual	int getCollision() = 0;	//< 当たり判定に必要な関数(てきとう)
};

class Character
	:public	Entity
	,public	CollisionInformation
{
private:
	CharactorEnergy infomationEnergy;

public:
	const	CharactorEnergy&	getCharactorEnergy() const	{ return this->infomationEnergy; }
			int					getCollision()				{ return 0; }
};
イメージはこんな感じでした。ただ読んでみると少し勝手が違ったようなのでわかる範囲で書き換えるなら・・・

コード:

/// とりあえず勢力情報が構造体だとして、
struct CharactorEnergy
{
};

/// 勢力情報を取得するインターフェイス
class EnergyInformation
{
public:
	virtual	const CharactorEnergy& getCharactorEnergy() const = 0;	//< 勢力情報を取得するインターフェイスメソッド
};

class ContactArea
{
public:
	/// Runでは勢力情報を受け取るインターフェイスを引数にしてみる
	/// 勢力情報がどこで使用されるのか説明がないのですがおそらく攻撃を受けたいEntityの内部だと思うのです、そこへContactAreaを介してEnergyInformationを攻撃を受けたいEntityに渡せば良いのでは?
	void Run( EnergyInformation* energyInformation )
	{
		energyInformation->getCharactorEnergy(); //< これを渡す
	}
};

class Entity
{
private:
	ContactArea contactArea;	//< 当たり判定に関する情報や処理はこちらで行う?

protected:
	ContactArea& getContactArea(){ return this->contactArea; }

public:
	virtual void test() = 0;
};

class Character
	:public	Entity
	,public	EnergyInformation
{
private:
	CharactorEnergy infomationEnergy;	//< 勢力情報

public:
	const CharactorEnergy&	getCharactorEnergy() const	{ return this->infomationEnergy; }	//< インターフェイスの実装

	void test()
	{
		this->getContactArea().Run( this );
	}
};
なかなか見難いコードですいません、とりあえずこんなイメージで流せば勢力情報を流せるのではないでしょうか。的外れだったらすいません。
ダウンキャストの危険性はご存知でしょうか?
Character型にしてしまうと、関係ないもの、存在しないものまでアクセスできてしまうのが非常に危険なんです。危険であると同時に、オブジェクト指向の基礎的な概念であるカプセル化は崩壊します。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月02日(火) 11:38
by LisetteLander
Contact::RunはCharacterの処理順によってできるだけ不公平が無いよう1ループ1回呼び出されます。
説明不足でした、すいません。

Characterそれぞれの処理(Update)をしたあとにContactAreaList::Runを実行しContactAreaを経由してEntityの純粋仮想関数にFeedbackしてそれぞれのCharacterが振る舞いを決めます。
mainループ

コード:

int main(){
 CContactAreaList ContactAreaList;
 CCharacter CharacterFriend(&ContactAreaList);
 CCharacter CharacterEnemy(&ContactAreaList);

 while(/* */){
  CharacterFriend.Update();
    CharacterEnemy.Update();
  ContactAreaList::Run();
 }
}
↑一括

Entity

コード:

class CEntity : public CObject{//CObjectは座標情報があるだけ
protected:
 CContactArea ContactArea;
 CContactAreaList* ContactAreaList;
public:
 virtual void Feedback(ContactArea& ,int)=0;
 virtual void Update()=0;
}
Character

コード:

class CCharacter : public CEntity{
public:
 CCharacter(*CContactAreaList tmp){
  ContactAreaList=tmp;
  //他ContactAreaの設定
 }

 virtual void Update(){
  ContactAreaList->Entry(ContactArea);
 }

 virtual void Feedback(ContactArea* ContactArea){
  /*ダメージ処理・勢力判定*/
 }
};
ContactAreaList

コード:

class CContactAreaList{
private:
 vector <CContactArea*> List;
public:
 void Entry(){}//省略

 void Run(){
  if(/*色々やって、当たっていたら*/){
   ContactArea[攻撃]->NotifiAgency(ContactArea[被害]);//攻撃したContactAreaに被害を与えたContactAreaのポインタを渡す。
  }
 }
};
ContactArea

コード:

class ContactArea : public Object{
 CCharacter* Owner;
public:
 void NotifiAgency(CContactArea* tmp){Owner->Feedback(tmp);}
};
ひょっとすると、もうEntityはインターフェースクラスということを割りきってすべての継承先で必要になる純粋仮想関数を宣言しといたほうがいいのでしょうか?
つまり、壁だろうが人だろうが全部同じ関数を使って処理して、プレイヤーキャラクターだけアドレスをもらって操作する・・・みたいな・・・

Re: ダウンキャストはやっていいの?

Posted: 2012年10月02日(火) 18:55
by nullptr
あーそういうことでしたか。
んー・・・
ひょっとすると、もうEntityはインターフェースクラスということを割りきってすべての継承先で必要になる純粋仮想関数を宣言しといたほうがいいのでしょうか?
細かい仕様分かりませんがあまりイイ事である気はしません。
インターフェイスは見せるモノを絞るのに使ったほうが良いでしょうし、全部まとめてしまっては意味が無いのでは。


えっとー・・・具体的にどこでダウンキャストしちゃってるんですか?
一向に肝心のダウンキャストの部分が見えないのですが。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月02日(火) 22:52
by LisetteLander
あ、すいません、コードを間違えてました。

コード:

class ContactArea : public Object{
 CCharacter* Owner;
public:
 void NotifiAgency(CContactArea* tmp){Owner->Feedback(tmp);}
};
ではなく

コード:

class ContactArea : public Object{
 CEntity* Owner;
public:
 void NotifiAgency(CContactArea* tmp){Owner->Feedback(tmp);}
};
です。
例えばCharacterが他のEntityのContactAreaの勢力情報を調べたい時に
そのContactAreaの持ち主であるEntityがCharacterなのか?それともWallなのか、Weaponなのかというサブクラスに落とさないと判断できないということです。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月02日(火) 22:56
by LisetteLander
勢力によってダメージを受ける・受けないという判断をするのはCharacter同士だけなのですから、逆に勢力を全く考えないってゲームの仕様を変更したほうが楽ですけど・・・

Re: ダウンキャストはやっていいの?

Posted: 2012年10月03日(水) 04:33
by nullptr
あーーなるほど

うーんなかなか複雑ですねぇ・・・
そうですね・・・

勢力を考慮するのがキャラどうしだけで、今後仕様が変わらないのなら、ダブルディスパッチはどうでしょう。
こちらを参照してみてください。
これを応用して、ContactAreaのオーナ毎に振る舞いを変えるのも手です。

Re: ダウンキャストはやっていいの?

Posted: 2012年10月03日(水) 10:13
by LisetteLander
ありがとうございます、ダブルディスパッチ試してみます。
とりあえずこの質問は解決とさせていただきます。