【Unity】TimelineのClipの見た目拡張

Timelineで利用するClipの拡張をする場合、ClipEditorクラスを継承をして実装を行います。

[CustomTimelineEditor(typeof(HogeClip))]
public class HogeClipEditor : ClipEditor
{

}

CustomTimelineEditor Attributeは拡張対象になるClipのクラスをしていします。

ClipDrawOptions

ClipEditorのGetClipOptionsを使えば簡単に色々な設定ができます。

ClipDrawOptionsで設定できるもの
  • エラー文言
  • Clip名の表示非表示
  • Tipsの文言
  • ハイライト色の指定
  • Icon

デフォルトでは次のように設定されています。

public virtual ClipDrawOptions GetClipOptions(TimelineClip clip)
{
    return new ClipDrawOptions()
    {
         errorText = GetErrorText(clip),
         tooltip = string.Empty,
         highlightColor = GetDefaultHighlightColor(clip),
         icons = System.Linq.Enumerable.Empty<Texture2D>()
     };
}

iconに関しては複数設定可能です。 見た目はこのような感じになります。

f:id:albatrus:20210715112035p:plain
ClipにIconを2個並べてみる

その他、拡張できるもの

その他拡張ができそうな項目

  • Clipの名前
  • 背景の拡張

Clipの名前の変更

ClipEditorにOnCreateメソッドがります。 これはClipを作成したときに呼び出されるのですが、次のようにdisplayNameを変更することができます。

public override void OnCreate(TimelineClip clip, TrackAsset track, TimelineClip clonedFrom)
{
     var hogeClip = clip.asset as HogeClip;
     if (hogeClip == null)
     {
        return;
     }

     clip.displayName = "ほげくりっぷ";
}

f:id:albatrus:20210715114855p:plain
Clip名を変更してみる

背景の拡張 DrawBackground

DrawBackgroundメソッドをCLipが描画されるときに呼び出されます。 Clipの情報が引数で渡されるので、これを使って拡張ができます。

public override void DrawBackground(TimelineClip clip, ClipBackgroundRegion region)
{
    var bgSize = new Rect(region.position.x, region.position.y, region.position.width, region.position.height);
    EditorGUI.DrawRect(bgSize,Color.blue);
}

f:id:albatrus:20210715145730p:plain
Clip背景を青色にする

バックログを仕込んだらよく分かるのですが、呼び出される頻度がとても高いメソッドですので、ロジックを書く場合にはなるべく重い処理は避ける必要がありそうです。

小ネタ

ClipBackgroundRegionに渡されるposition.heightに関してはハイライト色を除いた高さが渡されている。 ですので少し調整が必要。 f:id:albatrus:20210715150722p:plain:w300

最後に

Clipに関してはある程度決まった箇所の変更はできそうです。 この辺りを使って使いやすいようにカスタマイズを行っていきましょう。

参考リンク

qiita.com

forum.unity.com

【Unity】TimelineのTrackAsset周りを調べてみる

TimelineのTrackを自作するときにTrackAssetクラスを使います。 このクラスでは何ができるのかを調べてみました。

f:id:albatrus:20210713111001p:plain:w300
TimelineのTrack

TrackAsset

自作のTrackを作る場合にはTrackAssetクラスを継承します。

public class TestTrack : TrackAsset
{
}

Clip指定する

Trackで指定できるClipをTrackClipType Attributeを使って指定します。

[TrackClipType(typeof(TestClip))]
public class TestTrack : TrackAsset
{
}

対象のオブジェクト指定する

Timelineでアニメーションを行うオブジェクトを指定する場合には、TrackBindingType Attributeを使います。

[TrackClipType(typeof(TestClip))]
[TrackBindingType(typeof(Image))]
public class TestTrack : TrackAsset
{
}

Trackの色を指定する

Trackの判別をするために色を付けることができます。 こちらはTrackColor Attributeを使って指定します。

[TrackClipType(typeof(TestClip))]
[TrackBindingType(typeof(Image))]
[TrackColor(255,0,0)]
public class TestTrack : TrackAsset
{
}

f:id:albatrus:20210713112733p:plain:w450
TrackとClipの色を変更

メソッド

TrackAssetで定義されているメソッドを調べてみました。

docs.unity3d.com

GetClips

Trackで設定されているClipを取得できます。

public IEnumerable<TimelineClip> GetClips()

CreateTrackMixer

このメソッドを継承することでClip同士のブレンド周りの処理を自作できます。

public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
    var mixer = ScriptPlayable<TestMixer>.Create(graph, inputCount);
    // 各mixerの処理
    return mixer;
}

GatherProperties

TimelineからTrackを削除したときに呼び出されます。 利用方法としては、Trackに設定されているobjectを元に戻すために利用することが多いみたいです。

public override void GatherProperties(PlayableDirector director, IPropertyCollector driver)
{

}
PlayableDirector

PlayableDirectorはTimelineを管理するコンポーネントです。

f:id:albatrus:20210713123240p:plain:w400
PlayableDirector コンポーネント

Trackに設定されているObjectを取得する場合GetGenericBindingを使います。 Imageの色を戻す場合は次のようにGatherPropertiesの記述ができそうです。

var bindingImage = director.GetGenericBinding(this) as Image;
if(bindingImage != null)
{
    // Bindingされたobjectがある場合、元に戻している
    driver.AddFromName<Text>(bindingImage.gameObject, "m_Color");
}

tsubakit1.hateblo.jp

【Unity】ExecuteAlwaysについて

Unityで使うc#のクラスでは色々なAttributeを定義することができます。 その中で「ExecuteAlways」というAttributeがあります。

docs.unity3d.com

ExecuteAlways

簡単な例は次のようになります。

[ExecuteAlways]
public class ExecuteAlwaysTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Start");
    }
}

通常、Unityを再生しないとStartは呼び出されませんが、ExecuteAlwaysの場合 Prafabモードに移行したときや、再生が終わった時などに呼び出されます。

OnValidate

MonoBehaviourには、Onvalidateというメソッドがあり、これを使うとInspectorの変更時の更新処理ができます。

#if UNITY_EDITOR
    private void OnValidate()
    {
        Debug.LogError("OnValidate:" + _value);
    }
#endif

今まではこちらを使っていたのですが、ExecuteAlwaysに関しても何かしらできそうだなと感じました。

【Unity】自作のPlayableを作成しTimelineで再生できるようにする

Playable API周りについての概要がなんとなくわかってきたので、次にScriptablePlayableを自作してみたいと思い調査をしてみました。 最終目標は独自のTrackをTimelineで再生をすることです。

ScriptPlayable を自作する

ScriptPlayableを作るにはPlayableBehaviourを継承してクラスを作成します。 では適当にPlayableを作ってみます。

public class CustomPlayable : PlayableBehaviour
{
    // PlayableGraph再生時
    public override void OnGraphStart(Playable playable)
    {
        Debug.Log("Graph Start");
    }

    // PlayableGraphストップ時
    public override void OnGraphStop(Playable playable)
    {
        Debug.Log("Graph Stop");
    }
}

これはPlayableGraphで使って見ます。

 _playableGraph = PlayableGraph.Create();
           
var customPlayable = ScriptPlayable<CustomPlayable>.Create(_playableGraph, 0);
        animationPlayableOutput.SetSourcePlayable(customPlayable);
var scriptOutput = ScriptPlayableOutput.Create(_playableGraph, "custom");
scriptOutput.SetSourcePlayable(customPlayable);
            
 _playableGraph.Play();

Unityで再生するとPlayableGraph Visualizerではこのように表示されています。 (デバックログも吐き出されています)

f:id:albatrus:20210708175742p:plain
PlayableGraph Visualizerの中身

ProcessFrame

PlayableBehaviourにProcessFrameというメソッドが定義されています。 PlayableGraph再生時の経過情報を受け取ることができます。

public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{

}

FrameDataにはdeltaTimeなどの情報がありますのでこれを使うことで、何かしらのアニメーションを設定できたりできそうです。

Playableの時間を設定する

現状、Unity再生中にこのPlayableはずっと再生されています。 次に、Playableの再生時間を設定する方法を調べます。

public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
    var time = playable.GetTime();
    var duration = playable.GetDuration();
}

現在の経過時間はPlayableのGetTimeで取得でき、Playableの再生時間はGetDurationで取得ができます。 これを使い、必要な処理を行うことができます。

再生時間はPlayableのSetDurationで指定できます。

playable.SetDuration(1.0f);

ProcessPrameは経過時間を過ぎても呼び出し続けますので、もし止めたい場合には playable.Pause()を呼び出します。

 playable.Pause();

Timelineで使いたい

Playableについて少しわかってきたので、当初の目的であるTimelineで使えるようする方法が調べました。

blog.unity.com

必要なクラス

TImelineで用意するためには次の3つを表示できるクラスです。

  • Track
  • Playable
  • PlayableAsset (Clip)

f:id:albatrus:20210709150426p:plain
PlayableをTimelineで使うために必要な要素

f:id:albatrus:20210709151946p:plain:w300
編集のためのInspector

Playableを定義する

TImelineが再生されたときのClipの挙動をPlayableで作成されています。 今回はUGUIのImageの色を変更するできるように実装をしました。

public class CustomTimelineClipPlayable : PlayableBehaviour
{
    public Color StartColor = Color.white;
    public Color EndColor = Color.white;
    
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        var image = playerData as Image;
        if (image != null)
        {
            var duration = playable.GetDuration();
            var time = playable.GetTime();
            image.color = Color.Lerp(StartColor, EndColor, (float) (time / duration));
        }
    }
}

Color.Lerpを使って色変化を実装しているのですが、もしかしたらもっと良い方法あるかもしれません。

PlayableAsset

次にTimeline上にClip表示と編集ができるようにPlayableAssetを作成します。 今回はImageの始値終値を設定できるようにしました。

[System.Serializable]
public class CustomTimeLinePlayableAsset : PlayableAsset
{
    public Color startColor = Color.white;
    public Color endColor = Color.white;

    // Factory method that generates a playable based on this asset
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var playable = ScriptPlayable<CustomTimelineClipPlayable>.Create(graph);
        var behaviour = playable.GetBehaviour();

        // PlayableへInspectorでの設定値を渡しています。
        behaviour.StartColor = startColor;
        behaviour.EndColor = endColor;
        
        return playable;
    }
}

Trackを自作する

Timelineで使うTrackを作成します。

[TrackClipType(typeof(CustomTimeLinePlayableAsset))]
[TrackBindingType(typeof(Image))]
public class CustomTimelineTrack : TrackAsset
{
    
}

TrackBindingTypeで対象のImageを設定できるようにしています。

確認

これで準備完了です。 TimelineでTrackを作成するときに、新しく「CustomTimelineTrack」が選択できるようになっているはずです。

最後に

とりあえず、自作のPlayableをTimeline上で動かせるようになりました。 ただ、簡単なPlayableでしたので、今後はもう少し複雑なPlayableが作れるようにしたいないと思いました。

【Unity】Playable と Timelineについて

これまで触ってなかったUnityの「TimeLine」について調べる機会がありました。 その中で「Playable」についてブログに書いていきたいと思います。

Playable API

調べるとUnityには「Playable API」と呼ばれるものがあるのがわかりました。 これは、アニメーションを処理Animator Controllerとかで使われている内部処理を行っているのがPlayable Apiです。

Animation Controller

個人的にAnimator Controllerも余り触ったことないのですが、次の画像のようにStateでAnimationを管理しています。

f:id:albatrus:20210707121036p:plain
Animator ControllerのWindow

PlayableGraph

PlayableGraph を使用すると、複数のデータソースをミックス、ブレンド、変更し、1 つの出力として再生できます。 例えばAnimator Controllerで行うことのできるAnimationの再生やブレンドなどが実装できます。Animationだけではなく、Audioや独自に定義したScriptなどを組み合わせることができます。

また、Scriptで生成ができるので、ゲーム再生中などで動的に作成することができます。ですので、AnimatorControllerのように事前にAssetを用意しておく必要はありません。

Playable API - Unity マニュアル

PlayableGraph visualizer

PlayableGraphのPlayableがノードベースで確認できるWindowです。 PackageManagerからインストールできます。

GitHub - Unity-Technologies/graph-visualizer: Visualizer for your Playable graphs

f:id:albatrus:20210707125454p:plain
PlayableGraph Visualizer

PlayableGraphの生成
var playableGraph = PlayableGraph.Create()
PlayableGraphの再生
playableGraph.Play();

Playable

playable は IPlayable インターフェースを実装する C# 構造体です。 Playableを組み合わせてPlayableGraphを作っていきます。

主なPlayable
  • AnimationClipPlayable
  • AnimationMixerPlayable
  • CameraPlayable
  • TextureMixerPlayable
PlayableGraphにPlayableを紐付ける

playableを生成するときに、PlayableGraphを紐付けます。

var animationClipPlayable = AnimationClipPlayable.Create(playableGraph, clip);

PlayableOutput

PlayableGraphはPlayableOutputを使って出力を行います。 出力形式に応じてOutputクラスが違います。

主なPlayableOutput
  • AnimationPlayableOutput
  • AudioPlayableOutput
  • ScriptPlayableOutput
  • TexturerPlayaleOutput
PlayableGraphにPlayableOutputを紐付ける

playableOutputを生成するときに、PlayableGraphを紐付けます。

var playableOutput = AnimationPlayableOutput.Create(playableGraph, "Name", animator);

簡単な例

AnimationClipを再生するPlayableGraphです。

// 生成
_playableGraph = PlayableGraph.Create();
        
// PlayableGraphに PlayableとOutputを紐付ける
var animationClipPlayable = AnimationClipPlayable.Create(_playableGraph, _clip);
var animationPlayableOutput =
AnimationPlayableOutput.Create(_playableGraph, "Animation", GetComponent<Animator>());
        
// 出力設定
animationPlayableOutput.SetSourcePlayable(animationClipPlayable);

// 再生
_playableGraph.Play();

f:id:albatrus:20210708123532p:plain
シンプルなPlayableGraph

Playable詳細

visualizerの中でノードをチェックすると詳細を見ることができます。 AnimationClipの場合、Timeが渡されています。このTimeを元にアニメーションを再生しています。

f:id:albatrus:20210708123739p:plain:w300
Timeのところが更新されている

PlayableとTimeline

最後に本来調べていた項目であるTImelineとの関係です。 まず何もないTimelineをVisualizerで見ると次のように表示されます。

f:id:albatrus:20210708130257p:plain
空のTimeline

Visualizerに表示されるということはTimeLineはPlayableGraphで構成されていそうです。

Trackを追加してみる

試しにActivation TrackをTimelineに追加をしてみると、PlayableGraphの構成が変わりました。

f:id:albatrus:20210708151808p:plain
Activation trackを追加した場合

挙動だけを見ると「ActivationMixer」と「Playable」が追加された感じです。

複数追加すると

次にもう一つActivation Trackを追加してみます。

f:id:albatrus:20210708152301p:plain:w400
Trackが2つの場合

TimelineのTrackごとにOutputが設定されます。 今回は同じTrackを追加したのでOutputはScriptOutputになっています。

TimelinePlayable

Outputする前にかならずTimelinePlayableを通っています。 TimelinePlayableでは、 TrackとそのClipを担当しているPlayableを作成してリンクする役割をしています。

docs.unity3d.com

最後に

今回調べてみて、Playableについてなんとなくその概要がわかってきました。 次はTimelineで使われているPlayableを拡張周りをどうやってやるのかや、実際にPlayableを使ったらないができるかなどが気になりましたので、その辺り調査したいと思っています。

【C#】クラスをDeep Copyする

ローカルに保存したデータクラスを変更した場合、その参照先のデータも変更されます。 例えば、Saveボタンがあり、ボタンを押したときだけデータクラスを変更したい場合には、変更前の値をどこかに保持しなくてはいけません。

単純に次のように別の変数を用意しても、クラスが参照型のため、currentInfoが変わった時、prevInfoも変わってしまいます。

// HogeInfoはデータクラス
HogeInfo prevInfo = currentInfo

この場合、クラスをDeepCopyをすれば問題が解決できます。

DeepCopy

DeepCopyを行うときには Activator.CreateInstance()を使い、新たにクラスインスタンスを作成します。

次に、Copyをする対象の変数を取得し、コピー元のあたりを使って上書きます。

public static HogeInfo DeepCopy(HogeInfo self)
{
    // インスタンスを作成
    var instance = Activator.CreateInstance(typeof(HogeInfo), true) as HogeInfo;
    var type = self.GetType();

    // データクラスはプロパティで構成されているのでプロパティを取得しています。
    var properties = type.GetProperties();
    foreach (var property in properties)
    {
        property.SetValue(instance, property.GetValue(self));
    }
    return instance;
}

Activator.CreateInstance

var instance = Activator.CreateInstance(typeof(HogeInfo), true) as HogeInfo;

docs.microsoft.com

第2引数ではコンストラクタを公開しているかどうかのフラグです。

今回していない場合はtrueを選択します。 また別途パラメーターがあるコンストラクタを定義している場合には、パラメーターなしのコンストラクタを用意する必要があります。

public HogeInfo()
{
}

置き換え処理

今回の例ではプロパティの値を置き換えていました。

var properties = type.GetProperties();
foreach (var property in properties)
{
     property.SetValue(instance, property.GetValue(self));
}

プロパティ以外にも取得する方法があります。 例えばInstanceされたprivate変数を取得する場合は次のような記述ができます。

// private変数の場合
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
foreach (var field in fields)
{

}
BindingFlags

BindingFlagsはこちらを参考に。 docs.microsoft.com

使用例

DeepCopyは次の用に利用します。

HogeInfo prevInfo = HogeInfo.DeepCopy(currentInfo);

これで変更前の値を保持したローカルクラスを作成できます。

【Unity】クラス同士の比較を行いたいときに IEquatableを使う

とあるデータクラスを作ったときに、その中身が同じかどうかを比較をしたい場合にIEquatableを使って比較するのが良いと教わったので、実装をしてみました。

IEquatable

このインターフェイスは等価かどうかを判断するEqualsが定義されています。 継承したクラスではEqualsを使って判定を行います。

 public interface IEquatable<T>
  {
       bool Equals(T other);
  }

docs.microsoft.com

実装

たとえば次のような情報のデータクラスがあるとします。

public class HogeInfo : 
{
    public string Id {get; private set;}

    public int Value {get; private ser;}

    public HogeInfo(string id, int value)
    {
        Id = id;
        Value = value;
    }
}

このクラスにIEquatableを使って等価判定用のメソッドを用意します。

public class HogeInfo : IEquatable<HogeInfo>
{
    public string Id {get; private set;}

    public int Value {get; private ser;}

    public HogeInfo(string id, int value)
    {
        Id = id;
        Value = value;
    }

     public bool Equals(HogeInfo other)
     {
        if (other == null)
        {
            return false;
        }

        return Id.Equals(other.Id) &&
               Value.Equals(other.Value);
    }
}

比較する

実際に等価比較を行う場合に定義をしてEqualsを使います。

var info1 = new HogeInfo("hoge", 100);
var info2 = new HogeInfo("hogehoge", 100);
var info3 = new HogeInfo("hoge", 50);
var info4 = new HogeInfo("hoge", 100);

bool isEqual1 = info1.Equals(info2);  // false
bool isEqual2 = info1.Equals(info3);  // false
bool isEqual3 = info1.Equals(info4);  // true

最後に

当初はIEquatableを使わずに専用の判定メソッドを用意していたのですが、C#の振る舞い的に等価判定はEqualsを使っているのでそれを踏襲した形で対応をしました。