ページ 11

画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月21日(木) 23:48
by 迷彩吹雪
現在C++にて全方位STGを制作しています。
衝突判定を実装し、自機にショットを撃たせて遊んでいた時のことです。
画面端でのみ、移動しながらショットを撃つと自分でその弾に接触してしまいます。
自機の速度が弾の速度を上回っているわけではなく、画面端でなければ接触は発生しません。
そして必ず、反時計回りに移動した時にのみ接触するのです。(添付画像の左側)
……いや、もしかすると時計周りでも画面外で接触しているかもですが。

原因として考えたのは、弾が画面端に接近すると急減速していることです。
同じ方向、同じ速度で発射した二つの弾が、なぜか一方だけ急に減速するのです。
今までは移動量計算の結果として返って来るdouble型の値をfloat型へキャストしているための誤差かと思ったのですが、それでは同時に発射されたもう一方の弾が減速しない理由がわかりません。
(どの方角に向けて撃っても、必ず、自機にとって右側から発射された弾が減速します)
移動量の計算式は自機も弾も三角関数を用いた以下のもので、他に特別な処理はしていません。
画面外に出れば消滅処理をしていますが、速度を変更するような処理はありません。
(試しにconst修飾子で速度を定数にしても、改善されませんでした)

//x,yには現在の座標が格納されている(float型)
//mSpeedはint型で速度(移動量)を表し、GetAngle()は進行方向をdouble型で返す
x += static_cast<float>(mSpeed*sin(GetAngle()));
y -= static_cast<float>(mSpeed*cos(GetAngle()));

まとめると、質問したいのは以下の2点です。
・弾が急減速する原因として何が考えられるか
・急減速が自弾と自機の接触する原因でなければ、その原因として何が考えられるか
接触するのは自機にとって「左の弾」で、減速するのは「右の弾」なので、この二つは別々の原因の可能性が高いような気もしますが、ご意見をお聞かせ下さい。

画像について補足:
グラフィックの下に描画されているのが衝突判定のための円です。
グラフィックと描画位置がずれているのは、
接触判定→判定円を描画→移動処理(接触していたらそれに応じた処理)→移動→グラフィック描画
の順番で処理しているからです。
前フレームの移動の結果を、現フレームの最初に評価している事になります。

開発環境
VC++ 2008 EE(言語はC++)
DXライブラリ

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 01:24
by 迷彩吹雪
現象を再現できる最低限のプロジェクトファイルを準備しました。
どうかよろしくお願いします。

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 02:54
by めるぽん
>・弾が急減速する原因として何が考えられるか
の原因は分かりましたが、どう解決するか悩ましいところですね。

あるオブジェクトが画面外へ行ったときに NormalShot.cpp 内で RemoveMaterial(this) をしていますが、RemoveMaterial で ite++ をしているので、イテレータが1つ進みます。つまり次のタスクを指している状態になります。
その後、その弾の処理が終わり、UpdateGame に戻り、for 文へ進むのですが、ここでまた ite++ が実行されるため、さっきまで ite が指していたオブジェクト(今回の場合は自機の右側の弾)の Update が行われず、処理が遅れてしまうようです。

イテレート中の要素削除はかなり危険なので、どうするか難しいところですが、例えば RemoveMaterial の処理では
//if(i == ite){
                //    ite++;        //タスク実行中の要素を削除しないように一つ進める
                //}
                //mGameList.erase(i);
                i->mMaterial = NULL;
のように NULL だけ設定しておいて、UpdateGame 内で NULL チェックするようにして、
for(ite = mGameList.begin(); ite != mGameList.end(); ite++){
            if (ite->mMaterial == NULL) continue; // 追加
UpdateGame の for 文が終わった後に
struct
        {
            bool operator()(const GameListData& d) const { return d.mMaterial == NULL; }
        } pred;
        mGameList.remove_if(pred);
のようにして、mMaterial が NULL になっているものを削除するようにすれば、ちゃんと動きました。
要素の削除を for の後ろに持っていくことで、イテレート中の削除を行わないようにしています。
ただ、この方法だと、mMaterial がいつ NULL になってもおかしくないため、mMaterial へアクセスする際は必ず NULL チェックが必要になるので、mMaterial へのアクセスを頻繁に行う場合は面倒になるかもしれません。

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 12:16
by 迷彩吹雪
>めるぽんさん
ご協力ありがとうございます。
やはり、そこに原因があったのですね……。質問を投下してからiteの二重インクリメントについて薄々感づいていたのですが、はっきりと確証が持てないでいました。
今は出先なので、帰宅次第試してみようと思います。

一つだけ確認させてください。
i->mMaterial = NULL;
これだけだと、それまでmMaterialが差していたインスタンスは実体を残したままメモリ上で行方不明になりませんか?
事前にdeleteしなくても良いのでしょうか。

ところで本題とは関係ありませんが、
struct
{
    bool operator()(const GameListData& d) const { return d.mMaterial == NULL; }
} pred;
mGameList.remove_if(pred);
について解説をお願いしてもよろしいでしょうか。
オペレーター演算子のオーバーロードだと思うのですが、「()」なんて演算子はありましたっけ? 新しく設定した演算子なら、それは何に対する演算子で、どこで使用しているのでしょう?
処理内容も私の理解力を上回っているようで、いまいちどのような処理を行っているのかわからないです。
pred構造体を宣言して、d.mMaterialがインスタンスを差しているか真偽判定している? なぜ構造体?

……あれ、なんだか質問事項が雪だるま式に増えているような(汗)。

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 16:09
by めるぽん
>これだけだと、それまでmMaterialが差していたインスタンスは実体を残したままメモリ上で行方不明になりませんか?
なりますね。単に mGameList.erase(i) としていたので、その辺は別で管理しているのだと思いました。
コードを見直すと、どこも delete を呼び出していないっぽいので、mGameList.erase(i) するにせよ、i->mMaterial = NULL; にせよリークするので、delete で破棄してから処理を行わないといけないですね。
(erase しても、GameListData が破棄されるだけで、GameListData の先の要素までは delete されません)


>「()」なんて演算子はありましたっけ?
関数オブジェクトですね。これだけコードが書けるのであれば、多分検索すればすぐに理解できると思います。

>どこで使用しているのでしょう?
mGameList.remove_if(pred) で使われています。remove_if は「条件を満たす要素を list から削除する」という操作を行います。
その条件というのを、remove_if の引数として使います。今回 pred は「GameDataList の mMaterial が NULL かどうか」というのを行っているので、remove_if(pred) は「GameDataList の mMaterial が NULL の要素を list から削除する」という操作になります。

remove_if の中身はこんなイメージですね
template<class F>
void list::remove_if(F pred) {
    list::iterator it = begin();
    while (it != end()) {
        if (pred(*it)) erase(it++);
        else ++it;
    }
}
重要なのは pred がテンプレート引数として渡されていることと、pred(*it) と呼び出していることで、つまりは pred は pred(*it) と呼び出せるものなら何でも受け取れます(例えば今回みたいに operator() をオーバーロードしたオブジェクトとか)。
今回は関数オブジェクトを使いましたが、別に
bool pred(const GameDataList& d) { return d.mMaterial == NULL; }
...
mGameList.remove_if(pred);
でも構わないです。関数ポインタが分かるならこれは分かりますよね。
今回は変更する場所を伝えるのをできるだけ少なくしたかったので、ローカルで構造体を作って operator() をオーバーロードして渡していたのですが、逆に分かりにくくなっちゃいましたね。

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 22:20
by 迷彩吹雪
>めるぽんさん
>erase しても、GameListData が破棄されるだけで、GameListData の先の要素までは delete されません
よくよく考えてみれば確かにそうです。listには要素の中身なんて知った事ではありませんよね。何の裏付けもなく自動的にdeleteしてくれるものと思い込んでいました。今までメモリーリークしなかった事が奇跡ですね。

関数オブジェクトについては始めて知りました。構造体そのものに関数の機能を与える、という理解で良いのでしょうか(あれ、でもそれってクラスじゃ……?)。
実は関数ポインタも名前を知っているだけではっきり理解はしていないような輩だったりします。

ひとまず教えていただいた内容を参考に以下のようにコードを直してみたのですが、
if((*i).mMaterial == M){
    //if(i == ite){
    //    ite++;        //タスク実行中の要素を削除しないように一つ進める
    //}
    //mGameList.erase(i);
    delete (*i).mMaterial;    //インスタンスを破棄して
    (*i).mMaterial = 0;    //0を代入しておく
    return;
}
こうすると、delete演算子がイテレータiを破棄しようとしているのか、プログラムが止まってしまいました(「iterator not incrementable」と警告が表示されたので)。
deleteのある一行をコメントアウトすると動作しました。自弾は減速せず、自機自身が被弾することもなくなりました。
しかしそれではインスタンスは削除されないままになってしまうので、どうにかしたいところ。
現在進行形で対処法を探していますが、まだ見つかりません……。

しかし解決の目処が立ちました。本当にありがとうございます!

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月22日(金) 22:57
by めるぽん
>プログラムが止まってしまいました
そうやって書いている場合、RemoveMaterial(this); とするのは言ってみれば delete this; ってやってるようなものなので、それ以降メンバにアクセスするのはダメです。
ということで RemoveMaterial(this) の直後に return するようなプログラムにするとか、GameDataList に生きているかどうかのフラグを用意しておいて、NULL かどうかじゃなくてそのフラグを見るようにしてやって、remove する際にフラグが落ちてる要素の mMaterial を delete してから要素を削除するとかあると思いますけれど、どれも一長一短ですね。

タスクリストを使う場合の要素の削除は結構鬼門な部分だと思うので、頑張って下さい。

Re:画面端でのみ自分で撃ったショットに被弾する

Posted: 2010年10月23日(土) 01:15
by 迷彩吹雪
あ……何という初歩的ミス。

しかしGameListData構造体に削除するかどうかのフラグを置いて、mMaterialのメンバ関数処理が終わってからdeleteしようとしても止まってしまいました。
delete (*i).Mmaterial;//エラー
mMaterialの持つアドレスを一時変数に取り出してからdeleteしても、その瞬間停止。
BasicGameMaterial* G = (*ite).mMaterial;//アドレスをコピー
delete G;//コピーしたアドレスを介してインスタンスを破棄(ここでエラ-)
リスト内の要素をdeleteすることは不可能なのでしょうか……。

それはともかく本題から逸れ始めたので、一旦解決にしておきます。