VRで焚き火する #14 ポリモーフィズムで使い捨てライターに火をつける【Unity】
前回の記事では、薪オブジェクトを燃やして時間経過でじわじわ小さくする実装をしてみました。
Ignisの機能で薪オブジェクトに火をつけることはできますが、好きなタイミングで火をつけれる物があると便利ですね。
今回は、焚き火に火をつけるためのライターをつくります。
実装では、少しだけオブジェクト指向っぽいことをしてみました。
ポリモーフィズムという性質を使っていますよ。
目次
開発環境
・Unity 2020.3.15f2(High Definition RP 10.5.1)
・CPU:AMD Ryzen 7 3700X
・グラボ:ASUS ROG-STRIX-RTX2060S-O8G-GAMING
・Meta Quest 2(Oculus Link利用)
・炎アセット:Ignis – Interactive Fire
(1) ライターのアセットを探す
まずは、ライターのアセットを探します。
テントと同じメーカーでした
いつものようにアセットストアを見ていると、とても良いライターを見つけました。
以前の記事で、テントアセットを購入したのと同じメーカー(devotid様)だったようです。
かなりクオリティ高いです。
細かいところまで作りこまれていますね。
これで無料とは。
(2) ライターに Ignis を設定する
次に、ライターに炎アセット Ignis を設定して、火がつくように設定していきます。
基本的な設定方法は、薪オブジェクトと同じですよ。
追加後、「Rigidbody -> Collision Detection」の設定を Continuous Dynamic に変更しておきます。これで、ライターが地面を突き抜ける可能性を減らせます。たまに突き抜けてしまいますが。
ライターに直接 Ignis をつけると炎が少ない?
次に、Ignis を設定していきます。
最初試した時は、ライターの Fuel Adjusterオブジェクト(ガスが出て火がつく部分)に「OAVA-Convert -> Flammable Object」を実行してました。
ですが、このオブジェクトに設定した状態では Flame VFX Multiplier の値を大きくしても、実際に表示される炎の量が少なくなってしまいました。
どうやら、オブジェクトから出る炎の数は「Ignis を設定したオブジェクトの大きさ」でベースが決まるようです。Fuel Adjusterオブジェクトはかなり小さいので、炎が少なくなったようですね。
Ignis用に「見えないオブジェクト」を追加する
これを回避するため、Ignis用に「そこそこ大きい見えないオブジェクト」を追加することにしました。
まず、Hierarchy でライターオブジェクトに「3D Object -> Cube」を追加し、名前を Flame に変更します。
次に、作成したFlameオブジェクトに対して、以下の設定をやっていきます。
・「OAVA-Convert -> Flammable Object」を実行する。これで Ignis が使えるようになります。
・Ignis を適当に設定する。「Set This On Fire On Start:チェックあり」は必須です。
・Box Collider をオフにする。
・Flameオブジェクトの下に Point Light を追加して、適当に設定する。明るさはほどほどにしておく。
・Mesh Renderer のチェックを外します。
これで、オブジェクトが見えなくなります。
これでオブジェクトの設定はOKです。
この状態で、ライターをつけてみました。
Flameオブジェクトを「アクティブ」にすると、Ignis が発動して火が出ます。逆に、非アクティブにすると火が消えます。
(3) 新しくスクリプトを作る
どんなスクリプトを作れば良いでしょうか。
まずは、ライターをオン/オフする仕様を考えてみます。
仕様を考える
以前の記事で、コントローラやレーザーで薪オブジェクトをつかむことができました。
「タグに “Grab” が設定されているオブジェクト」をつかめるよう実装していましたので、ライターにも同じタグを設定すればつかめそうです。このつかむ処理は以前と同じスクリプトが使えるので、追加も必要ありません。
少しの設定変更だけで、ライターをつかめることは分かりました。
今回は、右ハンドトリガーを押してライターをつかんだ状態で「更に右トリガーも押したらライターをつける」という仕様にしましょう。この部分だけを追加実装すれば良さそうですね。
ちなみに、右ハンドトリガーは中指で押すトリガー、右トリガーは人差し指で押すトリガーです。
オブジェクト指向っぽく実装してみる
この「更に右トリガーも押したら、○○をする」という操作ですが、つかんでいる物の種類で「○○の内容が変わる」ようになると便利な気がしました。
例えば、ライターの時は「ライターをオン/オフする」ですが、別のもの、例えば薪オブジェクトをつかんでいた場合は「薪オブジェクトを消す」みたいな感じですね。どちらも同じ操作ですが、その結果(スクリプトの処理内容)が違います。
それらを踏まえて、今回は少しだけオブジェクト指向っぽい実装をしてみました。
ポリモーフィズムという性質を使っています。
[実装1] イベントメソッド用のインターフェイスを作る
まずは、インターフェイスというものを作ります。
インターフェイス(interface)は、普通のクラスとは違って「こういう名前のメソッドが絶対に必要です。中身は空なので、別のクラスで必ず実装してください」という宣言だけを行います。
[ITriggerAction.cs]
namespace Amaotolog { /// <summary> /// TriggerActionインターフェイス /// </summary> public interface ITriggerAction { // オブジェクトをつかんだ状態で、更にトリガーを押した場合の処理 void DoubleTriggerAction(); } }
6行目で、インターフェイスを定義しています。public class ではなく、public interface になってますね。
9行目で、メソッド名(DoubleTriggerAction)と返り値の型(void)を宣言しています。本来であれば、{ } の中に処理を書きますが、なにもありませんね。
なお、今回は引数なしですが、メソッドに引数が必要な場合はそれも宣言します。
インターフェイスの処理はこれだけです。シンプル。
[実装2] オン/オフ用のライタークラスを作る
次に、オン/オフ用のライタークラスを作ります。
このクラスの DoubleTriggerActionメソッド を実行すると、先ほどライターに設定した Flameオブジェクト のアクティブ状態を切り替えることができます。
[Lighter.cs]
using UnityEngine; namespace Amaotolog { /// <summary> /// ライタークラス /// </summary> public class Lighter : MonoBehaviour, ITriggerAction { // アクション処理(火を点ける/消す) public void DoubleTriggerAction() { // アクティブ状態を切り替える GameObject obj = transform.Find("Flame").gameObject; obj.SetActive(!obj.activeSelf); } } }
8行目で、Lighterクラスを定義し、「:」以降で「何を継承するか?」を宣言しています。
MonoBehaviour は、クラスを作った時に自動で追加される「すべてのUnityスクリプトの親クラス(基底クラス)」ですね。
ここで大事なのは、その次の ITriggerAction です。Lighterクラス が ITriggerActionインターフェイス を継承することで、このクラスには DoubleTriggerActionメソッド が存在することが保証されます。
もちろん、ITriggerActionインターフェイス では DoubleTriggerActionメソッド の中身は空(未実装) でしたので、中身はこのクラスで実装する必要があります。それが11~16行目ですね。
14行目で、ライターの子オブジェクトである “Flame” を探し、オブジェクトとして取得します。
15行目で、アクティブ状態を切り替えることで、火がでる → 火が消える → 火がでる ・・・ となります。
(4) 既存のスクリプトを修正する
ここからは、既存のスクリプト(クラス)を修正していきます。
せっかくですので、先ほど例に出した、
・薪オブジェクトをつかんだ状態で「更に右トリガーも押したら薪を消す」
も実装しておきましょう。
[実装3] 薪クラスを修正する
まずは、薪クラスを修正します。修正箇所は2か所ですね。
[FireWood.cs]
using System.Collections; using UnityEngine; namespace Amaotolog { /// <summary> /// 薪クラス /// </summary> public class FireWood : MonoBehaviour, ITriggerAction { // アクション処理(薪を消す) public void DoubleTriggerAction() { Destroy(gameObject); } // 時間経過で薪が小さくなる public void GetSmaller() { IEnumerator getSmallerIE = GetSmallerIE(); StartCoroutine(getSmallerIE); } private IEnumerator GetSmallerIE() { for (int i = 0; i < 150; i++) { float adjust = -0.002f; transform.localScale += new Vector3(adjust, adjust, adjust); yield return new WaitForSeconds(0.1f); } } } }
9行目で、FireWoodクラスの継承元に ITriggerActionインターフェイス を追加しました。Lighterクラスと同じですね。
12~15行目で、DoubleTriggerActionメソッドの中身を実装してます。Lighterクラスの同名メソッドでは「ライターのオン/オフ」でしたが、このクラスでは、Destroyメソッドで薪オブジェクトを消しています。
[実装4] 既存スクリプトに追加する
ここまでの実装で、インターフェイス・ライタークラス・薪クラスの準備ができました。
最後の実装は、「オブジェクトをつかんだ状態で、更に操作した場合」の処理の追加ですね。
オブジェクトをつかむ処理は以前の記事で実装済みなので、その処理の最後に追加していきます。
[TransformMover.cs]
private void LateUpdate() { ★省略★ // 右ハンドトリガーを押している時 if (OVRInput.Get(OVRInput.Button.SecondaryHandTrigger, OVRInput.Controller.Touch)) { // コントローラからRayを出す Ray ray = new Ray(RightHandAnchor.position, RightHandAnchor.forward); ★ここから追加★ // つかんだ状態で更に操作した場合 if (grabItem != null) { // 右トリガー押下時 if (OVRInput.GetDown(OVRInput.Button.SecondaryIndexTrigger, OVRInput.Controller.Touch)) { // オブジェクト毎のアクション処理を実行 ITriggerAction triggerAction = grabItem.GetComponent<ITriggerAction>(); triggerAction.DoubleTriggerAction(); } } } }
13~23行目を追加しました。
14行目で、「つかんでいるオブジェクトがあるか?」をチェック。
17行目で、右コントローラーの「トリガー」が押されたことを検知。
ポリモーフィズムの効果です
そして、20,21行目が一番のポイントです。
[TransformMover.cs]
ITriggerAction triggerAction = grabItem.GetComponent<ITriggerAction>(); triggerAction.DoubleTriggerAction();
20行目で、つかんだオブジェクトにアタッチされているスクリプト(クラス)を GetComponent() で取得しています。
このロジックだけ見ると、ITriggerActionインターフェイスを取得してしまうように見えますが、実際は、
・ライターをつかんでいる場合は Lighterクラス
・薪をつかんでいる場合は FireWoodクラス
を取得することができます!
これが「ポリモーフィズム」の効果です!
21行目で、取得したクラス内の DoubleTriggerActionメソッド を実行しています。
(5) ポリモーフィズムで別スクリプトの同名メソッドを実行できる!
先ほどの20行目の処理で、スクリプト(クラス)の取得に使っているのは ITriggerAction.cs という、最初に作ったインターフェイスでした。
個別に実装することもできるが…
本来であれば、ライターにアタッチしているスクリプトは Lighter.cs ですので、
// 処理1(ライターの場合) Lighter triggerAction = grabItem.GetComponent<Lighter>(); triggerAction.DoubleTriggerAction();
とするのが普通ですし、実際それでも問題なく動きます。
ですが、このように実装してしまうとライター以外をつかんだ場合に困ります。
Lighter.cs を持ってるのはライターオブジェクトだけですので、薪オブジェクトの場合は「そんなスクリプトは無い」エラーが出てしまいます。
また、薪の場合は、薪のスクリプト FireWood.cs を取得すれば良いので、
// 処理2(薪の場合) FireWood triggerAction = grabItem.GetComponent<FireWood>(); triggerAction.DoubleTriggerAction();
という実装でも良いのですが、今度は逆にライターの場合はエラーとなってしまいます。
タグを使えば識別できるが、数が多いと面倒…
これを回避するには、いま何をつかんでいるかを判定して、ライターなら処理1、薪なら処理2を実行する方法がありそうです。実際、薪とライターに「専用のタグ(Grab-Wood や Grab-Ligher)」を設定すれば、判定もできそうな気がします。
2種類くらいなら、このタグ判定方式で行けそうですが、これが10個、100個となると処理の分岐も増えて、実装がかなり面倒なことになってしまいます。
しかも、分岐が増えると試験パターンも増えますし、今後、新たな物が増えるたびにロジック修正や影響範囲調査も必要です。かなり面倒ですね。
共通なインターフェイスで指定できる
これを解決するのが、今回の ITriggerActionインターフェイス を使った実装です。
Lighter.cs と FireWood.cs の両方が ITriggerActionインターフェイス を継承している場合、
ITriggerAction triggerAction = grabItem.GetComponent<ITriggerAction>(); triggerAction.DoubleTriggerAction();
という書き方ができるようになります。
このロジックでは、Lighter も FireWood もどちらのスクリプト名も使っていません。あるのは ITriggerAction という共通のインターフェイスだけ。
共通のものを指定するだけなので、何をつかんだ場合でも同じロジックが使えて、分岐も必要なくなる、というわけです。
つかんだものによって別処理を実行できる!
共通的なインターフェイスを指定していますが、実際に取得できるのは、それぞれのオブジェクトにアタッチされているスクリプト(クラス)です。これも大事なことですね。
これらの仕組みによって、DoubleTriggerActionメソッドを実行すると、
(1) つかんだものがライターの場合:Lighter.cs の DoubleTriggerActionメソッド(中身はライターのオン/オフ処理)
(2) つかんだものが薪の場合:FireWood.cs の DoubleTriggerActionメソッド(中身は薪を消す処理)
が実行されるようになります。
便利な仕組みですね。
「クラスの継承」を使う方法もある
今回ポリモーフィズムを使った実装をするのに「インターフェイスの継承」を使いましたが、「クラスの継承」を使う方法もあります。
例えば、薪クラスとライタークラスの両方で同じ親クラスを継承していた場合、その親クラスを使って 20行目のような書き方ができます。どちらを使うのが良いかは、実装の状況によりそうですね。
(6) 動画で確認してみる
VRモード(Oculus Link)で確認してみましょう。
ライターで火をつける
動画で確認してみます。
ライターをつかんで、更にトリガーを押すと火がつきました!
まとめ
焚き火に火をつけるためのライターを作ってみました。
ポリモーフィズムを使うことで実装がシンプルになり、今後の拡張もしやすくなりました。
便利な仕組みですね。
次回は、開閉式のオイルライターを作ります。
コメント
トラックバックは利用できません。
コメント (0)
この記事へのコメントはありません。