いきなりまとめ
- MainモジュールのSimulation Space はWorldに
- EmissionモジュールでBurstsの設定をすることで水を一発ずつ打てる
- CollisionモジュールでSet Messageを有効にする. Life Time Loss を1にする
- Particle Systemのパーティクルとの衝突によって呼び出されるコールバック関数はOnParticleSystem(GameObject)であって、OnCollisionEnter(Collision)ではないことに注意
今回やること
前回の記事で水鉄砲のシェーダーはできたので、今回はクリックしたら水(パーティクル)を発射できるようにしましょう。
MuzzleとParticle Systemを子要素として追加
まずはMuzzleと水を発射するためのParticle Systemを水鉄砲の子要素として追加しましょう。階層構造は次の図のようにしました。 ここで紛らわしいのですが、ここでいうParticle SystemはParticle System Componentを付与されたGameObjectです。ということはわざわざMuzzle->ParticleとせずMuzzleに直接Particle Systemを付与したほうが良いのかもしれません。いたずらに階層構造を複雑にするのは良くないと思いますが、今回はGrip用のゲームオブジェクトとの階層を揃えるためにこのような構造にしました。Muzzleの位置は水鉄砲の銃口の位置にしましょう。ParticleのTransformのPosition(直上の親オブジェクトからの相対位置、すなわちMuzzleからの相対位置)は(0,0,0)のままでいいでしょう
Particle Systemの設定
続いてParticle Systemの設定をしていきましょう。今回適用するモジュールはMainモジュール、Emissionモジュール、Size Over Lifetimeモジュール、Collisionモジュール、Trailsモジュール、Rendererモジュールの6モジュールです。
Mainモジュール
必ずしなくてはいけない設定はLooping、Play On Awakeを無効にすることとSimulation SpaceをWorldにすることだけです。Start Lifetime、Start Speed、Start Sizeはお好みで。
Emissionモジュール
水鉄砲という性質上、ボタンをクリックしたときにだけパーティクルを発射したいのでRate over Time、Rate over Distanceはいずれも0にしておきます。その代わりBurstsの設定をすることで、クリックしたときにパーティクルが1つだけ発射されるようにしました。
Size over Lifetimeモジュール
ゲームエフェクトでよく取られる手法らしいのですが、パーティクルの寿命とともにSizeを小さくしていったほうがエフェクトがより効果的になるそうです。関数はどうでもよいと思いますが、とりあえずLifetimeに対して単調減少な関数を適用させます。
ゲームエフェクトやParticle Systemに関しては次の本がとても参考になりました。
Collisionモジュール
TypeをWorld、Modeを3Dにしておきます。また、Send Collision Messageを有効にしておくことでコールバック関数を呼び出せるようにします。必須ではないすがで衝突後水が消えるようにLifetime Lossは1にしておきましょう。このLifetime Lossですが、単位は秒ではなくパーティクルのStart Lifetimeに対する割合で指定するそうです。とりあえず1にしておけば衝突後パーティクルが自動で消滅してくれます。
Trailsモジュール
このモジュールでは軌跡を設定することが出来ます。これを設定することでタイムラプスで撮影したようなエフェクトをつけることが出来るので非常に嬉しいです。正直、よく設定が分からないのでモジュールを有効にするだけで各項目は特にいじってません。
Rendererモジュール
このモジュールも項目が多いですが、今回はパーティクルとTrailsのマテリアルを指定するだけです。水っぽいマテリアルとして、Unityのアセットストアで無料で配布されているParticle PackのWaterDropというマテリアルを使用しました。
以上でParticle Systemのモジュールの設定は終了です。
スクリプトの実装その1-クリックで水を発射する-
続いて、水鉄砲をピックアップ中にクリックしたらパーティクル(水)を発射できるようにしましょう。前回WaterGunに付与したスクリプトを次のように改造します。
using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; public class Watergun : UdonSharpBehaviour { private Material mt; private float wa; private float ini; private ParticleSystem childps; private Transform childtrans; void Start() { mt = GetComponent<Renderer>().material; wa = mt.GetFloat("_WaterAmmount"); ini = wa; childtrans = transform.Find("Muzzle/Particle"); Debug.Log(childtrans.position); childps = childtrans.gameObject.GetComponent<ParticleSystem>(); } public override void OnPickupUseDown() { LowerWater(); EmitWater(); } public override void OnDrop() { mt.SetFloat("_WaterAmmount", ini); wa = ini; } private void LowerWater() { wa -= ini/10.0f; mt.SetFloat("_WaterAmmount",wa); } private void EmitWater() { childps.Play(); } }
変更点としては、Start()内でParticleのインスタンスを取得しておくことと、クリック時にEmitWater()を呼び出しParticle Systemを再生することだけです。
スクリプトの実装その2-Collision時のコールバック-
最後に、衝突時に呼びだされるコールバック関数を実装します。テスト用にCubeオブジェクトを用意し、パーティクルが衝突したらこのCubeの色を赤色にしてみましょう。 Particle Systemにスクリプトを付与するとしたら、次のような実装になると思います。
// For Particle System using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; public class Particle : UdonSharpBehaviour { void Start() { } void OnParticleCollision(GameObject other) { other.GetComponent<Renderer>().material.color = Color.red; } }
OnParticleCollision()の引数としてGameObject(今回はCubeを想定)を受け取り、このGameObjectのRendererコンポーネントのmaterial.colorにアクセスします。実装してから思ったのですが、Cubeの方で同じような処理を行ったほうが自然な気がします。そこで、結局はParticle Systemの代わりにCubeに次のスクリプトを実装しました。
// For Cube GameObject using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; public class Cube : UdonSharpBehaviour { void Start() { } // You need to use OnParticleCollision(), not OnCollisionEnter() void OnParticleCollision(GameObject other) { var mtr = GetComponent<Renderer>().material; mtr.color = Color.red; } }
実はパーティクルとの衝突時に呼び出される関数はOnCollisionEnter()ではありません。正しくはOnParticleCollision()です。引数の型も微妙に違うので気をつけましょう。
ずっとOnCollisionEnter()が呼び出されると思っていたので、2、3時間溶かしましたが、ちゃ~んと公式リファレンスに書かれていました。
いいかんじにできました streamable.com