ウチツクシ

ゲームをしたり作ったり

HSPの物理エンジンOBAQのコリジョン処理でハマったこと

去年OBAQ使ったゲームを作ってる時にコリジョン関係でバグを何度か直して「もう直しきったはず!」と安心しきってたけど、今日やってみたらまだ残っててうんざりしたので、整理がてらメモ残しておきます。今後OBAQ使って同じような処理するゲーム作ることがあったら、あと同じようにうんざりしている人が他にいたら、役に立つかも。

初めに、あるオブジェクトAに他のオブジェクトBが当たったらBが消えてスコアが100入るケース。

#include "obaq.as"

    screen 0, 320, 240

    qreset
    qgravity 0, 0

    ; オブジェクトA設置
    qaddpoly target, 4, 20, 30, 0, 10, 20, , 1, , 2
    qtype target, type_bind

    score = 0
    fcnt = 0

*mainLoop
    redraw 0
    color : boxf

    qexec

    ; 60フレーム毎にオブジェクトB出現
    if ((fcnt \ 60) == 0) {
        qaddpoly obj, 4, 60, 30, , 4, 4, , 2
        qinertia obj, 1
        qspeed obj, -0.2
    }

    ; Aに当たったら消えてスコア加算
    qcollision target
    qgetcol i, x, y
    if (i >= 0) {
        score += 100
        qdel i
    }

    qdraw

    ; 情報表示
    color 255, 255, 255
    pos 30, 20
    mes "score " + score

    redraw 1
    await 15

    fcnt++
    goto *mainLoop

左にある中心点が赤いオブジェAに、中心点が緑のオブジェBが衝突してきてAに当たると消えて100点加算される。これは大丈夫、問題ない。

次に、B が2つ同時に A に当たるケース。
上のスクリプトのB出現の箇所を以下に置き換えて。

    ; 60フレーム毎にオブジェクトB出現
    if ((fcnt \ 60) == 0) {
        repeat 2
            qaddpoly obj, 4, 60, 25 + cnt * 10, , 4, 4, , 2
            qinertia obj, 1
            qspeed obj, -0.2
        loop
    }

同じ大きさ速度のオブジェをY座標だけ変えて同時に出しているから、同時にAに当たって同時に消えて200点入っている…ように見えるけど、よく見ると同時に消えていない。
ウェイト増やして確認すると、当たったとき上が先に消えて次のフレームで下が消えている。

Bを3つに増やしてみる。

    ; 60フレーム毎にオブジェクトB出現
    if ((fcnt \ 60) == 0) {
        repeat 3
            qaddpoly obj, 4, 60, 22 + cnt * 8, , 4, 4, , 2
            qinertia obj, 1
            qspeed obj, -0.2
        loop
    }

3つとも当っているはずなのに一番下だけ消えずに跳ね返ってしまう。

これはコリジョン処理に問題があって、1フレームに1オブジェクトしか処理していないから。そのため3つ消すのに3フレームかかってしまうけど、3フレーム目にはもうAとは接触していないため3つ目のオブジェクトは消えない。

だから1フレームで接触してるオブジェ全部を処理したい。qgetcol 命令は実行するごとにオブジェクトIDを次々と返し、もう返すIDがなくなったときは -1 が返るのでそれを利用する。

コリジョン処理部分を以下に書き換えて。

    ; Aに当たったら消えてスコア加算
    qcollision target
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        score += 100
        qdel i
    loop

if のところは (i = -1) の方が分かりやすいけどHSPTV部門的に…接触しているオブジェを全部処理して、-1 が返ったらループを抜ける。これで1フレームで全部消えてくれるはず。
実行するとちゃんと消えてる。ウェイト増やして見てみても間違いない!


実はここからが本題です…

実際作ってるときはここまでは実装しててゲームもほぼ完成間近で。大体なったりならなかったりして、スコアもずっと変化してるから気付かなかったよ…

さっきの、同時に消えるようにはなったけど、スコアが何かおかしい。オブジェ1つにつき100点だから3つで300点のはずが600点入ってる。ということはコリジョン処理の i が6回オブジェIDを返してるってことになる。調べてるオブジェは3つしかないのに… i を調べてみると同じIDを2回づつ返しているようだ。qdel が失敗している?OBAQ内部の計算回数の問題?

あーでもない、こーでもないと色々試していると、オブジェBを三角形にしたところで原因判明の糸口が見つかった。

            qaddpoly obj, 3, 60, 22 + cnt * 8, , 4, 4, 0, 2

すると300点入るようになった。もしかして頂点ごとに判定取っているのでは…?

ここで qgetcol 命令にコリジョンが検出された座標を返すパラメータがあったことに気づく(はやく気づいていれば…)同じIDを返すなら座標調べると何か分かるかもしれない。

コリジョン検出の ID と位置が分かるように、あとスペースキーで遅くして確認しやすくするように *mainLoop 以降を書き換え。

*mainLoop
    redraw 0
    color : boxf

    qexec

    ; 60フレーム毎にオブジェクトB出現
    if ((fcnt \ 60) == 0) {
        repeat 3
            qaddpoly obj, 4, 60, 22 + cnt * 8, , 4, 4, , 2
            qinertia obj, 1
            qspeed obj, -0.2
        loop
    }

    ; Aに当たったら消えてスコア加算
    qcollision target
    hitIDs = ""
    ddim hitPosX, 16
    ddim hitPosY, 16
    hitNum = 0
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        hitIDs += strf("%d ", i)
        hitPosX(hitNum) = x
        hitPosY(hitNum) = y
        hitNum++
        score += 100
        qdel i
    loop

    qdraw

    ; コリジョン位置表示
    color 255, 255
    repeat hitNum
        qcnvaxis x, y, hitPosX(cnt), hitPosY(cnt)
        circle x - 4, y - 4, x + 4, y + 4
    loop

    ; 情報表示
    color 255, 255, 255
    pos 30, 20
    mes "score " + score
    mes "hitIDs " + hitIDs

    redraw 1

    ; スペースキー押しっぱなしで遅く
    getkey k, 32
    if (k) {
        await 1000
    } else {
        await 15
    }

    fcnt++
    goto *mainLoop

四角形の頂点部分が検出されている。やっぱり頂点ごとみたいだ。
やっとスコアが余計に入る原因が分かったので、解決処理を考える。
同じ ID が続いているから直前の ID をスキップするようにしてみる。以下コリジョン処理部分。コメントのある行が追加箇所。

    hitNum = 0
    preID = -1 ; 追加
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        if (i == preID) : continue ; 追加
        preID = i ; 追加
        hitIDs += strf("%d ", i)

これでバッチリ!と喜びも束の間、失敗するケースが浮上。
以下 B 出現部分の qaddpoly のところ。四角形の角度を微妙に変えている。

            qaddpoly obj, 4, 60, 22 + cnt * 8, 0.06, 4, 4, , 2

ID の検出順が 2 3 4 2 3 4 。これだと同じ ID を取ってしまう。それじゃあ「前回以下の ID をスキップ」だとどうだ。一応小さい順に並んでいるようだし…コリジョン処理部分。== を <= に変えただけ。

        if (i <= preID) : continue

もう完璧!

じゃなかった…
またまた B 出現部分。角度は戻して今度はスピードを微妙に変える。

        repeat 3
            qaddpoly obj, 4, 60, 22 + cnt * 8, , 4, 4, , 2
            qinertia obj, 1
            qspeed obj, -(0.2 + 0.002 * cnt)
        loop

一見何も問題ないように見える。スコアもちゃんと入ってる。スペースキーで遅くして確認すると一番下のオブジェが先に消えて次のフレームで上2つが消えているけど、下のオブジェほど速くしたからそうなるのは問題ないように見える。でもこれ本当は1フレームで全部消えないといけないケース。

コリジョン処理部分。さっき変えた <= を再び == に戻す。

        if (i == preID) : continue

今度は1フレームで3つとも消えている。ID の順番は 4 3 2 。小さい順に並んでいるわけではなかった…

もうこうなったら最終手段である。リストを作り、ID 検出ごとに同じ ID がないか探して、あったらスキップ、なかったらリストに入れるという方法。本来は数値の配列でリストを作るべきなんだろうけど、今回は表示用の文字列型変数 hitIDs を利用。以下コリジョン処理部分。

    ; Aに当たったら消えてスコア加算
    qcollision target
    hitIDs = " "
    ddim hitPosX, 16
    ddim hitPosY, 16
    hitNum = 0
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        if (instr(hitIDs, 0, strf(" %d ", i)) >= 0) : continue
        hitIDs += strf("%d ", i)
        hitPosX(hitNum) = x
        hitPosY(hitNum) = y
        hitNum++
        score += 100
        qdel i
    loop

疲れた…多分これでもう大丈夫。多分…(2013.10.26 ちょっと間違ってたので修正)

コリジョンの判定をオブジェクト単位だと勘違いしてたのが今回ハマった要因。ゲーム作る前からテストしとけば良かったなあ… ちなみに XenMai は ax サイズがもうパンパン(HSPTV部門の条件)なので最終手段まではやっていない。+R の方はサイズ関係ないけど、既にスコアランキング登録してあってスコアのルールが途中で変わるのもマズイと思い、こっちも直さないつもり。

(追記)
qdel 実行しているのに同じ ID が検出される問題忘れてた。命令が失敗してるのかと思っていたけど、stat を見ても正常な値が返っている。これもすごい勘違いしてて、OBAQ が qgetcol で衝突チェックの演算をしている、のではなくて、実際に衝突演算しているのは qexec 部分で、qgetcol はそこで記録したログを読み取っているだけ。qdel はオブジェクトを削除するだけでログは消さないから、qdel を実行しても同じ ID が検出されるのだろう。説明で「コリジョンログ」と書かれている意味がやっと分かったよ…

あと最終手段の別の方法を思いついたのでメモ。
初回 ID 検出時に quser でデータ入れておき2回目以降はそのデータがあればスキップするという方法は、qdel が実行されるとデータがクリアされてしまうので使えないと思っていた。けど、qdel ですぐ削除するのではなく一旦別の仮グループに割り当てて quser でデータを入れ、あとでまとめてグループ検索で削除する方法で解決できた。なんかすごい回りくどい方法だけど、スクリプト側でリスト用の変数を作らなくて済むメリットはあると思う。

    ; Aに当たったら消えてスコア加算
    qcollision target
    hitIDs = ""
    ddim hitPosX, 16
    ddim hitPosY, 16
    hitNum = 0
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        qgetuser i, u, dummy, dummy
        if (u == 1) : continue
        hitIDs += strf("%d ", i)
        hitPosX(hitNum) = x
        hitPosY(hitNum) = y
        hitNum++
        score += 100
        qgroup i, 4
        quser i, 1
    loop
    qfind 4
    repeat
        qnext i
        if (i < 0) : break
        qdel i
    loop

(追記 2013.11.4)
もっとシンプルな方法があった。qdel でちゃんと削除されているのだからオブジェの存在を確かめれば済むじゃないか…ユーザーデータも検索用リストもいらない。今までの苦労は何だったのか…

    ; Aに当たったら消えてスコア加算
    qcollision target
    hitIDs = ""
    ddim hitPosX, 16
    ddim hitPosY, 16
    hitNum = 0
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        qdel i
        if (stat) : continue
        hitIDs += strf("%d ", i)
        hitPosX(hitNum) = x
        hitPosY(hitNum) = y
        hitNum++
        score += 100
    loop

qdel の返り値を見る方法。qdel は実行失敗すると 0 以外の値を返すのだけど、存在しない(削除された)オブジェクトID を指定すると実行失敗するのでそれを利用する。ただちょっと心配なのが存在しているのに失敗することがあるかどうか。あるとまずい。

あと qgetstat の第2パラメータでも存在確認できそうだけど、これも存在しない ID 指定すると失敗して変数に何も代入されないから直前で変数に 0 を入れておく必要がある。

    ; Aに当たったら消えてスコア加算
    qcollision target
    hitIDs = ""
    ddim hitPosX, 16
    ddim hitPosY, 16
    hitNum = 0
    repeat
        qgetcol i, x, y
        if (i < 0) : break
        st = 0
        qgetstat i, st, dummy
        if (st == 0) : continue
        hitIDs += strf("%d ", i)
        hitPosX(hitNum) = x
        hitPosY(hitNum) = y
        hitNum++
        score += 100
        qdel i
    loop

qget系の命令は存在しない ID を指定すると変数に何も代入されないようで。これもつい最近気づいたよ…
上で qdel が実行されるとデータが削除されるって書いたけどこれも少し違って、存在しないオブジェクトのデータは参照できないから実際 qdel 実行時点でクリアされているかは分からない。テストしたときは qgetuser の直前で受け取る変数に値を何も代入してなかったから前の値が使われてて勘違いしてた。