LoginSignup
12
7

More than 5 years have passed since last update.

[Unity][Timeline] TimelineWindowの縦横スクロールを操作できるようにする [解説]

Last updated at Posted at 2018-10-31

初めに

先日、Twitterにて表題の内容を実装してみたというツイートを動画付きで投稿したら、思った以上に反響があったため慌ててgithubにコードを公開するということがありました

コードをポイッと公開しただけだとちょっと勿体無いなと思ったので、どのように実装されているかというのをここで解説したいと思います

とにかくコードを見たいという方はgithubのrepositoryへどうぞ!

注意事項

今回の内容は非公開APIをC#のReflectionと言う機能を使って呼び出すことによって実現されています

本来であれば公開されていないものなので、今後のUnityのバージョンアップ内容によっては突然仕様が変わって使えなくなることが起こりえます
そうなった際のメンテナンスは保証しかねるので利用する場合はご了承ください

また、この記事で解説する内容は、全て執筆時点の最新バージョンであるUnity 2018.3.0b7で動作確認を行っています
これ以前・以後のバージョンでは仕様によっては使えないものがあるかもしれませんのでこちらも予めご了承ください

スクロール・マウスホイール動作の標準仕様

実装の解説の前に、まずは標準の仕様についておさらいしましょう

私が把握できているTimelineWindow上におけるスクロールやマウスホイール関連の動作の仕様を列挙してみます
※これ以外にもありましたら教えてください

横軸(時間・フレーム)のスケールを変える

マウスホイールを回すときの標準の挙動です
特定の場所にフォーカスして細かく編集したり、全体を俯瞰してみたりするときに便利です
いや、スクロールしてほしい

縦軸のスケール(トラックの高さ)を変える

ctrl(command)キーを押しながらマウスホイールを回すときの標準の挙動です
正直使いたいと思ったことがありません
だから、スクロールしてほしい

ということでマウスホイールではスクロールさせることはできません

一応クリップの表示エリアに縦横のスクロールバーが用意されているのですが、そこまでマウスカーソルを持っていって掴んで動かすというのは地味に面倒です
サクッとスクロールさせる方法はないのか?

alt(option)キーを押しながらクリップの表示エリアを掴んでドラッグする

スクロールさせる方法、ありました!
alt(option)キーを押しながらクリップの表示エリアを左クリックで掴み、マウスを動かす(ドラッグする)と任意の方向にクリップの表示エリアをスクロールさせることができます

やった!

でもalt(option)キーを押しながら操作するのはそれはそれでまだ面倒…

ここはやはりマウスホイールだけでスクロールさせたい!!!

ということで、ようやく本題に入ります

どうやってスクロールさせるか

スクロールさせたいとはいっても、Timelineのエディタ周りのAPIはTimelineEditorクラスしか公開されておらず(Unity 2018.3.0b7現在)、編集ウィンドウに関しては外から一切触ることが出来ない状態になっています

そこで登場するのがC#のReflectionです

Reflectionに関する細かい解説は本題から逸れていくので割愛しますが、簡単にまとめると、Reflectionを使えば公開されていないAPIも掘り起こして実行することができるのです

ただし、非公開APIを掘り起こせると言っても、どのパラメータを変更すればスクロールを制御することができるかを知っていないと掘り起こすにも掘り起こせません

標準機能の動作からアタリを付けて探す

そこで目をつけるのが、先程紹介したalt(option)キーを押しながらマウスを動かすことでスクロールさせる機能です

この処理を行っているところを見つけることができれば、そこで操作しているパラメータと同じものを操作することでスクロールさせることができるはずです

これでザックリとしたアタリをつけることができたので、Editor上で使用するTimelineの機能がまとまっているUnityEditor.Timeline.dllをdecompileしてそれっぽいことをしているクラスを探し出しましょう
※dllのdecompileについても話が逸れていくので割愛します

UnityEditor.Timeline.TimelinePanManipulator

alt(option)キーとマウスドラッグのイベントを取得しているだろうということにアタリを付けて探していくとTimelinePanManipulatorというクラスを発見することが出来ます

実際にマウスドラッグ中の処理をしていそうなところを覗いてみるとスクロールさせているような実装を見つけることができます
ビンゴ!

TimelinePanManipulator.cs
using UnityEngine;

namespace UnityEditor.Timeline
{
  internal class TimelinePanManipulator : Manipulator
  {
  // 省略 

    protected override bool MouseDrag(Event evt, WindowState state)
    {
      if (!this.m_Active)
        return false;
      Rect treeviewBounds = TimelineWindow.instance.treeviewBounds;
      treeviewBounds.xMax = TimelineWindow.instance.position.xMax;
      treeviewBounds.yMax = TimelineWindow.instance.position.yMax;
      if (!((Object) state.GetWindow() != (Object) null) || state.GetWindow().treeView == null)
        return false;
      // ここ辺りでスクロール処理しているっぽい!
      Vector2 scrollPosition = state.GetWindow().treeView.scrollPosition;
      scrollPosition.y -= evt.delta.y;
      state.GetWindow().treeView.scrollPosition = scrollPosition;
      state.OffsetTimeArea((int) evt.delta.x);
      return true;
    }
  }
}

UnityEditor.Timeline.WindowStateUnityEditor.Timeline.TimelineTreeViewGUI

TimelinePanManipulatorクラスの実装をみる限り、実際にスクロールさせるためにはWindowStateクラスとTimelineTreeViewGUIクラスを使っているようです

どうしてスクロール処理するだけで2つのクラスに別れているのか?と思ったのですが、横スクロールの際はクリップ表示エリアのみを、縦スクロールの際はクリップ表示エリアだけでなくトラック表示エリアも動かせるようにするために処理が別れているようでした

なにはともあれ、操作すべきパラメータが分かってしまえばゴールまでもうあと少しです

ここから更にこの2つのクラスを追いかけていくと、この2つのクラスはUnityEditor.Timeline.TimelineWindowクラスがインスタンスを持っていて、このTimelineWindowはシングルトンパターンになっている事が分かります

そしてこのTimelineWindowクラスはTimelineを編集するためのウィンドウ本体であり、ウィンドウが開かれているときだけシングルトンパターンのインスタンスができるということも判明します

ここまで分かってしまえば、あとはReflectionの機能を使って掘り起こすだけです

TimelineWindowの縦横スクロール処理の全容

// 任意の方向へのスクロール
public static void Scroll(int x, int y)
{
    var assembly = Assembly.Load("UnityEditor.Timeline");

    // TimelineWindowのインスタンスを取得
    var timelineWindowType = assembly.GetType("UnityEditor.Timeline.TimelineWindow");
    var timelineWindowInstanceProp = timelineWindowType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static);
    var timelineWindowInstance = timelineWindowInstanceProp.GetValue(null);
    if (timelineWindowInstance == null)
    {
        Debug.Log("not open TimelineWindow.");
        return;
    }

    // WindowStateのインスタンスを取得
    var windowStateType = assembly.GetType("UnityEditor.Timeline.WindowState");
    var windowStateInstanceProp = timelineWindowType.GetProperty("state", BindingFlags.Public | BindingFlags.Instance);
    var windowStateInstance = windowStateInstanceProp.GetValue(timelineWindowInstance);

    // 横軸スクロール
    var offsetTimeAreaMethod = windowStateType.GetMethod("OffsetTimeArea", BindingFlags.Public | BindingFlags.Instance);
    offsetTimeAreaMethod.Invoke(windowStateInstance, new object[] {x});

    // TimelineTreeViewGUIのインスタンスを取得
    var treeViewType = assembly.GetType("UnityEditor.Timeline.TimelineTreeViewGUI");
    var treeViewInstanceProp = timelineWindowType.GetProperty("treeView", BindingFlags.Public | BindingFlags.Instance);
    var treeViewInstance = treeViewInstanceProp.GetValue(timelineWindowInstance);

    // 縦軸スクロール
    var scrollPositionProp = treeViewType.GetProperty("scrollPosition", BindingFlags.Public | BindingFlags.Instance);
    var scrollPosition = (Vector2) scrollPositionProp.GetValue(treeViewInstance);
    scrollPosition.y += y;
    scrollPositionProp.SetValue(treeViewInstance, scrollPosition);

    // 再描画を依頼する
    TimelineEditor.Refresh(RefreshReason.WindowNeedsRedraw);
}

githubで公開しているコードはクラス分けをしてしまっていて一覧性があまりないため、上記のサンプルでは1メソッドに全て収まる形で記述してみました

あとはEditorWindow上などでマウスホイールのイベントを取ってそのdelta値を流し込んであげればマウスホイールでスクロールができるようになります

実際の用途に合わせてこのコードを改造して使ってもらえれば幸いです

最後に

いかかだったでしょうか
今回のような対応をすることで少し大掛かりな内容にはなってしまいますが、上手い具合に標準機能の動作からアタリをつけることで非公開APIを掘り起こしてコードから自由に呼べることができるというのがC#の面白いところです

この方法を覚えておくと拡張の幅が拡がって楽しくなると思いますので、皆さんも使ってみましょう!
公開API以上に仕様がコロコロ変わるのでメンテナンスには注意!

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7