拡張メソッドのサンプル(Unityで使う)
サンプルから拡張メソッドの仕組みを学びましょう
基本の確認
拡張メソッドの動作を簡単なサンプルで確認してみましょう。
まずは、Unityの移動の基本を確認します。
オブジェクトの配置
新しいプロジェクトを作成し、Cubeを1つシーンに作っておきます。
コード
次の移動コードを試してみましょう
BasicMove.csスクリプト
positionプロパティに移動先(Vector3:3次元)を代入します。
using UnityEngine;
public class BasicMove : MonoBehaviour
{
void Start()
{
transform.position = new Vector3(1, 1, 1);
}
}
実行結果
ProjectウィンドウにTweenスクリプトがありますが、今は無視してください。

(1, 1, 1)の座標に移動しました
UnityEngineのTransform クラスには、positionプロパティがあるので代入できるわけですね。
(参考)Transformクラス
UnityEngineのTransformクラスを参考のため、掲載します。(一部略)
positionがプロパティになっているのがわかります。
public class Transform : Component
{
public Vector3 position {
get {
get_position_Injected (out Vector3 ret);
return ret;
}
set {
set_position_Injected (ref value);
}
}
}
TWeenの自作(基本編)
ここで、次のようなメソッドを実行してみたいとしましょう
transform.MoveTo(new Vector3(1, 1, 1));
動作するでしょうか?
エラーになりますよね。TransfromクラスにMoveToなどというメソッドがないからです。(私が勝手に名前をつけました)
拡張メソッドを使うとこのようなことが可能になります。
オブジェクトの配置
Cubeオブジェクト
Cubeオブジェクトを1つ作成し、以降で作成するTweenスクリプトをアタッチします。

スクリプト
Tween.cs スクリプト
上記メソッドの作り方を参考に今回のメソッドを実現するためにMyTweenクラスを追加したのが次のコードになります。
Startメソッドで、Tfansformクラスの拡張メソッド(MoveToメソッド)が実装できています。
あたかもTransformクラスにMoveToメソッドが存在しているかのように振る舞っていますね。
using UnityEngine;
public class Tween : MonoBehaviour
{
void Start()
{
transform.MoveTo(new Vector3(1, 1, 1));
}
}
static class MyTween
{
public static Transform MoveTo(this Transform tran, Vector3 vector3)
{
tran.position = vector3;
return tran;
}
}
メソッドチェーンへの発展
メソッドチェーンとは、メソッドを .(ドット演算子)でつなぎ合わせ、メソッドの連続実行ができるように実装することをいいます。
メソッドチェーンのメリットについて具体的にみていきましょう。
変数代入パターン
まず、それぞれを代入文で分けた場合を見てみましょう。
次のコードを見て下さい。
trans1には、(1, 1, 1)へ移動後のTransformインスタンスが代入されます。
trans2には、その後、(2, 2, 2)へ移動後のTransformインスタンスが代入されます。
最後の行は、その後、(3, 3, 3)へ移動後のTransformインスタンスが代入されます。
Transformインスタンスのpositionプロパティがメソッドごとに変更されることになります。
サンプルコード
using UnityEngine;
public class Tween : MonoBehaviour
{
void Start()
{
Transform trans1 = transform.MoveTo(new Vector3(1, 1, 1));
Transform trans2 = trans1.MoveTo(new Vector3(2, 2, 2));
trans2.MoveTo(new Vector3(3, 3, 3));
}
}
メソッドチェーン(横並び)へ変更
この順番に代入しているメソッドを2つにまとめてみましょう。
インテリセンスのインライン化で自動変換してくれますので使ってみましょう。
つなぎ合わせると次のようになります。
それまで、1つずつ作成していた変数が不要になりましたね。
サンプルコード
using UnityEngine;
public class Tween : MonoBehaviour
{
void Start()
{
transform.MoveTo(new Vector3(1, 1, 1)).MoveTo(new Vector3(2, 2, 2)).MoveTo(new Vector3(3, 3, 3));
}
}
メソッドチェーン(レイアウト)へ変更
さらにインテリセンスの折り返しを使うと次のようになります。
サンプルコード
using UnityEngine;
public class Tween : MonoBehaviour
{
void Start()
{
transform.MoveTo(new Vector3(1, 1, 1))
.MoveTo(new Vector3(2, 2, 2))
.MoveTo(new Vector3(3, 3, 3));
}
}
大分、みやすくなりました。
単にメソッドの呼び出しを繰り返す場合、いちいち変数名を考えなくても、メソッドを繋ぎ合わせれば、シンプルになりますね。
実行結果
Startメソッドが1フレーム内で実行されるため、実行すると、瞬間的に(3, 3, 3)に移動するように見えます。
コードでは、ポジションは3回変更されていますが、次のフレームでは、最後の計算結果が表示されるためです。

TWeenの自作(応用編)
メソッドチェーンの実行はできるように更新しましたが、せっかくなので、1秒ごとにチェーンの1つのメソッドが実行できるようにしたいものです。C#とUnityの機能を駆使しながら実現を目指しましょう。
少し難しくなりますが、次の機能を活用します。
- コンポーネントの追加と取得
- Queueクラスの利用
- 座標の計算
- コルーチン
一つずつ解説しますので、順番にみていきましょう。
オブジェクトの配置
Cubeオブジェクト
ギズモが表示されている方のオブジェクトです。

Cube(1)オブジェクト
同じくギズモが表示されている方のオブジェクトです。

Projectウィンドウ
作成するスクリプトとシーンファイルの配置を確認してください。



スクリプト
TweenTest.cs
Cubeオブジェクトにアタッチされているスクリプト
メソッドチェーンで繋いだメソッドは、前のアニメーションが終了したら続いて実行されます。
次のコードは、1秒かけて現在のポジションから(1, 0, 0)に移動するメソッドになります。
.MoveTo(new Vector3(1, 0, 0), 1)
同様に、1秒かけて現在のポジションから(1, 0, 1)に移動するメソッドになります。
.MoveTo(new Vector3(1, 0, 1), 1)
これらのメソッドを繋いでいくことで連続したアニメーションを実行できるようにしていきたいと思います。
全てのコード
using UnityEngine;
using MyTWeenLib;
public class TweenTest : MonoBehaviour
{
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log($"{ gameObject.name}: 移動処理スタート");
transform.MoveTo(new Vector3(1, 0, 0), 1)
.MoveTo(new Vector3(1, 0, 1), 1)
.MoveTo(new Vector3(0, 0, 1), 1);
Debug.Log($"{ gameObject.name}: 移動処理開放。他の処理が実行可能");
}
}
}
TweenTest1.cs
Cube(1)オブジェクトにアタッチされているスクリプト
Cubeと同様ですが、移動位置の変更をしています。
また、2つのオブジェクトが同時にアニメーションできるかをテストする意味もあります。
using UnityEngine;
using MyTWeenLib;
public class TweenTest1 : MonoBehaviour
{
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log($"{ gameObject.name}: 移動処理スタート");
transform.MoveTo(new Vector3(5, 0, 0), 1)
.MoveTo(new Vector3(5, 0, 5), 1)
.MoveTo(new Vector3(0, 0, 5), 1);
Debug.Log($"{ gameObject.name}: 移動処理開放。他の処理が実行可能");
}
}
}
TweenExpansiton.cs
メソッドチェーンを実装するためのスクリプト(拡張メソッド)
Transformクラスの拡張メソッドを作ります。
あたかもTransformクラスに存在するメソッドのように動作します。
このように、現在存在するクラスにコードを使いするのが困難だが、後付けでメソッドを追加したい場合に利用します。
メソッドチェーンにするため、戻り値の型もTransform型になっています。
次のコード抜粋は、移動させるゲームオブジェクトにMoveTWeenスクリプトをアタッチするための部分コードになります。
MoveTWeenスクリプト(コンポーネント)がアタッチされていなければ、AddComponentメソッドでアタッチします。
なお、AddComponentメソッドは戻り値として、アタッチされたコンポーネントの取得ができます。
抜粋
MoveTWeen moveTWeen;
if (startTrans.gameObject.GetComponent<MoveTWeen>() == null)
{
moveTWeen = startTrans.gameObject.AddComponent<MoveTWeen>();
}
else
{
moveTWeen = startTrans.gameObject.GetComponent<MoveTWeen>();
}
すでにアタッチ済みであれば、そのコンポーネントを取得します。
このコードにより、1つのオブジェクトに複数のコンポーネントがアタッチされないようにしています。
プログラムを実行すると、実行中は、Cube, Cube(1)ゲームオブジェクトにmoveTWeenコンポーネントが追加されているのがわかります。
Cubeを選択して、インスペクターを確認してみましょう。
実際のコードでは、上記抜粋のところはnull演算子でリファクタリングをしています。どの場所か確認してみましょう。
全てのコード
using UnityEngine;
namespace MyTWeenLib
{
/// <summary>
/// Tweenアニメーションを実現する拡張メソッドを実装したクラス
/// </summary>
static class TweenExpansion
{
/// <summary>
/// 指示された時間で次の場所に移動するアニメーション
/// </summary>
/// <param name="startTrans"></param>
/// <param name="endPos">移動後の位置</param>
/// <param name="time">移動にかける時間(秒)</param>
/// <returns></returns>
public static Transform MoveTo(this Transform startTrans, Vector3 endPos, float time)
{
Debug.Log($"{startTrans.name} : MoveToメソッドの登録開始");
MoveTWeen moveTWeen = startTrans.GetComponent<MoveTWeen>()
?? startTrans.gameObject.AddComponent<MoveTWeen>();
moveTWeen.MoveObj(startTrans.gameObject, endPos, time);
Debug.Log($"{startTrans.name} : MoveToメソッドの登録終了");
return startTrans;
}
}
}
MoveTWeen.cs
オブジェクトをTWeenするスクリプト(Queueクラスによる待ち行列の作成、コルーチンによる遅延実行)
Tweenアニメーションするコードになります。
1フレームごとの移動距離を計算して、コルーチンで少しずつ移動されるように実現されています。
メソッドチェーンにより記述順通りに実行するのは、Queueクラスの、FIFO(ファースト・イン・ファースト・アウト)を使うことで実現しています。
Queueクラスの使い方
次のように実装しています。
抜粋
Queue moveTWeenQueue = new Queue();
// Queueにセット
moveTWeenQueue.Enqueue(/* セットするオブジェクト */);
// Queueから取り出す
moveTWeenQueue.Dequeue();
実際のコードでは、宣言をQueue<IEnumerator>ジェネリック型を使っています。これにより、型指定を厳密にして、間違って代入されないようにコンパイル時に間違った場合はエラーになるようにしています。どの場所か確認してみましょう。
List<int>のような宣言の方法と同じですね。
全てのコード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyTWeenLib
{
/// <summary>
/// Tweenアニメーションを実行するクラス
/// </summary>
public class MoveTWeen : MonoBehaviour
{
readonly Queue<IEnumerator> moveTWeenQueue = new Queue<IEnumerator>();
/// <summary>
/// スタート時点で実行されるコルーチン
/// キューにデータがある場合、順番に実行されるようにする
/// キューのメソッドが完了するのを待って、次のキューのメソッドを実行
/// コルーチンで、メソッド完了待ち
/// </summary>
/// <returns>イテレータ(コルーチンで必要)</returns>
IEnumerator Start()
{
Debug.Log($"{gameObject.name}: MoveLinearメソッド開始");
while (true)
{
switch (moveTWeenQueue.Count)
{
case 0:
yield return null;
break;
default:
Debug.Log($"{gameObject.name}: TWeen 開始");
yield return StartCoroutine(moveTWeenQueue.Dequeue());
Debug.Log($"{gameObject.name}: Tween 終了");
break;
}
}
}
/// <summary>
/// 実行待ちキューにコルーチンをセット
/// </summary>
/// <param name="obj">移動するゲームオブジェクト</param>
/// <param name="endPos">到達ポシション</param>
/// <param name="moveTime">到達するまでの時間</param>
public void MoveObj(GameObject obj, Vector3 endPos, float moveTime)
{
moveTWeenQueue.Enqueue(MoveLinear(obj, endPos, moveTime));
}
/// <summary>
/// 移動を実施する
/// コルーチンで、移動時間から1フレームで移動する教理を計算して、少しずつ移動するようにしている
/// </summary>
/// <param name="obj">移動するゲームオブジェクト</param>
/// <param name="endPos">到達ポシション</param>
/// <param name="moveTime">到達するまでの時間</param>
/// <returns>イテレーター(コルーチンで必要)</returns>
IEnumerator MoveLinear(GameObject obj, Vector3 endPos, float movetTime)
{
var deltaMovePos = (endPos - obj.transform.position) * Time.deltaTime / movetTime;
float nextDiffPosSize;
float diffPosSize;
do
{
transform.position += deltaMovePos;
diffPosSize = (endPos - transform.position).magnitude;
nextDiffPosSize = (endPos - (transform.position + deltaMovePos)).magnitude;
yield return null;
} while (diffPosSize - nextDiffPosSize > 0);
obj.transform.position = endPos;
Debug.Log($"{gameObject.name}: TWeen終了位置:{endPos}");
}
}
}
コンソール表示ログ
コード中にデバッグログを挿入しています。
次のコードは、実際に実行させて時に表示されるログになります。
Cube (1): 移動処理スタート
Cube (1) : MoveToメソッドの登録開始
Cube (1) : MoveToメソッドの登録終了
Cube (1) : MoveToメソッドの登録開始
Cube (1) : MoveToメソッドの登録終了
Cube (1) : MoveToメソッドの登録開始
Cube (1) : MoveToメソッドの登録終了
Cube (1): 移動処理開放。他の処理が実行可能
Cube: 移動処理スタート
Cube : MoveToメソッドの登録開始
Cube : MoveToメソッドの登録終了
Cube : MoveToメソッドの登録開始
Cube : MoveToメソッドの登録終了
Cube : MoveToメソッドの登録開始
Cube : MoveToメソッドの登録終了
Cube: 移動処理開放。他の処理が実行可能
Cube (1): MoveLinearメソッド開始
Cube: MoveLinearメソッド開始
Cube: TWeen 開始
Cube (1): TWeen終了位置:(5.0, 0.0, 0.0)
Cube (1): Tween 終了
Cube (1): TWeen 開始
Cube: TWeen終了位置:(1.0, 0.0, 0.0)
Cube: Tween 終了
Cube: TWeen 開始
Cube (1): TWeen終了位置:(5.0, 0.0, 5.0)
Cube (1): Tween 終了
Cube (1): TWeen 開始
Cube: TWeen終了位置:(1.0, 0.0, 1.0)
Cube: Tween 終了
Cube: TWeen 開始
Cube (1): TWeen終了位置:(0.0, 0.0, 5.0)
Cube (1): Tween 終了
Cube: TWeen終了位置:(0.0, 0.0, 1.0)
Cube: Tween 終了
実行動画
2つのオブジェクトが同時に移動しているのがわかります。
また、メソッドチェーンに従っての移動も確認できますね。
TWeenの自作(コールバックの実装)
メソッドチェーンによる一連のアニメーション実行が完了した後にメソッドを実行するようにしたいと思います。
このコードでは、前回、MoveObjメソッドに必要であった第一引数の(GameObject型 obj)は、コンポーネント側のgameObjectを使うため、引数を1つ省略したコードに変更しています。少しシンプルになりました。
説明図
イラストを使って説明していますので、各オブジェクト・メソッドの関係を概略把握しておきましょう。
アニメーション動作を担っているクラス(moveTWeenスクリプト)が実行中にそれぞれのオブジェクトにアタッチされる仕組みにしているのを確認しましょう。
スクリプト
TWeenTest.cs
抜粋コードのように、メソッドチェーンの最後にComleteメソッドを追加し、実行したいコード(メソッド)をラムダ式で追加します。
抜粋
.Complete(() => Debug.Log($"{gameObject.name}: 全てのアニメーションが完了"));
復習ですが、ラムダ式の分解をしてみましょう。
ラムダ式を展開すると次のようになります。
抜粋(参考コード)
.Complete(() =>
{
Debug.Log($"{gameObject.name}: 全てのアニメーションが完了");
});
さらに、ラムダ式を使わず、明示されたメソッド(メソッド名がある)にした場合のコードが次になります。
新たにメソッド名が必要で、そのメソッドを名前で呼び出すことになります。
メソッドが他から呼び出される可能性がある場合では、分離することで再利用ができるのでこのパターンを使います。
再度利用されることがない場合は、今回のような、無名(匿名)メソッドにするといいでしょう。
抜粋(参考)
.Complete(CompleteMethod);
// このようにメソッドを呼び出したのと同じ
void CompleteMethod()
{
Debug.Log($"{gameObject.name}: 全てのアニメーションが完了");
}
全てのコード
using UnityEngine;
using MyTWeenLib;
public class TweenTest : MonoBehaviour
{
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log($"{ gameObject.name}: 移動処理スタート");
transform.MoveTo(new Vector3(1, 0, 0), 1)
.MoveTo(new Vector3(1, 0, 1), 1)
.MoveTo(new Vector3(0, 0, 1), 1)
.Complete(() => Debug.Log($"{gameObject.name}: 全てのアニメーションが完了"));
Debug.Log($"{ gameObject.name}: 移動処理開放。他の処理が実行可能");
}
}
}
TweenExpansion.cs
拡張メソッドのクラスになります。
上記の実現のため、Transformクラスに拡張メソッド(Completeメソッド)を追加します。
このメソッドに使う引数は、引数、戻り値なしのコールバック(アニメーションが完了した時に実行されるメソッド自体)になります。
変更点の追加)
MoveTWeenのコンポーネント取得のためのコードをGetMoveTWeenComponentメソッドに分離しておきました。
アニメーションが終了したときのイベント実装は、UnityEventクラスを使っています。
実行されるメソッド自体を引数(型は、UnityActionです)で受け取り、ComleteActionフィールドのイベントにAddListenerメソッドで追加しています。
抜粋
public static Transform Complete(this Transform trans, UnityAction action)
{
var moveTWeen = GetMoveTWeenComponent(trans);
moveTWeen.CompleteAction.AddListener(action);
return trans;
}
static MoveTWeen GetMoveTWeenComponent(Transform trans)
{
return trans.GetComponent<MoveTWeen>()
?? trans.gameObject.AddComponent<MoveTWeen>();
}
Unity Eventのインスタンスがまだ作成されていない場合、ここで作成するようにします。
抜粋
if (moveTWeen.CompleteAction == null)
{
moveTWeen.CompleteAction = new UnityEvent();
}
Unity 2020.2a から C# 8.0 の機能がいくつか使用できるようになりました
C# 8.0 からは、null合体演算子(??=)が使えます
抜粋
moveTWeen.CompleteAction ??= new UnityEvent();
全てのコード
using UnityEngine;
using UnityEngine.Events;
namespace MyTWeenLib
{
/// <summary>
/// Tweenアニメーションを実現する拡張メソッドを実装したクラス
/// </summary>
static class TweenExpansion
{
/// <summary>
/// 指示された時間で次の場所に移動するアニメーション
/// </summary>
/// <param name="startTrans">移動前の位置</param>
/// <param name="endPos">移動後の位置</param>
/// <param name="time">移動にかける時間(秒)</param>
/// <returns>移動後のTransform情報</returns>
public static Transform MoveTo(this Transform startTrans, Vector3 endPos, float time)
{
Debug.Log($"{startTrans.name} : MoveToメソッドの登録開始");
MoveTWeen moveTWeen = GetMoveTWeenComponent(startTrans);
moveTWeen.MoveObj(endPos, time);
Debug.Log($"{startTrans.name} : MoveToメソッドの登録終了");
return startTrans;
}
/// <summary>
/// 一連のアニメーションの終了後、イベントバンドラーを起動
/// </summary>
/// <param name="trans">イベントを発行するオブジェクトのTransform</param>
/// <param name="action">移動チェーン終了後に実行したいイベントハンドラ</param>
/// <returns>移動後のTransform情報</returns>
public static Transform Complete(this Transform trans, UnityAction action)
{
var moveTWeen = GetMoveTWeenComponent(trans);
if (moveTWeen.CompleteAction == null)
{
moveTWeen.CompleteAction = new UnityEvent();
}
moveTWeen.CompleteAction.AddListener(action);
return trans;
}
/// <summary>
/// TWeen移動スクリプト(コンポーネント)の取得
/// </summary>
/// <param name="trans">イベントを発行するオブジェクトのTransform</param>
/// <returns></returns>
static MoveTWeen GetMoveTWeenComponent(Transform trans)
{
return trans.GetComponent<MoveTWeen>()
?? trans.gameObject.AddComponent<MoveTWeen>();
}
}
}
MoveTWeen.cs
Tweenアニメーションを実行するクラスです。
ここに、コールバックの本体を実装していきます。
使うには、まず、次のように宣言とインスタンスの作成をします。
public UnityEvent CompleteAction = new UnityEvent();
nullでない、つまりイベントが登録されていればイベントが実行されます。
その後、イベントを削除して連続して実行されないようにしています。
if (CompleteAction != null)
{
CompleteAction.Invoke();
CompleteAction = null;
}
イベントの登録は、このクラスではなく、TweenExpansionクラスのCompleteメソッドから行います。
全てのコード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace MyTWeenLib
{
/// <summary>
/// Tweenアニメーションを実行するクラス
/// </summary>
public class MoveTWeen : MonoBehaviour
{
readonly Queue<IEnumerator> moveTWeenQueue = new Queue<IEnumerator>();
public UnityEvent CompleteAction = new UnityEvent();
/// <summary>
/// スタート時点で実行されるコルーチン
/// キューにデータがある場合、順番に実行されるようにする
/// キューのメソッドが完了するのを待って、次のキューのメソッドを実行
/// コルーチンで、メソッド完了待ち
/// </summary>
/// <returns>イテレータ(コルーチンで必要)</returns>
IEnumerator Start()
{
Debug.Log($"{gameObject.name}: MoveLinearメソッド開始");
while (true)
{
switch (moveTWeenQueue.Count)
{
case 0:
if (CompleteAction != null)
{
CompleteAction.Invoke();
CompleteAction = null;
}
yield return null;
break;
default:
Debug.Log($"{gameObject.name}: TWeen 開始");
yield return StartCoroutine(moveTWeenQueue.Dequeue());
Debug.Log($"{gameObject.name}: Tween 終了");
break;
}
}
}
/// <summary>
/// 実行待ちキューにコルーチンをセット
/// </summary>
/// <param name="endPos">到達ポシション</param>
/// <param name="moveTime">到達するまでの時間</param>
public void MoveObj(Vector3 endPos, float moveTime)
{
moveTWeenQueue.Enqueue(MoveLinear(endPos, moveTime));
}
/// <summary>
/// 移動を実施する
/// コルーチンで、移動時間から1フレームで移動する教理を計算して、少しずつ移動するようにしている
/// </summary>
/// <param name="endPos">到達ポシション</param>
/// <param name="moveTime">到達するまでの時間</param>
/// <returns>イテレーター(コルーチンで必要)</returns>
IEnumerator MoveLinear(Vector3 endPos, float movetTime)
{
var deltaMovePos = (endPos - transform.position) * Time.deltaTime / movetTime;
float nextDiffPosSize;
float diffPosSize;
do
{
transform.position += deltaMovePos;
diffPosSize = (endPos - transform.position).magnitude;
nextDiffPosSize = (endPos - (transform.position + deltaMovePos)).magnitude;
yield return null;
} while (diffPosSize - nextDiffPosSize > 0);
transform.position = endPos;
Debug.Log($"{gameObject.name}: TWeen終了位置:{endPos}");
}
}
}
コンソール表示ログ
コード中にデバッグログを挿入しています。
次のコードは、実際に実行させて時に表示されるログになります。
Cube (1): TWeen終了位置:(0.0, 0.0, 5.0)
Cube (1): Tween 終了
Cube (1): 全てのアニメーションが完了
Cube: TWeen終了位置:(0.0, 0.0, 1.0)
Cube: Tween 終了
Cube: 全てのアニメーションが完了
ライブラリダウンロード
Unity バージョン 2020.3.0f1以降
クラス図

ディスカッション
コメント一覧
まだ、コメントがありません