8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UnityAdvent Calendar 2023

Day 12

Unity Searchを拡張する(QueryEngine編)

Last updated at Posted at 2023-12-11

TL;DR

  • Unity Searchの一部として、任意のコレクションに対するクエリを文字列を元に行うモジュールとして QueryEngine<T>が提供されている
  • QueryEngine<T> 利用することで、Unity Searchのクエリ式(!=検索式)のような構文のクエリを任意の対象に対して簡単に実現できる

はじめに

この記事は Unity Advent Calendar 2023 シリーズ1 の 12日目の記事です。

trapezoid(Haruto Otake)です。普段はモバイルゲームの基盤開発の仕事をしています。
先日、Unity UI 完全に理解した勉強会Unity Searchを拡張して高度な検索型UIを実現するという講演をさせてもらいました。

この講演ではUnity標準のAsset検索機能、Unity Searchを独自に拡張して、高度な検索型UIを実現するための手法を紹介しました。

今回の記事では、この講演で紹介しきれなかったUnity SearchのAPI、QueryEngine<T>について紹介していきます。

クエリ式

Unity Searchの基本にもなるクエリ式では、アセットの様々な属性に対する条件を記述することができるようになっています。

クエリ式内ではフィルタという識別子ごとに、演算子とオペランドを指定できます。
例えば、

path:DirName

のように指定した場合、構造として分解すると以下のような形になります。

  • pathフィルタ(アセットパス)を対象とする
  • :演算子(指定したオペランドを部分文字列として含む場合にtrueとなる)
  • DirName(オペランド)

つまり、アセットパスにDirNameを部分文字列として含むアセットに絞り込むという意味になります。operatorは:,=,!=,>=,<=,>,<などの基本的なものを使うことができ、これらをAND/OR等の論理演算で合成して、複雑な条件をクエリ式として表すことができます。

QueryEngine<T>

クエリ式のドキュメントにも記載されているように、これらのクエリ式の機能はQueryEngine<T>というAPIを通して実現されています。

そして、実はこのQueryEngine<T>は、任意のT型のオブジェクトのコレクション(IEnumerable<T>)に対して、クエリ式のような文字列指定での高度な動的フィルタ処理を可能にするAPIとして実装されているため、

アセット以外の任意の対象に対して、Unity Search同等のクエリ機構を実現する手段として用いることができます。

(ただし、Unity SearchのAPIなので、利用できるのはEditor内に限ります)

QueryEngine<T>の使い方

QueryEngine<T>は、UnityEditor.Search.QueryEngine<T>として提供されています。
具体的な使い方を説明します。

Filterの定義

まず、以下のような検索対象のクラス、コレクションがあるとします。

public class QueryEngineSample
{
    public struct Record : IEquatable<Record>
    {
        #region IEquatable<Record>
        //IEquatable<Record>の実装は省略。
        //後のテストで使うだけなので、QueryEngineの動作には不要
        #endregion
    
        public string String;
        public int Int32;
        public long Int64;
        public float Float;
        public double Double;
    }

    public static void Run ()
    {
        //検索対象のサンプルを配列として定義
        var sources = new Record[]
        {
            new Record() {String = "st1", Int32 = 1, Int64 = 101, Double = 200.1d, Float = 300.1f},
            new Record() {String = "st2", Int32 = 2, Int64 = 102, Double = 200.2d, Float = 300.2f},
            new Record() {String = "st3", Int32 = 3, Int64 = 103, Double = 200.3d, Float = 300.3f},
            new Record() {String = "st4", Int32 = 4, Int64 = 104, Double = 200.4d, Float = 300.4f},
            new Record() {String = "st5", Int32 = 5, Int64 = 105, Double = 200.5d, Float = 300.5f},
        };
    }
}

最低限のサンプルとしてStringフィールドの値を元にクエリを出来るようなQueryEngineを定義していきます。

var queryEngine = new QueryEngine<Record>();
queryEngine.AddFilter(nameof(Record.String), r => r.String);

Recordが検索対象の検索対象の型なので、QueryEngine<Record>をnewして、
AddFilter<TFilter>

  • 定義するフィルタ名
  • フィルタのRecord型からの値解決の方法のdelegate

を渡します

TFilterは標準ではプリミティブ型/文字列型などをそのまま指定することができるので、ここでは単純にStringフィールドをstringとしてそのまま返しています。

クエリの実行

//クエリ文字列を渡して、フィルを実際に実行するためのParsedQueryを得る
var query = queryEngine.ParseQuery("String=st3");
//ParsedQueryを使って、任意のコレクションに対して実際にフィルタをかける
var filtered = query.Apply(sources);
//結果の確認
Assert.IsTrue(filtered.SequenceEqual(sources.Where(r => r.String == "st3")));

クエリを行うにはまず、クエリ文字列をParseQueryメソッドに渡してParsedQuery<T>を取得します。
これに対してApplyを実行すると、任意のコレクションに対して実際を絞り込みをかけることができます。

Filterの複数定義

先のコードではStringフィルタのみを定義しましたが、フィルタはAddFilterを複数回呼ぶことで、複数定義することができます。

queryEngine.AddFilter(nameof(Record.Int32), r => r.Int32);
queryEngine.AddFilter(nameof(Record.Int64), r => r.Int64);
queryEngine.AddFilter(nameof(Record.Float), r => r.Float);
queryEngine.AddFilter(nameof(Record.Double), r => r.Double);

これで、String以外のフィールドを対象にした絞り込みを、フィルタを使って行うことができるようになります。

{
    var query = queryEngine.ParseQuery("Int32>2");
    var filtered = query.Apply(sources);
    Assert.IsTrue(filtered.SequenceEqual(sources.Where(r => r.Int32 > 2)));
}
{
    var query = _queryEngine.ParseQuery("Int32>1 and Int64<105");
    var filtered = query.Apply(sources);
    Assert.IsTrue(filtered.SequenceEqual(sources.Where(r => r is { Int32: > 1, Int64: < 105 })));
}

比較を行う演算子を利用したり、論理演算で複数のフィルタを利用することもできます。

Filterを伴わないクエリ

Unity Searchではフィルタを指定せずに任意の文字列をクエリとして入力すると、アセット名による検索が実行されますが、このFilterを伴わないクエリも同じくQuertyEngine<T>で実現できます。

queryEngine.SetSearchDataCallback(r =>
{
    return new string[]
    {
        r.String, r.Int32.ToString(), r.Int64.ToString(),
        r.Float.ToString(), r.Double.ToString()
    };
});

SetSearchDataCallbackに、レコードを引数として受け取り、任意の複数の文字列(IEnumerable<T>)を返す処理をdelegateとして渡すと、返り値の文字列を各レコードの検索対象文字列として認識します。

var query = _queryEngine.ParseQuery("st3 or 104");
var filtered = query.Apply(sources);
Assert.IsTrue(filtered.SequenceEqual(sources.Where(r => r.String == "st3" || r.Int64 == 104)));

あとは、Filterのときと同様にクエリを記述すれば、部分マッチする文字列があるレコードのみに絞り込まれるようになります。

さいごに

いかがでしたでしょうか。
QueryEngine<T>はUnity Searchの一部として提供されており、冒頭で紹介した講演で話したようなUnity SearchのSearchProviderを実装する際に有用なのはもちろんですが、
Editor内でクエリによるフィルタ処理を実装したい場面全般で活用できる、非常に便利なAPIになっています。

Unity Searchを使った検索型UIの実装についても冒頭で紹介したスライド及びアーカイブ動画をご覧いただければと思うので、興味があれば是非こちらも御覧ください!

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?