yumeの日記: Unity制作 メデューサ・ゲーム #2 1
●補習
昨日のコレだが:
public int playerLayer = 9;
int layerMask = ~(1 << playerLayer); //Exclude layer 9
++C++;曰く、
・<<は左シフト(これはまぁ覚えてた)
・~は補数
例えば二進数000111の0と1を逆転し、111000にする。
で、例えば
int D (1 << 5) //32
int rD = ~D //-33
となるようだ。ここではintを使っているが、bitに直すと
0b11111111111111111111111111011111
になり、符号付10進数であるintは、これを-33と読むらしい。
たぶんUnityのLayerMaskのとらえ方はすごく賢くて、intで与えられた数字を二進数に読み替えている(厳密にいえば、機械はそもそも二進数で読んでいるんだろうけど)んだろう。
だから、0b11111111111111111111111111011111は「5番目だけが0、ほかは1」、つまりレイヤーマスク5以外すべてOKとして扱えるようだ。
--
●メデューサ・ゲーム
石化の続き
石化について実装したい機能をまとめて考える
・石化進行中は視覚的にそれがわかるようにしたい
・メデューサに見られていない間は、石化の進行が止まるのではなく、徐々に元に戻していきたい。
うーむ、現状では直接GetEvilSeeingメソッドを見られるたびに実行するが、そうではなく、見られているときは「邪視状態」をTrueにして、
邪視状態中は徐々に石化が進行する、ということにしようか。
private void CheckEvilSeeing()
{
if (evilSeeing)
{
evilDamage += Time.deltaTime;
if (evilDamage > resistPower)
{
evilDamage = 0.0f;
Debug.Log("石化しちゃった!");
}
evilSeeing = false;
}
else
{
evilDamage = Mathf.Min(0.0f, evilDamage - Time.deltaTime);
}
evilSeeing = false;
}
このメソッドをFixedUpdateメソッドで実行すれば、毎Fixedフレームごとに、evilSeeing状態ならば石化ダメージを負い、evilSeeingでないなら石化ダメージを徐々に回復する。
フレームの最後で状態をfalseにしておけば、「メデューサに見られたフレーム」だけevilSeeing状態になるはず。
evilDamageに応じて、一旦スプライトの色を青色に変えていこう。evilDamageが0なら正常な色。
そんで、実際の石化処理を行う。
これは石化したSoldierをDestroyするか、「石化状態」というべき状態に切り替えてしまえばよさそうだが、どっちがよいだろう。
Destroyする場合:
・メデューサ側でStart時にキャラクターの数を数え、それを配列にしているが、この配列が可変にならなければならないので、Listにする必要がある。
・ListからDestroyされるタイミングで削除する。
・Destroy後に、自分の石化した状態のSpriteだけを持つGameObjectをそこに置く。
Destroyしない場合:
・自分の状態を「石化状態」にする。
・様々なコンポーネント(RigidBody2Dとか)を無効化する。
・Spriteを「石化状態」に変える。
ん~。後々Soldierが後から増えたりする場合は、どちらにせよリスト化する必要はでてくるし、「様々なコンポーネントを無効化」が後々大変そうなので、
ここはDestroy + Listから削除 + 死体オブジェクトを残す方式にしよう。
メデューサが持つcharactersリストには、個々のGameObject情報が入っている。そこにはインスタンスIDもある。
Soldierが破壊される直前に、メデューサのリストから自分のインスタンスIDを削除するよう命令しよう。
ただ、それをFixedUpdate中にやるといろいろややこしそうだ。
公式曰く、
どうやらイベントの中には「OnDestroy」なるものがある。ここでもろもろの処理をすればよいだろう。
で、リストから自分のインタンスIDに合致するものを削除する、だが
リスト自体はGameObject型のリストで、GameObject型からインスタンスIDをGetする関数はあるものの、
直接リストからRemoveするメソッドがないかな。Remove()はGameObjectそのものを指定しなきゃならないし。
public void DeleteFromList(int instanceID)
{
for (int i = 0; i < characters.Count; i++)
{
int checkID = characters[i].GetInstanceID();
if (checkID == instanceID)
{
characters.RemoveAt(i);
return;
}
}
}
で、これを石化時にSoldier側から呼び出す。呼び出す場合はまずメデューサを名前で検索してから……。
なんだか回りくどいな。例えばGameManagerみたいなオブジェクトがいて、すべてのCharacterのリストはそいつが持ってて、すべてのキャラクターはそいつへの参照をもって生まれたらもうちょいスマートかな。でもとりあえずこれでいこう。
これで動くかな?と思ったがダメ。エラー:
NullReferenceException: Object reference not set to an instance of an object
EvilEye.EvilEyeSeeing () (at Assets/Script/EvilEye.cs:38)
リスト削除メソッドの起動を、Destory時ではなくFixedUpdate中に移すとうまくいった。
さてなぜだろう。
OnDestroyでの処理の流れは:
・FixedUpdate(Player):Soldier[i]のevilSeeingをTrueに。
・FixedUpdate(Soldier[i]):evilSeeingがTrueなので、石化ダメージを追加。
・同:石化ダメージが抵抗力を超えたので、SoldierをDestroy
・OnDestroy(Soldier[i]):
・デバッグログを流し
・メデューサの名前で検索
・メデューサのリストを含むスクリプトを取得
・スクリプトにDeletFromListを実行させる
ところが、OnDestroyのデバッグログが流れる直前にエラーがでる。
ん~。例えば、あるフレームにおいて、SoldierのDestroyが実行された後に、PlayerのFixedUpdateが実行されて、そのあとにSoldierのOnDestroyが実行される、という順序になった場合、Player側では「すでにDestroyされたオブジェクトを探す」ということをやろうとしてしまうか。
一方、OnDestroyを使わない場合は
SoldierのFixedUpdateでリストからの削除も同時にされ、そのあとPlayerのFixedUpdateが走ってもすでにリストにはないので、問題はない。
逆に、PlayerのFixedUpdateで検索した後に、Soldier側から削除命令が出ても、やはり問題はない……か。
なら順序を問わない方法がいいか。
それで、OnDestroy時にStatueオブジェクトを生成する。
Qiita @Teach曰く、
・Instantiate(obj, this.transform.position, Quaternion.identity); みたいな感じでその場に生成できる。
生成はできたが、Unityになんか怒られる。
テラシュールブログ曰く、
・OnDestroy中にInstatiateするとゴミが残る。
・シーン停止時に、すべてのオブジェクトでUnityがOnDestroyを実行するため
だそうだ。回避策は、OnApplicationQuitかどうかを判別するか、単にOnDestroyでなんか生成するのをやめればよい。やめよう。
~ は「ビット反転演算子」 (スコア:0)
「~は補数」のところ、正しくは「~はビット反転」です
補数には「1の補数」とか「2の補数」ってのがあって
1の補数がビット反転で計算できるだけです
「n番目だけが0、他は1」な値を作る時に
「n番目だけが1、他は0」という値を (1<<n)で生成して (nはゼロオリジン。値は0,1,2,3,4...)
それのビット反転を行うことで「n番目だけが0、ほかは1」という整数値を作ります
これはプログラムの常套手段です
詳しいことは「ビット演算」とかでググってください
これを補数と言ってしまうと多くの人は混乱します
なぜなら、計算しているのは「ビット反転した値」であって補数ではないからです
(たまたま1の補数と一致してるだけ)