バグの概要
ざっくり言うと
- 事象:大量のメッシュコライダを密集させた状態だと、そのGameObjectが虚空に消え去る
- **原因:**transform.positionが (NaN, NaN, NaN) になってる
- **根本的な原因:**Unity(PhysX?)のせい
- **対策:**大量に生成して、しかもそれらが超満員でぶつかり合うようなGameObjectには
メッシュコライダを使わない動的にコライダを生成すれば割といけるかも(最後に追記)
そんな感じのわりと救いようのないバグがあるみたいなのですが、「Invalid worldAABB」というがっつりコンソールに表示されるワードでググってもそれっぽい記事が一向に出ないので困りました。
以下、色々調べて分かったことを書きます。
発生手順
私の場合は、プッシャー系のメダルゲームをテスト中に発生。
使用するメダルはBlenderで適当に作ったモデルに、RigidbodyとMesh Colliderで物理演算を適用した、ごく一般的なものです。
このように、とにかく大量にメダルをフィールドに落としてみていると、最初のほうは問題なく動いたのですが、
300枚を超えたあたりで、突如としてメダルとプッシャー(メダルを押して動かす台)が消失。しかも処理が異常に重くなっています。
見ると**"Invalid worldAABB. Object is too large or too far away from the origin"**などと怒られてます。
それでtransform.positionを見ればわかりますが、直接の原因は座標がNaNになってしまってることですね。つまりなんでNaNになるのか考えなきゃいけません。(ダジャレではない)
原因の考察
NaN(非数)は0/0とか、そういう数学的に定義されてない結果に対して用いられるやつです。つまり今回の場合は、位置を計算するときにそのような演算が起きてしまっていたならば、それが原因ってことになります。
しかし原因を切り分けた結果、こちらのスクリプトにはそういうミスはないことを確信。となると物理演算以外に疑う余地はないですね。
最初はBlenderで作ったモデルが何か問題あるのかと思いましたが、Unityの円柱にメッシュコライダ付けたやつに置き換えても結局同じバグが出たので、これも関係なし。
次にこんな風に、面の数を減らした雑なモデル(手前)を作ってこちらをメッシュコライダとして設定してみました。
これで何度か試してみると、800枚程度までは問題なくなりましたが、やっぱり同じエラーが発生。
ということは、メッシュコライダによる衝突回数が多くなるほどこの問題が発生する可能性が上昇するのだと推測できます。すなわちメッシュコライダそのものが原因であるってことになります。
あと大量のメダルが一気に消え去るのは、同時にこのバグが発生するとかではなく、どっかのメダルの座標がNaNになるとそれと隣り合っていたメダルの物理演算にもNaNが使用されるせいで、そのメダルの座標も"汚染"されるせいかな? たぶん、ボックスコライダしか持たないプッシャーも同時に消えてしまうのは、これが原因。
あとで調べると同じようなバグは報告されていましたが、Won't Fixって……。
https://issuetracker.unity3d.com/issues/assertion-failed-invalid-aabb-a-error-appears-after-multiple-object-collision
バグを回避
というわけで、Unityを使用する以上どうしようもないバグである、かと思われます。なんてことだ。
メッシュコライダを簡単にすればそれだけ多くの枚数があっても起こりにくくはなりますが、「何枚まで大丈夫」などという保証がないため(バグる枚数は同じ形状のコライダでも毎回5割くらいの範囲内で変わる)、それでは安心できません。
座標がバグったGameObjectはfloat.IsNaN()
などを使用して判断できます:
bool HasInvalidPosition(){
return float.IsNaN(transform.position.x) || float.IsNaN(transform.position.y) || float.IsNaN(transform.position.z);
//今回は全要素がNaNになるからxだけでもいいけど
}
……が、それをしたからと言って「Destroyする」か「一度Is Kinematicにして座標を代入し直す」かしないと処理落ちしっぱなしになります。
しかし、いずれの方法もメダルゲームとしては信頼性を損なうものなので、結局のところバグそのものを回避するしかありませんでした。
すなわち、メッシュコライダを使わないという方法しかありません。
このようにボックスコライダをいくつか指定すると、何千枚置いたところでバグらず、かつそれっぽい挙動をするようになりましたが、処理自体はこちらのほうが重くなってしまうという始末。しかし背に腹は代えられない……。
無論、このゲームのように「大量のオブジェクトに対してすべてメッシュコライダを設定すべきであり、しかもそれが密集する」というようなものでなければメッシュコライダを避ける必要はないはずです。
もしかすると、これがなかなか検索しても同じような状況に陥っている記事がない原因なのかもしれませんが……。
まだだ、まだ諦めんぞ!
しかしボックスコライダでそれっぽく指定したところでむしろ重くなるし、動きも微妙にぎこちないし、こんなことで満足していいのか!?
もしかしたらメッシュコライダはもっと最適化できるかもしれないじゃないか!
というわけで動的にコライダ用のメッシュを生成します。
using UnityEngine;
public class PolygonalMesh : MonoBehaviour
{
public int angle = 12; //何角形か
public float radius = 1f; //半径
public float height = 2f; //高さ
void Start()
{
//頂点を配置
Vector3[] vertices = new Vector3[angle * 2];
for (int i = 0, j = 0; i < vertices.Length; i += 2, j++)
{
float rad = 2 * Mathf.PI * j / angle;
vertices[i + 0] = new Vector3(Mathf.Cos(rad), -height / 2f, Mathf.Sin(rad));
vertices[i + 1] = new Vector3(Mathf.Cos(rad), height / 2f, Mathf.Sin(rad));
}
//メッシュを定義
int[] triangles = new int[angle * 6 + (angle - 2) * 6];
for (int i = 0, j = 0; i < angle * 6; i += 6, j++) //側面
{
int k = (j == angle - 1) ? -1 : j;
triangles[i + 0] = j * 2;
triangles[i + 1] = j * 2 + 1;
triangles[i + 2] = k * 2 + 3;
triangles[i + 3] = j * 2;
triangles[i + 4] = k * 2 + 3;
triangles[i + 5] = k * 2 + 2;
}
for (int i = angle * 6, j = 0; i < triangles.Length; i += 6, j += 2) //上下面
{
triangles[i + 0] = 0;
triangles[i + 1] = j + 2;
triangles[i + 2] = j + 4;
triangles[i + 3] = 1;
triangles[i + 4] = j + 5;
triangles[i + 5] = j + 3;
}
Mesh mesh = new Mesh();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
GetComponent<MeshCollider>().sharedMesh = mesh;
}
}
自分でも書いててワケわかんなくなってきたんですが、やってることは「正多角柱のメッシュを作って、それをコライダとして設定」です。一応どんな形の正多角柱でも作れるようにしてあるからご自由にお使いなせぇ。
ちなみに、メッシュとか直接触ったことなかったんで、こちらの記事を参考にさせていただきました。
https://sleepygamersmemo.blogspot.com/2017/04/unity-mesh-square.html
さて、こいつをメダルにアタッチすると……
こんな風にちゃんと綺麗な12角柱になります(最初はちゃんと緑枠が三角形で全部表示されてたんですが、再現できなくなりました。なぜでしょうか)。
ボックスコライダでやるとわずかに凹な部分ができてしまい、そこに他のメダルが刺さると描画が重なってしまうので、それを防げるのもポイント高し。
自信ありげに壁で囲っちゃったりなんかして、1000枚以上ぶち込んでやったんですが、一向にバグる気配ナシ。
また、ボックスコライダでやった際は8角形で妥協させながらもかなり処理落ちが激しかった(1000枚だと私の環境で20fps前後)ですが、今回は正確に12角形でありながら70fps程度をキープしており完璧。これは……やったか!?
やっぱダメじゃん
などと嬉々として記事を書いていた矢先、ゲーム画面を再び出すと
消えてんじゃん。
まあ、今回はワザとメダルを溜めてやったし……
1000枚も存在していながらバグるまで1分以上はかかったから、発生確率もかなり低下してるし……
普通のプレイなら多くて300枚は溜まらないようにするし……
ボックスコライダの方法は処理落ちしすぎてそもそもダメだし……
つまり「動的にコライダ生成する」方法は、根本的ではないが現実的な解決策にはなりうるということが分かりました。
これでどうかひとつ、妥協できませんかね?
とはいえ重大なバグに対処療法しかないというのもどうにもむず痒いので、根本的に修正されることを期待します。