おもちゃラボ

Unityで遊びを作ってます

【Unity】C#のデリゲートとコールバック超入門

C#で時々使われるDelegateは、調べてみると委譲とかラムダ式とかコールバックとか、更に難しい単語が出てきて混乱しますね。そこで、ここではC#のDelegateについて簡潔に解説してみたいと思います!

Delegateとは

Delegateは一言でいうと「メソッドを代入できる型」です。int型の変数には整数を代入できるし、string型には文字列を代入できますね。これと同じでDelegateで作った型にはメソッドを代入できます。

f:id:nn_hokuson:20210901212135p:plain:w350

メソッドを代入できる型があって何が嬉しいかというと、コールバック(Callback)という仕組みがこれで実現できるようになるのです。

コールバックとは

例えばTimerクラスと、それを呼び出すSystemクラスがあり、Timerのカウント0になったらSystemクラスにそれを伝えたい場合を考えます。このカウントが終わったことを伝える矢印が、コールバックと呼ばれる仕組みになります。

これを実現するには、予めTimerクラスのdelegateにカウントダウン後に呼び出したいメソッドを設定しておきます。カウントが0になったときにdelegateを通してこのメソッドを実行することで、コールバックが実現できるというわけです。

f:id:nn_hokuson:20210901232621p:plain:w550

理屈だけではわかりにくいので、実際にプログラムを書いて確かめてみましょう。

タイマーを作ってみる

Timerクラスを作る

では実際にDelegateを使ってコールバックの仕組みを作ってみましょう。まずはTimerクラスは次のようになります。

public class Timer 
{
    public delegate void OnCompleteDelegate();
    public OnCompleteDelegate onComplete;
    
    public void CountDown()
    {
        // 本当はちゃんと3秒かけてカウントする
        Debug.Log(3);
        Debug.Log(2);
        Debug.Log(1);
        
        // コールバック
        onComplete();
    }
}

ここではTimerクラスの中でdelegateを使って「OnCompleteDelegate」という型を作っています。 デリゲート型の定義はdelegate キーワードを用いて次のように記述します。

delegate 戻り値の型 デリゲートの型名(引数);

イメージ的にはClassやStructで型を作るのと同じですね。次の行では、いま作ったOnCompleteDelegate型を使ってonComplete変数を作成しています。CountDownメソッドの中では、カウントが0になったタイミングでonCompleteに登録したメソッドを呼び出しています。

呼び出し元のクラスを作る

次に呼び出し元のクラスを作ります。

public class System : MonoBehaviour
{
    void Start()
    {
        Timer timer = new Timer();
        timer.onComplete += Alarm;
        timer.CountDown();
    }

    void Alarm()
    {
        Debug.Log("カウントダウン終了");
    }
}

ここではタイマーのインスタンスを作成し、デリゲート(onComplete)にAlarmメソッドを登録しています。その後CountDownメソッドを呼び出すことで、カウントダウンが開始され、最後にonCompleteデリゲートに登録したAlarmメソッドが呼び出されます。

実行してみる

これを実行すると、コンソールには次のように表示されます。

f:id:nn_hokuson:20210901233355p:plain:w300

UnityEventを使って作り直す

Delegateを使うとコールバックの仕組みが作れることが分かりました。ただ、Delegateで型を宣言してから、変数を作るところが少しメンドウですね・・・。そこでUnityにはUnityEventという仕組みが用意されていて、コールバックを簡単に記述できるようになっています。

UnityEventを使って書き換えてみましょう。まずTimerクラスは次のようになります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events; // 必要!

public class Timer 
{
    public UnityEvent onComplete = new UnityEvent();
    
    public IEnumerator CountDown()
    {
        // 本当はちゃんと3秒かけてカウントする
        Debug.Log(3);
        Debug.Log(2);
        Debug.Log(1);

        // コールバック
        onComplete.Invoke();
    }
}

まずUnityEventクラスを使うため、「using UnityEngine.Events;」を追加しています。またdelegateの代わりにUnityEventクラスの変数を作成しています。カウントダウン後はonComplete変数に登録したメソッドを呼び出すため、UnityEngineのInvokeメソッドを実行しています。

また、Timerを呼び出す側のSystemクラスは次のようになります。

public class System : MonoBehaviour
{
    void Start()
    {
        Timer timer = new Timer();
        timer.onComplete.AddListener(Alarm);
        timer.CountDown();
    }

    void Alarm()
    {
        Debug.Log("カウントダウン終了");
    }
}

delegatreを使ったときはonCompleteにメソッドを直接代入していましたが、UnityEventを使う場合はAddListenerメソッドを使ってコールバックに使うメソッドを指定します。

実行してみて、カウントダウン後に「カウントダウン終了」と表示されるかを確かめてみてください!

【Unity】VuforiaでARの背景にエフェクトをかける

f:id:nn_hokuson:20210830153153p:plain:w600
Vuforiaのカメラ映像にエフェクトをかけたいときの方法を紹介します。エフェクトをかけるための流れは

になります。

ポストエフェクト用のシェーダを作る

Vuforiaで背景映像に対してエフェクトをかけるにはシェーダを使います。こちらで紹介しているポストエフェクト系のシェーダですが作り方は少し違います。

nn-hokuson.hatenablog.com

まずはVuforiaPostEffectという名前のシェーダを作成してください。このシェーダは上の記事と同じように、背景をモノクロにするシェーダです。

Shader "Custom/VuforiaPostEffect" {
    // Used to render the Vuforia Video Background

    Properties
    {
        [NoScaleOffset] _MainTex("Texture", 2D) = "white" {}
        [NoScaleOffset] _UVTex1("UV Texture 1", 2D) = "white" {}
        [NoScaleOffset] _UVTex2("UV Texture 2", 2D) = "white" {}
    }

    SubShader
    {
        Tags {"Queue" = "geometry-11" "RenderType" = "opaque" }
        Pass {
            ZWrite Off
            Cull Off
            Lighting Off

            CGPROGRAM

        //Custom Shaderwords for different texture formats
        #pragma multi_compile VUFORIA_RGB VUFORIA_YUVNV12 VUFORIA_YUVNV21 VUFORIA_YUV420P VUFORIA_YUVYV12

        #pragma vertex vert
        #pragma fragment frag

        #include "UnityCG.cginc"
        #include "../Library/PackageCache/com.ptc.vuforia.engine@66f2f26b3878-1627315709000/Vuforia/Shaders/VuforiaVideoBackground.cginc"

        v2f vert(appdata_base v)
        {
            return vuforiaConvertRGBVert(v);
        }

        half4 frag(v2f i) : COLOR
        {
          //RGBからモノクロ変換
            half4 c = vuforiaConvertRGBFrag(i);
            half gray = c.x * 0.3 + c.y * 0.6 + c.z * 0.1;
            c = half4(gray, gray, gray, 1);

#ifdef UNITY_COLORSPACE_GAMMA
            return c;
#else
            return half4(GammaToLinearSpace(c.rgb), c.a);
#endif	
        }

        ENDCG
    }
    }
    Fallback "Legacy Shaders/Diffuse"
}

フラグメントシェーダの中で、vuforiaConvertRGBFragメソッドを使ってピクセルの色をとりだし、それをモノクロに変換して出力しています。

なお、vuforiaConvertRGBVertやvuforiaConvertRGBFragメソッドはVuforiaVideoBackground.cgincで定義されているメソッドなので、28行目でインクルードしています。

ARCameraにシェーダを設定する

VuforiaのAR Cameraにはポストエフェクト用のシェーダを設定するためのスロットが用意されています。そこに今作ったシェーダを設定します。

ヒエラルキーウインドウでAR Cameraを選択して、インスペクタの「Open Vuforia Engine Configuration」ボタンをクリックしてください。

f:id:nn_hokuson:20210829064413p:plain:w600

次にインスペクタから「Video Background Shader」の欄を探して、そこに作成したVuforiaPostEffectシェーダをドラッグ&ドロップします。これでVuforiaの背景映像にポストエフェクトをかけることができます。

f:id:nn_hokuson:20210829065302p:plain:w250

確認とまとめ

シェーダの設定ができたら実行してみましょう。次のように背景映像がモノクロになっていれば成功です!

f:id:nn_hokuson:20210830153153p:plain:w400

また、今回作ったシェーダはあくまでも背景映像用のシェーダなので、ARで表示するオブジェクトに対してはエフェクトがかからないことに注意してください!もし、ARオブジェクトも含めてトータルでエフェクトをかけたい場合は、これまでどおり通常のポストエフェクトを使用します。

nn-hokuson.hatenablog.com

Vuforiaの背景映像にエフェクトをかける方法を紹介しました。シェーダを使っているので、色の変更だけでなくアニメーションに対応したエフェクトや、画面のズームアップなど、さまざまな効果を追加することができます。ぜひ試してみてくださいね!

【Unity】Listの内容をDebug.Logで表示する

UnityでListの内容を表示するため、次のように書いたとしましょう。

List<string> names = new List<string>(){"おそ松", "トド松", "カラ松", "チョロ松", "一松", "十四松"};
Debug.Log(names);

本当は全員分の名前が表示されてほしいのですが、残念ながら次のように「List自体」の情報が表示されてしまいます。

f:id:nn_hokuson:20210824193906p:plain:w500

foreachを使う(けど不便)

そこで普通は次のようにforeachで1つずつ表示するのですが、プログラムが長くなったり、表示が改行されたりして不便ですねー

List<string> names = new List<string>(){"おそ松", "トド松", "カラ松", "チョロ松", "一松", "十四松"};

foreach(string name in names)
{
      Debug.Log(name);
}    

結果は次のようになります。
f:id:nn_hokuson:20210824194612p:plain:w250

Joinメソッドを使う

そこで、Listの内容を表示するためJoinメソッドを使ってみます。Joinメソッドを使うことで、次のようにスッキリ書けます。

List<string> names = new List<string>(){"おそ松", "トド松", "カラ松", "チョロ松", "一松", "十四松"};

Debug.Log(string.Join(",", names)); 

stringクラスにはstaticメソッドとしてJoinメソッドが用意されています。Joinメソッドは第2引数に渡した文字列のListを連結して一つの文字列にするメソッドです。連結する各要素の間には第1引数の文字列が挿入されます。

コンソールウインドウには次のように表示されます。バッチリですね!
f:id:nn_hokuson:20210824195218p:plain:w500

文字列以外のListについて

Joinメソッドの第2引数にはstring型のListを渡す必要があるので、残念ながらint型のListなどをそのまま渡すことはできません。この場合は次のように書きます。

List<int> nums = new List<int>(){1, 2, 3, 4, 5};

Debug.Log(string.Join(",", nums.Select(n => n.ToString())));    

まず、LinqのSelectメソッドを使ってそれぞれの要素を取り出します。次に各要素に対してToStringメソッドを使って文字列のリストに変換しています。Linqのメソッドを使うため「using System.Linq」が必要になります。

結果は次のような表示になりました。
f:id:nn_hokuson:20210824200700p:plain:w300

まとめ

Listの内容をDebug.Logで表示したいときにはJoinメソッドとSelectメソッドを組み合わせると便利!

Linqの各メソッドについては「確かな力が身につくC#超入門」でも解説しているので、あわせてどうぞ〜