Unityに関連する記事です

 プログラムの学習において、最初のうちは1つのクラス内に色々な処理を書いていくケースがほとんどです。
教材やネットのサイトでも多くそういった書式が紹介されています。

 ソースコードを書くことに慣れてきたら、クラス内の処理を役割単位で分割して、複数のクラスに分けて役割ごとに制御する方法を学習し、実践しましょう。



<新しい学習内容>
 ・インターフェース
 ・インターフェースの実装とメソッドのオーバーライド処理
 ・プロパティの理解を深める



ケーススタディ


 よくあるケースとして、CharaController クラスや GameManager クラスの肥大化があります。

 これは仕方のない部分でもあります。特に初心者の場合や、プログラムに慣れていない場合、
複数のクラスに処理を書くことによって処理を読み解くことが難しくなるため、最初のうちは1つのクラスに集約していくことが多いです。

 ですが、ずっとその手法しか知らないと、クラスの設計方法や、ソースコードを書くスキルのステップアップが出来ません。

 ここでは1つのクラスに書いた処理を機能別に分けて、複数のクラスに分割し、制御していく方法を実践して学習しましょう。
 
 実際に1つのクラスに色々な処理がまとまっているソースコードを利用し、クラスの分割を行っていきます。


分割前のクラス


 今回は CharaController を例に、役割ごとに処理を分割して考えてみます。


CharaController.cs

<= クリックしたら開きます。




 プレイヤー役のゲームオブジェクトの構成は、次の通りです。


構成



親クラスのインスペクター画像(CharaController クラスとコライダー、Rigidbody が一緒にアタッチされています)



子クラスのインスペクター画像(画像と Animator コンポーネントがアタッチされています)




処理を役割に沿って考えてみる


 CharaController クラス内には、多くの処理があります。
それらの処理を、どういった役割を果たしているのか、という観点で分割してみてください。
その分割したものが役割の1つとなり、同時に、1つのクラスとして作成していく、という方向性になります。

 また処理の内部には、複数の処理内で共通で利用している処理も見受けられます。
例えば十字キー入力値は移動の処理と、アニメの向きの処理の両方で同じ値が利用されています。
こういった部分は共通している部分として、別の役割(共通処理をまとめたクラス)として考えます。

 つまり、移動とアニメの処理からは「十字キー入力値」の処理は含まないようにします。
では、必要な値はどうするのか、ということですが、「共通で利用する処理」をまとめた役割(クラス)を作り、
そこから「十字キー入力値を受け取って」利用するようにします。

 それぞれのクラス内で同じ処理を書くのでなくて、共通している情報を管理しているクラスから受け取って利用する、という考え方です。
このようにすることで、十字キー入力の処理は、個々のクラスに書くのでははなく、共通して利用するクラス内のみ(1つのクラス)に集約出来ます。

 そのためにはメソッドの引数や戻り値を利用することが多いです。


リファクタリングの方向性


 クラスを分割するにあたっては、インターフェース機能を利用して、役割(責務)の分割を行っていきます。

 そのため、分割して作成されるクラスは、すべてインターフェースを実装する形式にします。

 インターフェースは抽象クラスと同様で、それ単体ではインスタンスが作成できません
いずれかのクラスに実装させる前提で設計を行う機能になります。



 1つのクラスを複数のクラスに分割するにあたり、クラス内の処理を機能別に仕分けします。
例えば、移動の処理、アニメの処理、という風に、現在の CharaController クラスに書かれている処理を役割ごとに、変数とメソッドをセットにして分けます。

 設計と実装にあたり、全体の設計が完了したら、まずはインターフェースから順番に作成していくことになります。

 インターフェースを作成したら、それを実装した Chara 〜 クラスを作成し、これに役割ごとの処理をまとめていきます。


設計 ー役割の考え方ー


 CharaController クラス内の処理を機能別に分けてみましょう。
下記のような仕分けによる分類ができると思います。

・移動させる機能
・アニメーションを移動方向に同期させる機能
・攻撃する機能
・Hp を管理する機能
・レベルと Exp を管理する機能

 このうち、移動させる処理とアニメーションを移動方向に同期させる処理については、キー入力の値を同じ情報として利用しています。
そのため共通する処理をまとめたクラスを1つ作成し、また、このクラスで、それ以外のクラスを管理します。
つまり、管理者(マネージャー)となるクラスを新しく用意し、他の機能をまとめ上げるようにします。


<新しく作る>
・移動させる機能を持つクラス
・アニメーションを移動方向に同期させる機能を持つクラス
・攻撃する機能を持つクラス
・マネージャークラス

 今回は Hp を管理する機能とレベルと Exp を管理する機能についてはクラスは作成しません。
この学習を通じて機能の分割方法を考えて、自作してみてください。


インターフェースを作成する


 マネージャークラス以外のクラスに実装するためのインターフェースを作成します。
インターフェースの名称の先頭には I をつけ、その後、名称をつけます。


<= クリックしたら開きます。



<インターフェースとメソッドの定義>


 class の部分を interface に書き換えることで、インターフェースを作成することができます。

<インターフェース>
public interface IChara
{
    void SetUpChara();
}

 インターフェースは、プログラムの設計において、クラスが持つべきメソッドやプロパティを定義するための「契約」または「取り決め」のようなものです。

 例えば、車やバイクというクラスがあるとします。それぞれ異なる機能を持つかもしれませんが、移動できるという共通の特性があります。
この共通の特性を「移動可能」として表現することができます。それがインターフェースです。



 インターフェースは具体的な実装を持たず、単にメソッドやプロパティの宣言だけを持ちます。
クラスがインターフェースを実装することで、そのクラスがインターフェースで定義されたメソッドやプロパティを実装することが義務付けられます。
強制力があるとも言い換えられます。

 先ほどの車を例にしたとき、具体的には、以下のような形でインターフェースを定義します。

public interface IMovable
{
    void MoveForward(float distance);
    void TurnLeft(float angle);
    void TurnRight(float angle);
}

 そして、車やバイクなどのクラスがこのインターフェースを実装します。

public class Car : IMovable
{
    // 車の特有のプロパティやメソッドの定義
    // ...

    public void MoveForward(float distance)
    {
        // 車を指定した距離だけ前進させる処理
        // ...
    }

    public void TurnLeft(float angle)
    {
        // 車を指定した角度だけ左に曲げる処理
        // ...
    }

    public void TurnRight(float angle)
    {
        // 車を指定した角度だけ右に曲げる処理
        // ...
    }
}

public class Bike : IMovable
{
    // バイクの特有のプロパティやメソッドの定義
    // ...

    public void MoveForward(float distance)
    {
        // バイクを指定した距離だけ前進させる処理
        // ...
    }

    public void TurnLeft(float angle)
    {
        // バイクを指定した角度だけ左に曲げる処理
        // ...
    }

    public void TurnRight(float angle)
    {
        // バイクを指定した角度だけ右に曲げる処理
        // ...
    }
}

 このように、インターフェースを利用することで、異なるクラスでも共通の機能を持たせることができます。
また、インターフェースの恩恵は疎結合性を高めることで、より柔軟で拡張可能な設計を実現することができる点にあります。
それによって、処理を抽象化していくことが出来ます。

 今回のクラスの分割には、抽象化の考えに基づいて実装を行っていきます。


<参考サイト>
MicroSoft
interface(C# リファレンス)
++C++; // 未確認飛行 C 様
インターフェース
Qiita @yutorisan 様
【C#】インターフェイスの利点が理解できない人は「インターフェイスには3つのタイプがある」ことを理解しよう
エンジニアが送る穴倉生活のすゝめ 様
【C#】Interfaceの使いどころは配列ぶん回しのポリモーフィズムで理解出来る!


<インタ―フェースを実装したクラスを作成する>


 作成した IChara インターフェースをクラスに実装して、CharaController の中身を複数のクラスに分割していきます。

 インターフェースを実装した場合、実装しているクラスには必ずインターフェース内にあるメソッドやプロパティを書く(実装)しなければなりません
書かないとエラーが出るようになっています。これはインターフェースも特徴です。

 実装したメソッドは public 扱いになります。また、インターフェースではメソッドの定義だけを行っているので処理の中身がありません。
そのため、override キーワードはありませんが、処理を実際に書き込んで、文字通り「実装」を行う必要があります。


インターフェースを実装したクラスを順番に作成する


 各クラスごとに明確な役割を設けるとともに、責務のない処理は書き込まないように配慮します。

 また CharaManager が、分割したすべてのクラスの管理者となります。
そのため、各クラス同士はお互いを知らない状態(メンバ変数で管理していない。疎結合化)を目的にもしています。

 キャラの向いている方向の情報に関しては、複数のクラスで利用するため、そのような情報は CharaManager クラス内に管理します。
各クラス内で別々に方向の情報は取得せず、必要なクラス(CharaAnime と CharaAttack)に、メソッドの引数を通じて送り届ける形で実装しています。


1.CharaMove.cs


 移動に関する機能を役割として持つクラスです。
CharaController クラスにあった、移動に関するメンバ変数とメソッドをこちらに移しています。

 インターフェースに定義されているメソッドである SetUpChara メソッドを実装し、このクラス独自の振る舞いを行っています。

 移動の処理は画面をクリック(タップ)している間、その方向に向かって移動するタイプの移動方法です。
CharaManager の Update メソッド内でクリックの入力を受け付け、Move メソッドの引数に移動方向の情報をもらうことで、移動を実行します。


<= クリックしたら開きます。




 キャラの移動範囲を limitPosX 変数と limitPosY 変数で管理していますが、ここでは別の実装方法を新しく追加しています。

 元の処理でも問題なく機能しますが、今回の方法は、シーンビュー内に Create Empty で作成したオブジェクトを2つ配置し、
それらを対角(左下と右上)に配置することで、このオブジェクトの位置を移動範囲の値として利用するものです。

 新しい変数は Transform 型で宣言するため、シーンビュー内にあるオブジェクトの位置を参照して移動範囲を設定する方法になります。
そのため、以前の実装方法よりも融通が利きやすく、移動範囲を変えたい場合にはシーンビュー内にあるオブジェクトを動かすだけで変更できます。

 ステージのサイズなどは変更になる可能性もありますので、その都度、変数の値を登録し直す方法よりも
オブジェクトの位置を参照してプログラム自体が自動的に範囲を調整してくれる方式の方が汎用性と修正に強い設計になります。


構成



管理用フォルダ用のオブジェクト



左下に配置するオブジェクト



右上に配置するオブジェクト



 他のゲームにも応用できます。


2.CharaAnime.cs


 アニメに関する機能を役割として持つクラスです。
CharaController クラスにあった、アニメに関するメンバ変数とメソッドをこちらに移しています。
また、リテラル表記していた Animator の Parameter 値について、メンバ変数を追加して置き換えてります。

 このクラスでは移動方向に合わせたアニメーションの同期処理を実装しています。
先ほどの CharaMove クラスと同様に、インターフェースを実装し、定義されている SetUpChara メソッド内にて Animator クラスの取得を行っています。

 先ほどの CharaMove クラスの SetUpChara メソッドでは Debug.Log メソッド以外には特に何も処理は書き込んでいませんでした。
このように、同じメソッドであっても実装されている(書かれている)内容が異なっていることが分かります。

 なお、SetUpChara メソッドで実行している Animator コンポーネントを取得する命令は
子オブジェクトに Animator コンポーネントがアタッチされている場合を想定しています。
子オブジェクトがない構成の場合には、SetUpChara メソッドの処理を適宜修正して、コンポーネントの取得先を変更してください。


<= クリックしたら開きます。




 if / else 文による分岐処理のうち、処理内部で同じ変数に対して代入処理を行う場合には、三項演算子を利用して記述することが出来ます。
本来の if /else 文と比較しながら構文を読み解いてみましょう。


参考記事
@crazy_traveler様
参考になる三項演算子


3.CharaAttack.cs


 攻撃に関する機能を役割として持つクラスです。
CharaController クラスにあった、攻撃に関するメンバ変数とメソッドをこちらに移しています。
その際にメソッド名を変更したり、コルーチンメソッドを直接実行しないために仲介役のメソッドを追加しています。

 ポイントは、プレイヤーの最新の方向の情報を CharaManager から参照するためのプロパティを用意している部分です。
C# のプロパティは、値型であっても参照型であっても、外部の変数や値を保持するのではなく、ゲッターやセッターのメソッドを通じて外部との値のやり取りを行います
そのため、プロパティを通じて値を設定すると、内部での値のコピーが行われ、その後も外部変数が更新されるとプロパティのセッターが呼び出されて値が更新されます。
この仕組みにより、最新の情報をプロパティを通じて参照できるようになります。

 今回であれば、CharaManager にある direction 変数は Vector2 型であり、値型です。
その情報を参照するために用意するプロパティの型も Vector3 で値型ですが、プロパティを介して値を設定する事で、最新の参照が可能になっています。

 この機能により、CharaManager の情報を元に自動的に最新のプレイヤーの方向の情報を更新できる仕組みを導入しています。

 また新しい機能として、攻撃を一時停止/再開するための機能を追加します。


<= クリックしたら開きます。




 bool 型の変数は ! 演算子を利用することで、代入する値を入れ替えることが出来ます。
!isAttack が true の場合には false になり、false の場合には true になります。


4.CharaManager.cs


 上記の3つのクラスをまとめて管理するクラスです。
このクラスには RequireComponent 属性を付与し、該当する3つのクラスを指定していますので、
このクラスをアタッチすると、先ほどの3つのクラスも自動的に追加でアタッチされます
アタッチの手間が省けるとともに、アタッチ忘れを防止することも出来ます。

 このクラスのみが3つのクラスをメンバ変数として管理し、それぞれの処理に対して命令を出しています。
トップダウン型のような命令系統の仕組みをイメージしてもらえれば分かりやすいでしょう。

 また共通の処理としてキャラの方向の情報を管理し、その情報をアニメーションのクラスと攻撃のクラスに対して、メソッドを通じて渡しています。
こうすることにより、各クラス内で判定をさせる必要なく、共通の値をそれぞれのクラスに利用してもらう設計です。

 ただし、処理のタイミングの問題で、このメソッドの引数で渡す方法では、方向の情報を渡せないクラス(CharaAttack)もあります。
これは攻撃処理のタイミングが CharaManager ではなく、CharaAttack に依存しているため、常に最新の値を渡しておく必要があります。

 最も簡単な方向情報の渡し方は、CharaManager クラスの Update メソッドで CharaAttack 内に用意してある方向情報の値を随時更新するものです。
これであれば、いつ攻撃の処理をおこなっても最新の方向の情報を利用できます。

 今回はそのアプローチではなく、CharaAttack クラスにプロパティを用意し、その参照先を CharaManager クラスの方向情報と紐づけます。
プロパティを通じて値を設定すると、内部での値のコピーが行われ、その後も外部変数が更新されるとプロパティのセッターが呼び出されて値が更新されます。
この仕組みにより、CharaAttack クラス側でも、最新の情報をプロパティを通じて参照できるようになります。

 なお今回は処理の確認のため、Start メソッドを使って処理をスタートさせていますが、
実際には外部クラスから SetUpCharaManager メソッドへ実行命令を受けて動いていく前提で設計しています。


<= クリックしたら開きます。



<プロパティの理解を深める>


 今回、プロパティの set に外部のクラスの情報を設定する手法を実装しています。

    // direction のプロパティ。内部で CharaAttack の AttackDirection プロパティも同時に更新している
    public Vector2 Direction {
        get => direction;
        set {
            direction = value;

            if (charaAttack) {
                charaAttack.AttackDirection = value;
            }
        }
    }

 このようにすることで、CharaAttack クラス内にある AttackDirection プロパティの参照先(set) が CharaManager 内の Direction プロパティになります。

 direction 変数は Vector3 型であるので、この情報をそのまま別の情報として Vector3 型で扱おうとすると、それも値型になります。
値型の情報は、代入したタイミングでコピーした値を保持します。そのため、ずっと保持しておくことには向いていますが、
最新の値については、参照している訳ではないため、常に値の更新処理を行わないと最新の状態にはなりません。
そのため、例えば Update メソッドなどで、CharaAttack 側が direction 変数の値を常に更新するように(監視するように)しないと、古い情報を使うことになります。

 そこで、プロパティの機能を利用します。
プロパティで set する情報は他のクラスであっても設定できます
そのため、上記のように設定先を外部のプロパティにしておくことで、自動的に最新の値に更新されるようになります。

 プロパティの機能をしっかりと理解し、上手くロジックを組んでいくようにしましょう。



 また UniRx の ReasctiveProperty の機能を活用することにより、より効率の良いプログラムを組むことが可能です。

 下記は参考用のソースコードの抜粋です。


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



クラス図


 ここまでに制作したクラス間の関係性を可視化することで、分割されたクラスがどのような状況か、イメージしやすくなります。

 クラス図を用意しましたので、各クラスの関連性を見てみましょう。



<インターフェースとクラスの関係性>



<管理クラスと他のクラスの関係性>



 3つのクラスがインターフェースを実装しているため、すべてのクラスに SetUpChara メソッドが実装されています。
同名のメソッドですが、実際にはそれぞれのクラス内に書いてある処理が実行されますので、振る舞いが変わります。

 また CharaManager クラスは他の Chara 〜 クラスを知っていますが、他のクラス同士にはつながりがありません。
よって、クラス間の関係性が疎結合化でき、クラス内部を修正する際に、他のクラスのことを気にせずに修正することが出来ます。

 このように、インターフェースを利用することで、同名のメソッドを使うが、その内容までは関知しない、という形で処理の抽象化ができ、
加えて、同名のメソッドでありながら、異なる処理を実行していく(振る舞いを変える)という、多態性の概念も一緒に利用しています。

 オブジェクト指向型プログラムは実際に使いながら学習していくと、概念や機能の便利さなどが見えてきますので、
なるべく処理を書いて動かしながら学習をしていくとよいでしょう。


 
 クラス図を書くことにより、自分のイメージを整理する際にも役立ちますし、第三者にクラスの状態を説明する場合にも情報を正確に伝えることが出来ます。


プレイヤー役のゲームオブジェクトに各クラスをアタッチし、設定を行う


 まずプレイヤー役のゲームオブジェクトを複製します。複製後、1つは非表示にします。
カメラなどで追従対象になっている場合には、カメラの設定先も見直してアサインし直してください

 以前の状態のゲームオブジェクトを残しておくことで、インスペクターでの設定値の確認なども出来るためです。



 複製された方のゲームオブジェクトにある CharaController のスクリプトを Remove し、代わりに CharaManager クラスをアタッチしてください。
一緒に3つのクラスも自動的にアタッチされます。コライダーと Rigidbody はそのままで問題ありません。
また、BulletGenerator クラスがある場合には、それもアタッチしてください。

 CharaMove クラスには移動速度と移動範囲の設定、CharaAttack には攻撃の間隔の設定がありますので、それぞれ設定を行ってください。


インスペクター画像



 以上で設定は完了です。



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


 ゲームを実行し、クラスを分割する前と同じように動作するかを検証してください。


<確認動画>
動画ファイルへのリンク


設計の重要性とクラスの分割


 1つのクラスが肥大化してしまうことは多々あります。
学習中のうちは問題ありませんが、自分でプログラムを考えながら書いてみると、こういった設計部分での問題にぶつかります。
そのため基本的には、クラスを作成する前から肥大化する可能性が見えることがほとんどです。

 かといって最初から分割して作成していければよいのですが、それも中々難しいです。

 ソースコードを書く前に、設計を考えることから始めてみてください。
以前よりもたくさんの時間をクラスの設計に費やしてみることをお勧めします。
どのような処理を、どのクラスに書くのかを明確にしておくことで、肥大化しやすいクラスを前もって認知しておくこともできます。

 ソースコードを書いている間も、常に、設計を頭に考えて進めていくようにしてみてください。
設計は書き出しておくことで考えていることを言語化・可視化できるので、よりイメージしやすくなります。

 また書いて終わりにはせず、リファクタリングを行うことも念頭に置いておくと、良質なコーディングが行えるようになっていきます。

 プログラミングは反復と改善の繰り返しによってスキルアップが出来ます。


<サンプルコード ーBulletGenerator クラスと Bullet クラスー>

<1.BulletGenerator クラス>


 レベルの値に応じて、複数の弾を発射することができる機能を持ちます。

 このクラスもプレイヤー役のゲームオブジェクトにアタッチして利用します。

 switch 文による分岐処理を使い、レベルに応じて弾の発射数を変更するようにしています。
この部分は演算式を自分で考えることで、switch 文なしでの実装も可能です。
チャレンジしてみてください。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletGenerator : MonoBehaviour
{
    [SerializeField] private Bullet bulletPrefab;

    [SerializeField] private float offsetDegrees;  // 弾の角度の基準値(各バレット同士の角度間隔)
    [SerializeField] private float bulletSpeed;    // 弾の速度

    [SerializeField] private int level;


    /// <summary>
    /// バレット生成の準備。レベルに応じてバレットの数を増やす
    /// </summary>
    /// <param name="direction"></param>
    public void PrepareGenerateBullet(Vector2 direction)
    {
        switch (level)
        {
            case 1:
                GenerateBullet(direction);
                break;

            case 2:
                for (int i = -1; i < 2; i += 2)
                {
                    // 弾の方向と回転を CalculateBulletDirection メソッドを使って設定して、弾の生成
                    GenerateBullet(CalculateBulletDirection(i, direction));
                }
                break;

            case 3:
                for (int i = -1; i < 2; i++)
                {
                    GenerateBullet(CalculateBulletDirection(i, direction));
                }
                break;

            case 4:
                for (int i = -3; i < 4; i += 2)
                {
                    GenerateBullet(CalculateBulletDirection(i, direction));
                }
                break;

            default:
                for (int i = -2; i < 3; i++)
                {
                    GenerateBullet(CalculateBulletDirection(i, direction));
                }
                break;
        }
    }

    /// <summary>
    /// バレット生成
    /// </summary>
    /// <param name="direction"></param>
    private void GenerateBullet(Vector2 direction)
    {
        // このクラスをプレイヤー役のゲームオブジェクトにアタッチすれば、その位置で発射できる
        Bullet bullet = Instantiate(bulletPrefab, transform.position, Quaternion.identity);

        bullet.Shoot(direction, bulletSpeed);
    }

    /// <summary>
    /// 弾の方向を計算する
    /// </summary>
    private Vector2 CalculateBulletDirection(int numBullets, Vector2 direction)
    {
        // 角度の補正値の決定
        float offsetAngle = numBullets * offsetDegrees;

        // Z 軸を中心に回転させた回転情報(四元数)を作る
        Quaternion offsetRotation = Quaternion.Euler(0, 0, offsetAngle);

    // Unity の場合、Quaternion * Vector3(Vector2) の計算をすると、Vector3 を Quaternionで回転させた座標が得られる
        // 今回の場合、上記の回転情報(Quaternion)に、direction(Vector2)を掛けることで、弾の方向のベクトルが決まる(directionをoffsetRotationだけ変える)
    // direction の方向を維持しつつ、Z 軸だけを回転させた情報を持っている新しいベクトルを作成
        Vector2 offsetDirection = offsetRotation * direction;

        return offsetDirection;
    }
}



 Vector3 などのベクトルとは、数値上は単純に3軸の情報(成分)での単位ですが、
これらの成分は、大きさ(速度)、方向、回転も加味して作られているということを理解しておくことが大切です。

 Vector2 offsetDirection = offsetRotation * direction;

 この処理は、弾の向きをプレイヤーの向いている向きに合わせている処理です。
丸い弾ですとイメージしにくいですが、矢のように先端のあるもので考えるとわかりやすいです。
上記の処理によって、方向 + 回転の情報をつかい、矢の先端をプレイヤーの反対方向に向けて(敵側に矢じりを)合わせている感じです。


<2.Bullet クラス>


 弾のプレハブにアタッチするクラスです。
Rigidbody を制御して、弾を移動させる機能を持ちます。


using UnityEngine;

public class Bullet : MonoBehaviour
{
    [SerializeField] private float destroyTime = 1.5f;


    /// <summary>
    /// バレット発射
    /// </summary>
    public void Shoot(Vector2 direction, float bulletSpeed)
    {
        if (TryGetComponent(out Rigidbody2D rb))
        {
            rb.AddForce(direction * bulletSpeed);
        }

        Destroy(gameObject, destroyTime);
    }
}


サンプル画像

コメントをかく


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

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

Menu



技術/知識(実装例)

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

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

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

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

レースゲーム(抜粋)

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

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

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

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

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

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

VideoPlayer イベント連動の実装例

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

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

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

private



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

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