C++幼女先輩

プログラミング成分多め

GoでメソッドチェインのMapReduceライブラリ作ろうか悩んだ

全部For文で筋力プレイしたくない

ってことで、MapReduce系のライブラリ作ろうかと悩んで下記のコードをサンプルで作った

Go Playground - The Go Programming Language

// You can edit this code!
// Click here and start typing.
package main

import "fmt"

type Map[T any] struct {
    value []T
    err   bool
}

func NewMap[T any](v []T) *Map[T] {
    return &Map[T]{value: v, err: false}
}

func errMap[T any]() *Map[T] {
    return &Map[T]{nil, true}
}

func copyMap[T any](v []T, e bool) *Map[T] {
    return &Map[T]{value: v, err: e}
}

// var errMap Map[T] = &Map[T]{value: nil, err: true}

func (s *Map[T]) Foreach(fn func(T) T) *Map[T] {
    if s.err {
        return errMap[T]()
    }

    nv := make([]T, len(s.value))
    for i, v := range s.value {
        nv[i] = fn(v)
    }

    return copyMap(nv, false)
}

func (s *Map[T]) Ok() *Map[T] {
    if s.err {
        return errMap[T]()
    }
    return copyMap(s.value, false)
}

func (s *Map[T]) Ng() *Map[T] {
    if s.err {
        return errMap[T]()
    }
    return copyMap(s.value, true)
}

func (s *Map[T]) Get() ([]T, bool) {
    return s.value, s.err
}

func main() {
    hoge := NewMap[int]([]int{1, 2, 3, 4, 5})

    result, err := hoge.Ok().Ok().Foreach(func(v int) int { return v * 2 }).Get()
    if err {
        fmt.Println("Error")
    } else {
        fmt.Printf("%v\n", result)
    }

}

思い付きで書いたコードだけど
他のMapReduceライブラリだと、リフレクションを使うもの
コード自動生成系

があったが
リフレクションは最終手段にしたい事、interface{}になりキャストが発生
実行速度や動作不安定さが怖いので使いたくない
コードジェネレート系も、色々なからみで使いたくない

ので、ジェネリクスを使いサンプル実装してみた

まず、メソッドチェインをしたかったが、Goの基本は 値とerrのタプルを返却するものなので
タプルだとメソッドチェインが出来ないため
ジェネリクスを使い2値をもつStructを作りメソッドチェインを実現させ
Goには例外がないので、メンバ変数でErrを保持し処理をし
最後にGetメンバで、値とErr値を取得

最低限の動作はできそう

本当は遅延評価とかしたいんですけどね・・
Goで遅延評価考えたくないし、そもそもクロージャの記述がだるく、関数型に向いてないよねえ

例外処理的な事考えずに素直に関数型っぽくする方が楽かなあ・・

Docker-OSX再び

Docker-OSXKVM上で動いているようだ
KVMLinuxカーネルで動くように出来ているため

当然Linuxからは期待できる
MacOSからDocker-OSXを動かす事は現時点では絶望的と思った

問題はWindowsのWSL2
結論から言うとWindows11のWSL2からは可能らしい
wsl.confに nestedVirtualization=true を設定が必要
Windows10のWSL2では、以前Microsoftから、insider previewにてnestedVirtualizationに対応するというアナウンスがあったが
現在はそのアナウンスがなくなり、Windows10では諦めた可能性がある
そのため、現時点ではWindows10では無理かもしれない

Windows11のマシンを作り、テストを行おうかなあ?

エンジニアトーク YoutubeLive1回目終了&Windows10の上にDockerでXCODEビルド&デバッグ環境を作りたい

youtu.be

エンジニアトークのYoutubeLiveを毎週水曜20時~行う事にしました
初回なので、企画の説明や自己紹介的なものをメインにお話ししました
企画の趣旨としては、自分の興味ある話題を中心に質問や相槌を受けながらトークをする

私が一般的な会話が出来ず、エンジニアの話題で盛り上がるのが一番楽しいので
誰も来なければ私の独壇場になってしまうので、誰か来て話題ふってください・・

そして、とても収穫になった話題

視聴者さんからの相談で、Dockerを使ってスマホ開発環境をローカル汚染せずに作りたいと
Androidの場合は、LinuxのDockerイメージにAndroidStudioをインストールし、TCP接続で実機あるいはローカルのエミュに接続出来る事は想像できる
一応、以前Docker上のLinuxにxrdpをインストールして、リモートマシンから接続しGUIを使える事は確認している
ただ当時は、動きが少し怪しかったけど、今は直ってればいいなあ
Windows11だとWSL2でネイティブにGUI対応しており、WindowsのWindowと同じように扱えるらしい
興味ある

ただし、XCODEをDockerで動かす事は考えた事がなかった
HakintoshやOSX-Dockerという、MacOSを他のOSで動かすプロジェクトは知っていて
動かせる事はしっていたものの、安定稼働するか不安だし、そもそも合法とはいえない
Legality of Docker-OSX · Discussion #267 · sickcodes/Docker-OSX · GitHub

個人で使うにはあれだが、仕事で使う事が現状どうなのか?と言われるとねえ

ただ、実際にMacbookも色々あって5台ぐらい所持しているし、個人で遊ぶ分には許してね!とは思わなくもないので
ちょっと調査してみたい

Docker-OSX用のGPUドライバも動くか気になるし、色々なポイントはある

github.com

Windows11上のWSL2でしか動かないという話だが、xrdp使ってGUI起動できる可能性もある

それとは別に、Windows11のWSL2は、Linux GUIWindowsと同列に実行できて興味もある

WSL で Linux GUI アプリを実行する | Microsoft Docs

もちろんWindows2022のWindowsコンテナも気になるし

AWSサーバレスを使いたい。 その資格はない、おぉその資格はない

モチベーション

プログラマとして、EC2なりECSにモノリシックなサーバを作る方が慣れているが
サーバレスを積極的に使いたい
どちらもメリットはあるが
サーバレス(ファンクション単位)にすると、ライブラリを使ったり管理が面倒だったり
デプロイ作業がやりにくかったりする
お互いの機能を呼び出すにもAPIで無駄なリクエスト投げ合ったり。。
ただし、小さい関数を作るには非常に向いているし、サーバ料金が安くなる事が多い
なんといっても、サーバ管理やスケーリング等が全て不要になる

昔はnode.jsが多かったが、今はランタイムも色々選べるようになったし
カスタムランタイムを使えば何の言語でも実行可能

これはやらなければならない

フレームワーク

サーバレスはデプロイが面倒
モノリシックサーバの場合はEC2なりコンテナを作り、ソースをコピーすればいいが
サーバレスだと、API Gatewayを設定したり、Lambdaの関数をUploadして設定したり
AWSコンソールで色々と設定が必要
デプロイも、関数毎にわかれているので、関数の数だけデプロイが必要
デプロイ忘れの関数が残ってると本当に大変
なので、何らかのフレームワークを使うのが良いと思う

今回はAWS限定で

CloudFormation

AWSの公式の IaaC(Infrastructure as Code)
要は面倒なAPI GatewayやLambda、あるいはDynamoDB等のデプロイを
スクリプトを書いて自動化できる
Terraformみたいなやつ
基本的にAWSのデプロイはこれを使うので下記のフレームワークもCFを使用している

Amplify、AppSync

GraphQLベースのサーバレスアプリケーション用のフレームワーク
という認識

SAM(AWS Serverless Application Model )

AWS公式のサーバレス用フレームワーク
下記のServerlessFrameworkと機能的によく似ている
が、AWS限定である

ServerlessFramework

オープンソースのサーバレスのデプロイやテストを行うフレームワーク
上記のSAMはAWS専用だが、こちらはAzure、GCP、ALIクラウド、テンセントクラウド・・に対応している
ServerlessFrameworkの方が進んでたが、最近はSAMも十分に機能があると思うのでどちらを使っても良い
ただ、ServerlessFrameworkの方が情報が多い気がするので、今回はこちらを使う

作るもの

API GatewayがWebsocket対応していて、使ってみたかったので、Websocketにする
まずはチャットサービスを作ろうと思う

準備

aws-cli、node、その他必要なものは入っている認識で

serverless-frameworkをインストール

serverlessコマンドでテンプレート作成やデプロイが出来るが、エイリアスとして sls slssが割り当てられている
sls コマンドでプログラムのテンプレートを作成可能
slsを入力すると

❯ AWS - Node.js - Starter 
  AWS - Node.js - HTTP API 
  AWS - Node.js - Scheduled Task 
  AWS - Node.js - SQS Worker 
  AWS - Node.js - Express API 
  AWS - Node.js - Express API with DynamoDB 
  AWS - Python - Starter 
  AWS - Python - HTTP API 
  AWS - Python - Scheduled Task 
  AWS - Python - SQS Worker 
  AWS - Python - Flask API 
  AWS - Python - Flask API with DynamoDB 
  Other 

と、テンプレートを選ぶ事が可能だが、一覧にないものを選ぶには、-t オプションを使う

sls create -t aws-nodejs-typescript -n sample
と打つと、Typescriptを使った、sampleアプリのテンプレートが生成される

途中でAWSのCredencialを設定したり、IAMのロールが必要
IAMはAdmin権限のあるユーザーを作りその情報を入力すれば通った

今回はメニューより、Express API with DynamoDB を選びそこから改良する事に
Typescript化は今度やる

このテンプレートは、DynamoDBを使用し、REST APIにより usersで登録、user/:userId で情報取得
のようなものになっている

sls deploy で、AWSで上記の構成とコードのアップロードが出来る
以降は、毎回全部deployしなくとも関数単位でのdeployも可能
sls deploy function -f エントリポイント
簡単にGithubと連携する事も出来るっぽいが、それは後日しよう

ただし、今回はローカルでテスト実行しようと思う
serverless-offlineと、serverless-dynamodb-local をインストールする必要がある
このあたりは、npmコマンドでインストールする
これらはServerlessFrameworkのプラグインである

npm i serverless-offline serverless-dynamodb-local

dynamodb-localに関しては本体(AWS提供)をインストールする必要がある

sls dynamodb install

ただし、dynamodb-localはJavaで動くのでJREが必要

そして、上記プラグインを serverless.ymlに記載する

plugins:
#  - serverless-webpack
  - serverless-dynamodb-local
  - serverless-offline

webpackは今は使わないが、今後使う予定
プラグインは順番も重要なので必ず上記の順番で書くように

sls offline start
でオフラインで動くはずである・・・ が、上手く動作しない
シンプルなテンプレート(DynamoDBを使わない)だとちゃんと動作するので
dynamodb-localの動作に問題があるようだ

試しにサーバにデプロイしたらちゃんと動いた
ただし、色々と serverless.ymlやコードをいじったり、AWSのリージョン指定したりCredencialをいじったらそのうち動くようになった
まあ、WindowsでWSL2上で動かしたりしているので、内部IPアドレスまわりの問題かもしれない

dynamodb-localの動作確認は aws dynamodb list-tables --endpoint-url http://localhost:8000 などで出来る。

Websocket化

github.com

これを見ながら書けばいけた
とりあえず覚えておくこととして

serverless.ymlに

functions:
  connect:
    handler: onconnect/app.handler
    events:
      - websocket:
          route: $connect
  disconnect:
    handler: ondisconnect/app.handler
    events:
      - websocket:
          route: $disconnect
  sendmessage:
    handler: sendmessage/app.handler
    events:
      - websocket:
          route: sendmessage

こんな感じでRouteを指定し、それぞれのhandlerコードを書いた
あとは上記URLを参考に。。

sls offline start
でオフライン実行できる

ただし、RestAPIのようにCurlで叩けないので、Websocket用のツール wscatをインストールする

wscat -c ws://localhost:3001/dev
とすると、Websocketに接続し、プロンプト状態になる

{"message":"sendmessage", "data":"hello world"}

というデータを送るとレスポンスが返ってくる

hello world

ローカルでの動作も確認できた

github.com

やり残し作業

  • TypeScript化
  • 自動Deploy
  • Client(S3とCloudFront あと、Reactかなあ?)
  • ルーム機能(DynamoDBのキー変更)
  • メッセージに発言者ID等を付与
  • ログイン(名前入力&LocalStorage保存程度)
  • ログインユーザ一覧

UNITY Editor拡張で、タイムラインをスクリプトから生成する

お題

タイムラインを自動生成しよう
トラックやクリップも外から作成してみます
それに伴い、エディタ拡張の勉強

メニューよりEditorWindowを表示

とりあえず最も汎用的な?メニュの拡張から

メニューの拡張

Editor拡張で最も使われると思うので、よーく覚えよう

UnityEditor.MenuItem - Unity スクリプトリファレンス

MenuItemを使う
ドキュメントの通り、非常に機能が豊富なので細かい事は説明しません

まず、Editor専用コードなので、Editorディレクトリを作りその下にコードを置く事
そして、Editor拡張は静的関数しか呼べないので、そういう設計にする事

主な機能をざっと

メニューアイテムの追加

[MenuItem("hoge/fuga")]
のように属性をつけると、メニューに hoge->fuga と追加される
メニュー名に日本語も使えます
当然そのメニューを選んだ時に、その関数が呼ばれます
ホットキーも簡単に付ける事ができます
また、Validateもする事が出来ますし、メニューの表示順番も変更できます
Validate関数を設定すれば、関数がFalseを返すと無効、Trueを返すと有効になります

ヒエラルキー

MenuItemのパスの頭をGameObjectにすると、ヒエラルキーで右クリックした際のメニューに追加できます

    [MenuItem("GameObject/CustomEditor")]
    private static void Create()
    {
        Debug.Log("Menu");
    }

f:id:murasame-labo:20210618062518p:plain

ヒエラルキー右クリックでちゃんと表示されました

f:id:murasame-labo:20210618063104p:plain

もちろん、メニューの のGameObjectにも表示される

Assets

MenuItemのパスの先頭をAssetsにすると、Assetsビューのコンテキストメニューに表示される

    [MenuItem("Assets/CustomEditor", false, 1)]
    private static void Create()
    {
        Debug.Log("Menu");
    }

f:id:murasame-labo:20210618063228p:plain

もちろん、メニューのAssetsにも表示される

f:id:murasame-labo:20210618063546p:plain

CONTEXT

コンポーネントの 右上の3点アイコンを押したときのメニューを作れます!
もちろん、自作コンポーネントだけじゃなく、UNITYのデフォルトコンポーネントも可能です

    [MenuItem("CONTEXT/PlayableDirector/CustomEditor", false, 1)]
    private static void Create()
    {
        Debug.Log("Menu");
    }

今回はPlayableDirectorコンポーネントにメニューを追加しました

f:id:murasame-labo:20210618064111p:plain

今回使うもの

今回は、選択したGameObjectにPlayableDirectorコンポーネントを追加したいので
ヒエラルキー上のGameObjectのコンテキストメニューに追加する

コンテキストメニュー

public class CustomEditorWindow : EditorWindow
{

    [MenuItem("GameObject/CustomEditor", false, 1)]
    private static void Create()
    {
        // 生成
        GetWindow<CustomEditorWindow>("CustomEditor");

    }

ヒエラルキーのGameObjectメニューにCustomEditorを追加します

そして、CustomEditorWindow(自分のクラス)を呼び、GUIを表示させます
今回は EditorWindowを継承し、ウインドウを表示させます

EditorWindow

    private void OnGUI()
    {
        using (new GUILayout.HorizontalScope())
        {
            targetGo_ = Selection.activeGameObject;

            if (GUILayout.Button("CreateTimeline"))
            {
               // ここにボタン押したときの処理が入る
            }
        }
    }

とりあえずボタンが1個あるだけの単純なウインドウ

f:id:murasame-labo:20210618070129p:plain

以上でEditor拡張のおさらい終了

Timeline等をコードから生成

本題という事になる

GameObjectにPlayableDirectorのアタッチ

                if(targetGo is null)
                {
                    return;
                }
                // 選択中のGameObjectにPlayableDirectorコンポーネントをつける
                var director = targetGo.GetComponent<PlayableDirector>();
                if(director == null)
                {
                    director = targetGo.AddComponent<PlayableDirector>();
                }

AddComponentを使って普通にPlayableDirectorを作成する

TimelineAssetを作成しAssetデータベースへ保存する

TimelineAssetを作成します
つまり、タイムラインの情報すべてを保存するものです
ScriptableObjectで作成します

ただし、このままでは、シーン上に作られます
実際の運用では、タイムラインはファイル(Asset)にして読み込んで使うと思うので
AssetDatabaseに登録します
拡張子は playableです

                // TimelineAssetの作成
                var timelineAsset = playableDirector.playableAsset as TimelineAsset;
                if (timelineAsset == null)
                {
                    timelineAsset = ScriptableObject.CreateInstance<TimelineAsset>();
                    director .playableAsset = timelineAsset;
                }


                var path = "Assets/Resources/sample.playable";
                AssetDatabase.DeleteAsset(path); 
                AssetDatabase.CreateAsset(timelineAsset, path);
                AssetDatabase.SaveAssets();

AssetDatabaseの仕様で、ディレクトリが無い場合はエラーになるので、ディレクトリは事前に作っておく事
そして、既にAssetが存在する場合は上書きするとエラーになるので、事前にDeleteしておきます
SaveAssetは、即座にデータベースに反映するおまじないです

トラックの作成&バインディングを設定

今回はGroupトラックを作成し、そこにトラックを作成します
型は前回作った NewPlayableTrackです
Bindingにはゲームオブジェクトをつけます。今回はCubeという名前のゲームオブジェクトをつけます

                var groupTrack = timelineAsset.CreateTrack<GroupTrack>(null, "Group");
                var track = timelineAsset.CreateTrack<NewPlayableTrack>(groupTrack, "NewPlayable");

                var b = GameObject.Find("Cube");
                director.SetGenericBinding(track, b);

クリップ作成とKeyFrame作成

クリップを作成します。もちろん NewPlayableAssetで
そしてそのクリップにKeyFrame(AnimationCurve)を設定します

今回は最初が1fで、デフォルトのカーブでエンドが0fになる形で
もちろん、カーブのEaseを変えたり、KeyFrameを増やす事は容易

                var clip = track.CreateClip<NewPlayableAsset>();

                clip.CreateCurves(null);
                var curve = new AnimationCurve();

                curve.AddKey(new Keyframe(0f, 1f));
                curve.AddKey(new Keyframe((float)clip.duration, 0f));
                clip.curves.SetCurve("", typeof(NewPlayableAsset), "hoge", curve);

リフレッシュ

最後にリフレッシュ

                EditorUtility.SetDirty(timelineAsset);
                TimelineEditor.Refresh(RefreshReason.ContentsAddedOrRemoved);

成果物

f:id:murasame-labo:20210618090053p:plain

とりあえず、目的の事は出来た

タイムラインをスクリプトから生成する

CreatePlayable周りの調査。AssetからBehaviourを、BehaviourからBindingを参照したい

CreatePlayable周りがちょっとややこしかったので調査
まず初めに断っておくが、本来はMixerを作るのがベストだが、今回はあえてMixerを作らずに頑張ってみる

ScriptPlayable

        var playable = ScriptPlayable<NewPlayableBehaviour>.Create(graph);
        var trackBinding = go.GetComponent<PlayableDirector>().GetGenericBinding(this) as GameObject;
 
        playable.GetBehaviour().target_ = trackBinding;
        return playable;

前回はこのようにしてBehaviourにバインディングを渡した
もちろん、単なるプロパティ経由なのでどんなものでも渡せる

ただし、この場合 PlayableAsset::CreatePlayableが呼ばれない そのため、PlayableAssetがBehaviourを知る方法がメンドクサイなど問題が生じる
なので、この方式を変更しようと思う

TrackAsset::CreatePlayable

BaseClassのCreatePlayableを呼ぶと、その中からPlayableAsset::CreatePlayableが呼ばれるようになる
具体的には

NewPlayableTrack

return base.CreatePlayable(graph, go, clip);

Assetの方は下記コードで、Behaviourの参照を手に出来る
templateという変数名にするのが通例のようだ
今回は template_というプロパティーを持たせる

    public NewPlayableBehaviour template_;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph, template_);
    }

前にコード書いたときは上記で、template_にBehaviourの参照が渡ってきた覚えがあるが
コード間違えたかUNITYのバージョンか覚え違いか、Nullだったので変更した

    public NewPlayableBehaviour template_;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var playable = ScriptPlayable<NewPlayableBehaviour>.Create(graph);
        template_ = playable.GetBehaviour(); 
        return playable; 
    }

これで確実にtemplate_にbehaviourが設定される

上記で対応できると思ったが、Inspectorを作る時に不具合が生じる。今のところUNITY2020で問題が起こっている・・
UNITY2020では異なる方法で行う必要があるかもしれないので
今後は少し古いが UNITY2019.4での作業に変更

Bindingを渡したい

BehaviourにてトラックにBindingされたオブジェクトの操作をしたい事は多々ある
が、UNITYのTimelineはBehaviourから簡単にBindingが参照できない(ほんとこれは不便)
なので、どこかでBindingを渡す必要がある

まずBindingの取得方法だが、TrackAsset::CreatePlayable あるいは、PlayableAsset::CreatePlayableの引数の
GameObjectが、タイムラインのGameObjectになっていて
そこからPlayableDirectorコンポーネントを取得し、その中のBindingsから該当するトラックのBindingを検索する
具体的なコードは

        var trackBinding = go.GetComponent<PlayableDirector>().GetGenericBinding(key) as GameObject;

もちろんこのgoがGameObjectだが、問題はこのKeyである
Directorから何らかの方法でKeyを取得できるかもしれないが
ここはTrackAssetのthisを渡すのが簡単なので、TrackAssetから渡したい

結果

NewPlayableTrack

[TrackBindingType(typeof(GameObject))]
[TrackClipType(typeof(NewPlayableAsset))]
public class NewPlayableTrack : TrackAsset
{
    protected override Playable CreatePlayable(PlayableGraph graph, GameObject go, TimelineClip clip)
    {
        Playable playable = base.CreatePlayable(graph, go, clip);
 

        var trackBinding = go.GetComponent<PlayableDirector>().GetGenericBinding(this) as GameObject;
        Debug.Log("Track::" + trackBinding);

        var p = (ScriptPlayable<NewPlayableBehaviour>)playable;
        p.GetBehaviour().binding_ = trackBinding;

        return playable;
    }
}

NewPlayableAsset

public class NewPlayableAsset : PlayableAsset
{
    public NewPlayableBehaviour template_;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var playable = ScriptPlayable<NewPlayableBehaviour>.Create(graph, template_);
        return playable;
    }
}

以上で
PlayableAssetからは templateという名前でBehaviourが扱え
PlayableBehaviourからは binding
という名前でトラックにバインドされたオブジェクトが扱える

UNITYタイムライン PlayableTrackとBinding

PlayableTrack

プログラマとしては本題!
独自の処理をするトラックを作ります
用途はほんと色々あります
だいたい仕事では8割以上はこのカスタムトラックでタイムライン作るかも

Track、Asset、Behaviour、Mixerの4つで構成されます

PlayableTrack

カスタムのClipを入れる事のできるトラックです
UNITYから用意されているPlayableTrackをそのまま使う事も可能ですが
独自のバインディングやら初期化処理などを行う場合には自分で作ります
というか、自分で作りましょう

PlayableAsset

クリップ
実行時にインスタンス化するための情報を保持したりしている
実行時に下記のBehaviourをCreateする

PlayableBehaviour

インスタンス化したあとの実際のふるまいを定義する
カスタムトラックの肝

Mixer

上記のBehaviourの一種
クリップ同士(複数のBehaviour)をブレンドする事ができます

実際に作ってみる

Assets->Create->Playables
で、BehaviourとAssetのスクリプトのテンプレートが作成できる

f:id:murasame-labo:20210603160157p:plain

PlayableAsset

[System.Serializable]
public class NewPlayableAsset : PlayableAsset
{
    // Factory method that generates a playable based on this asset
    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return Playable.Create(graph);
    }
}

上記のような何もしないテンプレートが作成される

PlayableBehaviour

public class NewPlayableBehaviour : PlayableBehaviour
{
    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable)
    {
        
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable)
    {
        
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        
    }
}

こちらは、コールバックのみのBehaviourが作成されました

BehaviourとAssetを連結させる

とりあえずPlayableTrackを作りクリップを作成する
今回はNewPlayableAssetという名前なのでそれを作成します

先ほどのBehaviourのコールバックにログを仕込んでも、まだ連結させてないのでコールバックが来ません
なので接続しましょう

AssetのCreatePlayableにて、今は汎用的なPlayableを作成していますが、それを自作のBehaviourにします

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return ScriptPlayable<NewPlayableBehaviour>.Create(graph);
    }

これで、今回作ったNewPlayableBehaviourが呼ばれるようになり、ちゃんとコールバックが呼ばれるように

ScriptPlayable

    public struct ScriptPlayable<T> : IPlayable, IEquatable<ScriptPlayable<T>> where T : class, IPlayableBehaviour, new()
    {
        public static ScriptPlayable<T> Null { get; }

        public static ScriptPlayable<T> Create(PlayableGraph graph, int inputCount = 0);
        public static ScriptPlayable<T> Create(PlayableGraph graph, T template, int inputCount = 0);
        public bool Equals(ScriptPlayable<T> other);
        public T GetBehaviour();
        public PlayableHandle GetHandle();

        public static implicit operator Playable(ScriptPlayable<T> playable);
        public static explicit operator ScriptPlayable<T>(Playable playable);
    }  

IPlayableBehaviourを持つBehaviourをCreateしたり。
GetBehaviourとかGetHandleとかもしかして使うかもね

PlayableTrack

TrackAssetから派生する

[TrackClipType(typeof(NewPlayableAsset))]
public class NewPlayableTrack : TrackAsset
{
    protected override Playable CreatePlayable(PlayableGraph graph, GameObject go, TimelineClip clip)
    {
        Playable playable = base.CreatePlayable(graph, go, clip);
        return playable;
    }
}  

TrackClipTipeにカスタムのAssetを指定する
先ほどつくったPlayableTrackを削除し、今度は今作ったカスタムTrackを作成 そのトラック上で右クリックをすると、先ほど作ったNewPlayableAssetのクリップを作成できる

TrackBinding

カスタムトラックにオブジェクトをバインディングできます アトリビュートを追加

[TrackBindingType(typeof(GameObject))]
[TrackClipType(typeof(NewPlayableAsset))]

他にもトラックの色を変えたりアトリビュートがある
TrackBindingを設定すれば指定したオブジェクトに対して影響を与えるトラックが作れます
今回はGameObject型にしていますが、Animatorを設定したり出来る
TrackBindingにシーン上のCubeを指定しよう

f:id:murasame-labo:20210603202430p:plain

TrackBindingの取得

ここが非常に昔から不満点ですが
UNITYのTimelineではBehaviourからTrack情報を取得するのが難しいです

クリップの開始は OnBehaviourPlayのコールバックで取得しますがそのプロトタイプは

  public override void OnBehaviourPlay(Playable playable, FrameData info)

となっており、トラックの情報が取れません
なので以下の3パターンで取得する事になります

Behaviour#ProcessFrameで取得

ProcessFrameだけはプロトタイプが

public virtual void ProcessFrame(Playable playable, FrameData info, object playerData);

となっており、playerDataにBindingが入っているのでCastすればよいです

    public GameObject target;

    public virtual void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        target = playerData as GameObject;
    }

ただし、ProcessFrameは毎フレーム呼ばれる処理です。最初のフレーム時のみtargetを取得するように改造が必要ですが
そもそも初期化時に設定すればいいものを毎フレーム処理したくありません

Mixerを作りMixerのOnBehaviourPlay時に渡す

今回はMixerを作っていないので省略しますが
わりと見ますね
ただし、Mixerを作る必要があるのでMixer不要な場合は手間です

TrackのCreatePlayable時に渡す

今回はこれでいきます
Trackのコードを

    protected override Playable CreatePlayable(PlayableGraph graph, GameObject go, TimelineClip clip)
    {
        var playable = ScriptPlayable<NewPlayableBehaviour>.Create(graph, 1);
        var trackBinding = go.GetComponent<PlayableDirector>().GetGenericBinding(this) as GameObject;
        playable.GetBehaviour().target = trackBinding;
        
        return playable;
    }

CreatePlayableで渡ってきたGameObjectからPlayableDirectorを取得し、そこからBindingを取得して
作成したBehaviour(Playable)に渡す

これでOK