Unityに関連する記事です

 今回は、クラスの継承多態性という、オブジェクト指向プログラミングにおける重要な概念(原則)を利用した設計の学習と実装を行います。



<学習内容>
 ・クラスの継承
 ・protected(プロテクテッド) キーワード
 ・virtual(バーチャル) キーワード
 ・override(オーバーライド) キーワード
 ・base(ベース) キーワード
 ・クラスの継承時に利用できる、仮想(抽象)メソッドのオーバーライド処理 ー多態性(ポリモーフィズム)ー



設計


 今回はまず、イベントを1つだけ実行する機能を考えます。


<命令を出す側(ファミコン側)>
 ・プレイヤー側

<命令を受け取る側(ソフト側)>
 ・イベントを行うオブジェクト側
   → NPC、宝箱、探索地点、お店など

 このような構図です。

 現在はこれらの処理が、すべて1つのクラスにまとまってしまっています。

 そのため、まずは、ソフト側の処理について、クラスの継承を利用して抽象化の処理を行っていきます。

 いままでクラスによって処理を分岐していましたが、そうではなく、クラスの中身は知らないままで処理を実行していく仕組みを作ります。
つまり、ソフトは刺さっているかどうかだけをファミコンが知っている状態で動くようにしていきます。

 その後、残った部分の処理をファミコン本体の機能として動くように改良します。


考え方の変換方法


 クラスやタグによる分岐処理と分岐内容を、「クラスにつき、1つの分岐処理を作る」という考え方から、「クラスは1つのイベント(独立したクラス)」という風に変えてみてください。

 クラスとは、オブジェクト指向型プログラムにおいては設計図にあたります。
この設計図から1つのオブジェクト(インスタンス)が作られて利用されることになりますので、
イベントを作るためには、クラス = オブジェクトとして置き換えて考えることが出来ます。

 仮に3つの分岐処理がある場合、それは3つの分岐から、3つのイベントがあると考えます。
このイベントとは、3つのクラスのことであり、そのクラスから3つのオブジェクトが作られていて、それがイベントとなっている、という考え方です。

 抽象化を行っていく際のポイントは、この考え方の変換です。
処理ベースではなくて、オブジェクトベースで考えてみる、という形でイメージを作っていくと理解しやすくなります。
(この考え方だけが正という訳ではなく、他にも抽象化についての考え方はあります。理解するための1つの方法と考えてください。)


共通化できる処理を考えて親子となるクラスの設計をする


 各イベントをクラスによって分岐している部分のリファクタリングを行っていきます。

 親となるクラスはMonobehaviourを継承したクラスになります。これがすべてのイベントの親となるクラスになります。

 子クラスでのみ実装が必要な情報と、親クラスに実装して子クラスで共通化して利用したい情報とがありますので
まずはその切り分けを行って、共通化できるもののみを親クラスに用意します

 ・UTAGE のフラグ
 ・イベントの実行(中身は別)

 この辺りは、共通化できる処理になります。

 ですが、

 ・イベントの中身(会話イベント、宝箱イベント、御朱印イベント、移動イベント、生成イベントなど)

 この機能については、それぞれのイベントによって内容が異なるため、
親クラスに共通化して処理を作った上で、子クラスでそれぞれのイベントの内容に沿う内容に修正する必要があります

 クラスの継承では、この共通化の機能と、子クラスで異なる処理に変える機能を実装できます。

 親クラスは通常通り、新しいC#スクリプトを作成していく手順で作っていきます。



 親クラスに記述する内容は、通常のクラスと同じように変数の宣言とメソッドの作成になりますが、記述する書式がいくつか変わります。

 変数やメソッドの宣言において private 修飾子を利用する部分には、代わりに protected 修飾子を使用します。
これは継承したクラス間でのみ使用できることを許可する修飾子です。
外部で利用したい変数やメソッドの場合には、通常通り public 修飾子を使用します。

 またメソッドの場合には、宣言時に virtual キーワードを記述します。こうすることで子クラスが上書き可能な、親クラスのメソッドとして成立します。

 親クラスで設定されたアクセス修飾子の情報は子クラスでも引き継がれます
例えば親クラスで public float x を作成していれば、それは子クラスでも public float x として扱われます
メソッドも同様です。


親クラスの作成


 ファミコンで例えると、ソフト側全体で利用する仕様に当たります。
カセットの形状であったり、本体に刺す部分であったりという箇所はすべてのソフトに共通しています。

 その共通する部分を親クラスとして作成することで、すべての子クラスの仕様を共通化します。



 MonoBehaviour クラスを継承した親クラスを作成します。
作成方法は通常のクラスの作成方法と同じです。

 このクラスには継承先になる子クラス内で共通して利用する変数やメソッドを定義しておきます。

 ここでは ExecuteEventCoroutine メソッドを定義し、すべてのクラスで共通で実行できるメソッドとしておきます。
ファミコンにソフトを指して電源を入れたらゲームが起動しますが、その起動後の処理に当たります。
ただし、ソフトによって動くゲームが様々あるのと同じで、この処理も、
実際の処理の内容(ファミコンであれば動くゲームの内容)については、各子クラス側で実装します。

 このように親クラスでは、メソッドだけ定義しておいて、実際には子クラスでメソッド内の処理の内容を書き換えることが出来ます。
そのためのメソッドを仮想メソッド(抽象メソッド)といい、専用の virtual キーワードを使うことで定義します。

 今回であれば、宴のデータを利用するケースが多いため、その情報を変数として定義しておきます。


<= クリックすると開きます。


 新しく protected 修飾子と virtual キーワードが出てきていますので、順番に説明します。


<protected(プロテクテッド) キーワード>


 protected キーワードはアクセス修飾子の1つです。private や public の仲間です。

 protected キーワードを宣言した変数やメソッドは、クラス内部、あるいは派生(子)クラスからのみアクセスすることが出来ます。

 そのため、親クラスとなるクラスにおいて、親子間で利用したい変数やメソッドには protected 修飾子を宣言します
これは親クラスにおいて private 修飾子で宣言している場合、親クラス内部では利用できますが、派生クラスでは利用できなくなってしまうためです。


<親クラスにある protected 修飾子を持つ変数やメソッド>
  // 変数での利用
  [SerializeField]
    protected UtageParamBoolName utageParamBoolName;


<参考サイト>
MicroSoft
protected (C# リファレンス)
MicroSoft
アクセス修飾子


<virtual(バーチャル) キーワード>


 親クラスにおいて定義したメソッドは、virtual (仮想) キーワードを一緒に宣言することで、派生(子)クラスにおいてオーバーライド処理をして利用することが許可されます。
この機能を有しているメソッドを仮想メソッドといいます。仮想メソッドにより、メソッドには多態性(子クラスにより、メソッドの振る舞いが変わる)が生まれます。


<親クラスにある仮想メソッド>
    public virtual IEnumerator ExecuteEventCoroutine() {

    // 継承した子クラス側で処理の書き換えが出来る。

    }


 virtual キーワードのないメソッド(通常のメソッド)は仮想メソッドではないため、処理をオーバーライドすることは出来ません


<各キーワードの関係性>
 virtual => override できる

  abstract => override できる


参考サイト
MicroSoft
virtual (C# リファレンス)


子クラスの作成


 親クラスである GameEventBase クラスを継承した子クラスを作成します。

 ここで用意するメソッドが、ソフトの中身に当たります。

 この例では宴アセットを会話イベントとして利用している場合の実装例です。
いままでは分岐処理の内部に書かれていた処理になります。

 親クラスとして GameEventBase クラスを継承しているので、この子クラス自体にも MonoBehaviour クラスの機能を実装出来ます。

 親クラスにて定義している仮想(virtual)メソッドを override しての子クラスにて利用する場合、修飾子は同じ修飾子で宣言します。
今回であれば、親クラスの ExecuteEventCoroutine メソッドを実装しているため、同じ public 修飾子にて宣言しています。


<= クリックすると開きます。




 MonoBehaviour クラスが継承されていませんが、正常に動作し、ゲームオブジェクトにアタッチ出来ます。
これは親クラスである GameEventBase に MonoBehaviour クラスが継承されているためです。

 クラスの継承は、常に1つのクラスしか継承出来ません。そういうルールになっています(多重継承の禁止)
ですが、子クラスは親クラスの継承している、すべてのクラスの情報を扱うことが出来ます
つまり、親クラスを継承した子クラス、子クラスを継承した孫クラスと継承先を重ねていくことで、複数のクラスが利用できます。

 また using に UnityEngine の記述がありませんが、これも、このクラス内で UnityEngine に関連する機能がないためです。



 子クラスの処理は以上です。

 子クラスに書かれていない utageParamBoolName 変数は親クラスである GameEventBase 側に記載されていますので、そちらで処理を行ってくれます。

 そのため子クラスでは、親クラスに足りない情報や、親クラスのメソッド内容に対して修正したい処理だけを記述することで、親クラスの処理も含めて実行されます。


多態性 ーメソッドの振る舞いを変えるー


 子クラスにおいて親クラスのメソッドに変更を加えた場合、親クラスの同メソッド自体は実行されますが、親クラス内の処理は無視されます
これをメソッドのオーバーライドといいます。

 オーバーライドできるメソッドは、親クラスで virtual キーワードを付けて宣言している仮想メソッドか、
今回はまだ扱いませんが親クラスで abstract キーワードを付けて宣言している抽象メソッドいずれかのみになります。

 メソッドのオーバーライドを行うと、親クラスに記述されているメソッド内の処理はすべて上書きされて無視されます
そのため、親クラスのメソッド内の処理に加えて、子クラスの処理を追加したい場合には、オーバーライドしたメソッド内に base.オーバーライドしたメソッド名(); を記述します。
詳しい使い方については長くなってしまうため別のページで行いますが、まずはご自分で継承について調べてみましょう。

 以上のことより、オーバーライドを行わないメソッドは、子クラスで親クラスの処理を上書きする必要がないメソッドになりますので、
親クラスに書かれている変数同様、子クラスでの記述は不要になります

 このように「同じ名前のメソッドを呼び出し、異なる振る舞いをすること」を多態性といいます。
これを実現するための機能が仮想メソッド(抽象メソッド)であり、抽象化という考え方になります。


<override(オーバーライド) キーワード>


 abstract キーワードによる抽象実装や、virtual キーワードによる仮想実装に対して実行できる機能です。
上記の2つのキーワードを持つメソッドに対して、上記部分を override に変更することで、処理を拡張したり、書き換えたりすることが出来ます。

 このとき、メソッドのアクセス修飾子を変更することは出来ません
例えば、public virtual メソッドであれば、public override メソッドのように、あくまでも同じアクセス修飾子での実装になります

<親クラスにある仮想メソッド>
    public virtual IEnumerator ExecuteEventCoroutine() {

        // 各イベントの処理記述する

    }

 ↓ 

<派生(子)クラスでのオーバーライド実装>
    public override IEnumerator ExecuteEventCoroutine() {  //  ← アクセス修飾子は同じレベルにする

        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));
    } 

 オーバーライドという単語の持つ意味の通りで、この処理を行った親クラス側に記述されていた仮想メソッド内の処理は上書きます
もしも親クラスに実装されている仮想メソッド内の処理も利用した上でオーバーライドしたい場合、base キーワードを利用します。こちらは次に説明します。

 親クラスにある virtual キーワード、あるいは abstract キーワードと override キーワードとは、1対1でつながっているイメージです。


参考サイト
MicroSoft
override (C# リファレンス)


<base(ベース) キーワード>


 override キーワードを利用して仮想メソッドを変更する際、
親クラスでの実装内容を利用して拡張したい場合と、まるごと書き換えたい場合とがあります。
メソッドのオーバーライド処理は基本的に上書きですので、丸ごと書き換わります。

 親クラスにある仮想メソッドの処理を残した上で、拡張する形でメソッドのオーバーライドを行いたい場合には、base キーワードを使って実装します

 記述する場合には、base. に続けて親クラスにある仮想メソッドのメソッド名と引数を宣言します。
処理を書く順番にも注意してください。プログラムは上から順番に実行されますので、拡張した処理に対してどのタイミングで親クラスの処理を実行するかを考える必要があります。


 例えば、親クラスの仮想メソッドが下記のような状態であったとします。

    public virtual IEnumerator ExecuteEventCoroutine() {

        Debug.Log("仮想メソッドは");
        Debug.Log("処理を書き換えて");
        Debug.Log("上手に使おう");

        yield return null;
    } 

 上記の実装の場合、親クラスの処理に加えて、子クラスの処理が実装されますので、実際には次のような内容になります。

<base キーワードを利用した場合の処理 
    public override IEnumerator ExecuteEventCoroutine() {

        base.ExecuteEventCoroutine();

        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));
    } 

  この場合、実際には ↓ のようになります。

    public override IEnumerator ExecuteEventCoroutine() {


////*  base.ExecuteEventCoroutine で実装される親クラスの処理  *////


        Debug.Log("仮想メソッドは");
        Debug.Log("処理を書き換えて");
        Debug.Log("上手に使おう");

        yield return null;


////*  この処理が行われている  *////


        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));
    } 



 また、base.ExecuteEventCoroutine を下側に書いた場合

<base キーワードを利用した場合の処理◆
    public override IEnumerator ExecuteEventCoroutine() {

        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));

        base.ExecuteEventCoroutine();
    } 

  この場合、実際には ↓ のようになります。

    public override IEnumerator ExecuteEventCoroutine() {

        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));


////*  base.ExecuteEventCoroutine で実装される親クラスの処理  *////


        Debug.Log("仮想メソッドは");
        Debug.Log("処理を書き換えて");
        Debug.Log("上手に使おう");

        yield return null;


////*  この処理が行われている  *////


    } 

 base の書かれている位置によって、処理される順番が変化しています。



 base を記述すれば、その処理の内容としては親クラスの処理がそのまま適用されますので、非常に便利です。
特に親クラスでの仮想メソッド内の処理が多いほど、1行の base キーワードによって実装される処理も多くなりますので、恩恵は大きくなります。

 逆に考えると、親クラスにおけるメソッドの挙動をしっかりと理解していないと、クラスの継承を利用したプログラムを組み込めない、ということになります。



 オーバーライドしたメソッドでは、base キーワードがない限り、親クラスでの処理の内容は反映されない(上書きされる)ことになりますので、
例えば、今回のように base キーワードがない場合には、子クラスでオーバーライドされた処理のみが実装されます。


<base キーワードがない場合の処理>
    protected override IEnumerator ExecuteEventCoroutine() {

        string npcScenario = GetComponent<NonPlayerCharacter>().npcData.npcScenario;
        
        //NPCシナリオの作成
        yield return StartCoroutine(AdvEngineController.instance.JumpScenarioAsync(npcScenario, null));


    //  base.ExecuteEventCoroutine で実装される親クラスの処理がないので
    // Debug.Log を3つ出す処理や、yield return null の処理は発生しない

    } 


<参考サイト>
MicroSoft
base (C# リファレンス)
++C++ // 未確認飛行 C 様
継承


命令を受ける側の機能の作成


 イベントの実行命令をプレイヤー側(ファミコン本体)から受け取り、実際にイベントを実行する側(ゲームを起動する)のクラスを作成します。
ここでは GameEventHandler クラスを作成し、このクラスにて GameEvent が実行できるように処理を作成します。

 そのためこのクラスは、イベントを実行する側(NPC 用のゲームオブジェクト、宝箱のゲームオブジェクトなど)にアタッチします。
一緒に GameEventBase を継承している子クラスをアタッチし、それにより、ゲームオブジェクトの役割(ソフトの中身)を設定します。
また、命令を受け取る側の処理としてアサインして「実行するイベントを登録」します。

 いままでとは異なり、この GameEventHandler クラスでは実行する GameEvent の種類は特定しません
GameEvent であれば自動的に処理を実行しています。ここまでです。

 つまり、ソフトの種類は知らないものの、ファミコンのソフトであれば起動はするというイメージです。

 実際に実行されるイベントは、登録している GameEvent (ソフトの中身)に依存します。
会話イベント用の GameEvent が実行されることもあれば、他のイベントが実行されることもあります。

 抽象化により、ここで実行されたメソッドが、先ほど作成した子クラスのオーバーライドされたメソッドによって
処理の内容が変わる(振る舞いを変える)ようにしているためです。

 「ゲームを起動する」だけの処理にしているので、今までの分岐確認の処理を一元化させることが出来ています。


<= クリックすると開きます。



命令を受ける側の設定


 GameEventHandler クラスと、会話イベント用の GameEventNpcTalk クラスを NPC 用のゲームオブジェクトにアタッチします。
子クラスが継承している親クラスはアタッチする必要はなく、子クラスのみをアタッチすれば問題ありません。自動的に親クラスの機能も実装されます。

 いつも自作しているスクリプトも MonoBehaviour クラスを継承していますが、MonoBehaviour クラスをアタッチしたことはないはずです。



 GameEventHandler クラスがイベントを管理しますので、GameEvent 変数に GameEventNpcTalk クラス(この NPC のゲームオブジェクトそのもの)をアサインしてください。

 これでイベントが発火(実行された)したとき、GameEvent 変数に登録されている GameEventNpcTalk クラスのメソッドが実行されます。


インスペクター画像



命令を実行する側の機能の作成(既存処理の修正)


 PlayerController クラスの処理を修正します。

 まず Action クラスの処理を修正し、ボタンを押し、Ray による判定を行った後の処理を変更します。
いままでは各ゲームオブジェクトにアタッチされているクラスを TryGetComponent メソッドを利用して分岐させていましたが、
この部分を各親クラスの確認を行い、特定のメソッドの命令を実行する処理に変えます。
そうすることで、確認の処理が短縮化されます。

 また、ボタンの押下処理はの判定 Action メソッド内ではなく、 Update メソッドで判定するように変更します。
こうすることで事前に Action メソッドを呼ぶか呼ばないか判定出来ます。(いまは、実行できるかわからなくても Action メソッドを呼んでしまう)

 実際にイベントを実行させるためのメソッドや、それに関わるメソッドを合計4つ追加します。
 

PlayerController.cs
<修正分>

<= クリックすると開きます。


PlayerController.cs
<追加分>

<= クリックすると開きます。



<クラスの継承時に利用できる、仮想(抽象)メソッドのオーバーライド処理 ー多態性(ポリモーフィズム)ー>


 多態性(ポリモーフィズム)とは、 同じメソッドの呼び出し命令に対して、異なるオブジェクトが異なる動作をする(振る舞いを変える)ことを言います
オブジェクト指向プログラミングのプログラミングにおける、重要な考え方・概念になります。
++C++; // 未確認飛行 C 様
C# によるプログラミング入門 多態性
https://ufcpp.net/study/csharp/oo_polymorphism.htm...



 今回の実装のケースであれば、GameEventBase クラスに用意されている ExecuteGameEventCoroutine メソッドを実行すると
同じメソッドであるにもかかわらず実行される処理が異なる ー振る舞いを変えるー ことを指しています。



 いままでの処理とは、どの部分が異なっており、どこがポイントであるか、考えてみてください。

 まず大きな変更点として、クラスによる分岐の処理はなくなりました

 タグやクラスによる分岐処理の場合には、分岐後、さらにそのクラスごとのメソッドを特定しないと処理を実行できませんでした。

 今回の実装の場合、イベントの判定には TryGetComponent メソッドは利用していません。
(イベント発生後、NPC であるかどうかでのみ利用しており、この処理自体は NPC の向きを変えているだけですので、イベント処理ではありません。)
つまり、イベントの種類を特定するために行っていたクラスのアタッチの判定はしていません



 そしてもう1つ。
 実行するイベントについても、このクラスではどのようなイベントが実行されるのかを知りません。
イベントを実行する、という機能だけを持っており、あとは、イベント側のクラスに処理を投げています。


 // 各クラスを判定するのではなく、イベントを発生させる GameEventHandler がアタッチされているかだけを判定する
  if (hitOject.TryGetComponent(out GameEventHandler gameEventHandler)) {

   // ここでも、GameEventHandler に登録されているイベントを実行する、ことだけやっている
      //     → どんなイベントを実行しているかは知らない
      gameEventHandler.ExecuteGameEventCoroutine();
  }

 非常に簡潔に処理を記述できる上に、イベントの種類が増えて子クラスが増えても、
そのクラスが GameEventBase クラスを継承していればここに処理を書き足す必要もありません
これがクラスの継承とメソッドのオーバーライド処理によって振る舞いを変えることで実装できる大きな利点です。



 つまり、実装を抽象化していくことが出来れば、実行側の責務は「イベントがある場合には、それを実行する」ことだけになります。
どんなイベントが起こるかを知る必要はなく、イベントがある場合には実行する、というシステムが機能するだけに作り替わっています。

 この抽象化の設計の実装により、ファミコンの本体の機能とソフトの機能が、ここで出来上がっています



 クラスの継承を理解するのは難しいですが、それだけの機能や恩恵が受けられることを考えると、是非習得していただきたい技術になります。
また、こういった処理を理解していくことでオブジェクト指向プログラミングにも慣れていくことが出来ます。

 繰り返し処理を考えて記述し、自分でもクラスを継承したクラスを自作してみてください。


ゲームを実行して動作を確認する


 いままでと同じように NPC に対して会話イベントが発生するかを確認してください。


まとめ


 すでに気づかれているかもしれませんが、GameEventHandler は、イベントを起動するだけです。
そのときには GameEvent 変数に登録されているイベントを起動します。

 GameEvent 変数には、GameEventBase クラスを継承している子クラスであれば、どのようなものでも登録可能です。
つまり、イベントであれば(GameEventBase を継承していれば)、イベントの中身を知っている必要はない(子クラスがなんであるのかは知らなくても登録できる)という状態です。

 これはどういうことかというと、例えば、宝箱用のイベントを GameEventBase クラスを継承して作成すれば、
今回の会話イベントのように GameEventHandler が起動してくれます。
なぜなら、GameEventHandler はイベントであれば、それがどんなイベントであるかは知らなくてもいいためです。

 同じように、アイテムの入手イベントも、キャラの自動移動イベントも、オブジェクトの追加・削除のイベントも
すべて GameEventBase を継承した子クラスを作成すれば、この GameEventHandler の GameEvent 変数に登録できますので、
どんなイベントであっても、GameEventHandler が実行してくれることになります。

 そして実行される実際の処理は、各子クラス(作成したイベントのオーバーライドしたメソッド)により、振る舞い(処理の内容・イベントの内容)が自動的に変わります。



 ずっとお話しさせていただいている通り、ファミコン本体とソフトの起動、そしてソフトの中身の役割分担が出来ているので、
以降は、GameEventBase を継承した子クラスを作れば(新しいソフトを作れば)、イベントを追加できます。

 そして、GameEventHandler(ソフトの起動)や PlayerController(ファミコン本体) などの完成されている機能については、
今後イベントの増減(子クラスの増減)があったとしても、それに関連したスクリプトへの修正は一切不要です。
(もちろん、本体機能そのものを拡張する場合には、修正が必要になります。)

 このように処理の抽象化によって設計された機能には、保守しやすく、汎用性と拡張性の高い機能になります。



 以上でこの手順は終了です。

 次の手順は 処理の抽象化による実装例 です。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



プログラムの基礎学習

コード練習

技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

3Dトップビューアクション(白猫風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます