記事の目次

    今回は、データベースを検索して結果を表示する図鑑コンテンツの実装方法を紹介します。

    TEXT_高田稔則 / Toshinori Takata(Codelight
    EDIT_小村仁美 / Hitomi Komura(CGWORLD)

    今回のサンプルデータはこちら
    https://github.com/toshinoritakata/DictSample

    <1>Unityで検索コンテンツをつくる3つの方法

    こんにちは、高田です。今回は「検索コンテンツ」に関して紹介してみます。

    「検索コンテンツ」とはあるデータの集まりから必要な情報を引き出し、表示させるものと考えてください。弊社では美術品、大学や地域の歴史、機械のパーツ紹介など様々な情報検索コンテンツをUnityで作っています。今回はUnityでの検索コンテンツの作り方の一例を紹介していきます。

    身近な検索コンテンツはWeb上にあります。まずシンプルな検索ページである「Yahoo!きっず図鑑」を見てみましょう。

    ●Yahoo!きっず図鑑
    https://kids.yahoo.co.jp/zukan/


    トップページから「動物」を選ぶと「名前からさがす」「なかまからさがす」「場所からさがす」「大きさからさがす」の4つの方法で動物を探すことができます。

    試しに「名前からさがす」を選び、「あ」で始まるものを探すと、「アイアイ」「アイナメ」「アオウミガメ」「アオオサムシ」などが表示されます。では、これらの動物のデータはどのように管理しているのでしょうか?


    「あ」で始まる動物の検索結果のURLを見てみると、

    https://kids.yahoo.co.jp/zukan/animal/a/a/

    となっています。

    「/a/a」は「あ行」の「あ」を示していると考えられます。このサイトでは複合的な検索ができないので、データ登録の際に、分類に沿った静的なページ構造を作っているのでしょう。こうすることでサーバの負荷を最低限にすることができ、サイトの構築も楽になります。

    <2>データのディレクトリ階層をたどる

    Unityでもこの考え方は利用できます。展示用に作る図鑑コンテンツの場合、サーバを置くことは少なく、それぞれのPC単独で動くように作ることが多い印象です。その理由としては、2つ大きな点があると思います。

    1:不特定多数のアクセスがない
    2:閲覧中に情報の更新がない

    そのため、Yahoo!きっずの図鑑のようにファイル階層そのものを検索画面に反映させる方法が良く使われます。

    それでは、実際に図鑑コンテンツを作ってみましょう。以下のように「海の生き物」、「陸の生き物」フォルダを用意します。


    各フォルダには表示させたい画像を入れておきます。


    これをUnityで表示させるときに自動的にUIを構築していきます。カスタムのファイルビューアを作るイメージです。


    今回のサンプルコードは下記に置いています。
    https://github.com/toshinoritakata/DictSample

    今回はSample1〜3の3つを用意しました。Sample1がここで説明するディレクトリを探索していくタイプで、Sample1.csだけで処理が完結しています。Sample2、Sample3は後半で説明します。

    Dataフォルダ内で見つかったデータがフォルダの場合は名前付きボタン、jpgファイルの場合は画像を表示します。



    この作り方の利点は大きく2つあります。

    1:プログラムが簡単になる
    2:指定通りにファイルとフォルダを作ってもらえればそのまま反映できるため、直感的に管理できる

    利点1で大きいのはUnityに集約できることだと思っています。検索コンテンツだけHTMLで構築して、CSSでページレイアウトを行なったり、JavaScriptでページ遷移の演出を入れたりするのは対応が煩雑で難しいため、Unityで全て行えるのは助かります。

    利点2は、データ構造がわかりやすく、コンテンツの運営スタッフの人が理解しやすいことです。登録のプログラムも不要でルール通りにファイルを置いてもらうだけです。実際、この方法はとても多く使われており、図鑑コンテンツの8割方はこの方法で済ませることができます。

    <3>データベースを使って検索できるようにする

    しかし、この方法では、ページをたどるだけで検索を行うことができません。検索できるようにするにはどうすれば良いでしょうか。

    メトロポリタン美術館の、所蔵美術品を検索できるページを例にみてみましょう。
    https://www.metmuseum.org/art/collection/search


    ここで、素材をブロンズ、時代を紀元前2000年から1000年として検索を行うと、以下のURLのページが表示されます。

    https://www.metmuseum.org/art/collection/search#!?perPage=20&searchField=All&sortBy=Relevance&offset=0&pageSize=0&material=Bronze&era=2000-1000%20B.C.

    これはURLに検索条件を加え、サーバに対して問い合わせを行なっていると考えられます。様々な検索条件の組み合わせを全て静的にページ構築するのは無理なので、データベースを構築し、それに対して問い合わせた結果の情報からHTMLを生成して表示しています。

    このような、Webシステムなどで情報を管理するしくみは「データベース」と呼ばれ、MySQL、PostgreSQL、SQLiteなどがあります。これらのデータベースはC#からでも接続できるため、Unityで利用することもできます。

    MySQL、PostgreSQL、SQLiteは全て「リレーショナルデータベース」と呼ばれる歴史のある形式のデータベースです。中でも、SQLiteはインストールも不要で最も軽量なため、Android端末では標準ライブラリとして使用されています。

    MySQLはWordPressなどのCMSのバックエンドで利用されています。このためWebの開発では馴染み深いものだと思います。

    メトロポリタン美術館のデータベースの一部はGitHub上にcsv形式で公開されているので、自分で検索システムに組み込むこともできます。

    ●The Metropolitan Museum of Art's Open Access Initiative
    https://github.com/metmuseum/openaccess

    ただ、このデータは大きすぎるので、以下のようなシンプルなものをSQLiteを使って検索できるようにしてみましょう。


    SQLiteを下記のサイトからダウンロードします。ここではPrecompiled Binaries for Windowsのsqlite-tools-win32-x86-3310100.zipを使用しますが、お使いの環境によって適したものを選んでください。

    ●SQLite Download Page
    https://www.sqlite.org/download.html


    zipファイルを展開して、以下のように実行します。

    .\sqlite3.exe test.db  
    

    起動したら、以下のようにテーブルとデータを登録します。

    1.	create table animals(id int primary key, category text, name text, size integer);  
    2.	insert into animals values(1, '哺乳類', 'アイアイ', 400);  
    3.	insert into animals values(2, '鳥類', 'アオサギ', 930);  
    4.	insert into animals values(3, '爬虫類', 'ムカシトカゲ', 600);  
    5.	insert into animals values(4, '両生類', 'ツチガエル', 40); 
    

    次に、SQLを使って問い合わせを行います。

    1.	-- 登録されている全ての情報を表示  
    2.	select * from animals;  
    3.	1|哺乳類|アイアイ|400  
    4.	2|鳥類|アオサギ|930  
    5.	3|爬虫類|ムカシトカゲ|600  
    6.	4|両生類|ツチガエル|40  
    
    1.	-- 爬虫類の名前を問い合わせる  
    2.	SELECT name FROM animals WHERE category='爬虫類';  
    3.	ムカシトカゲ  
    
    1.	-- 爬虫類もしくはサイズが40mmの生き物を問い合わせる  
    2.	SELECT name FROM animals WHERE category='爬虫類' or size=40;  
    3.	ムカシトカゲ  
    4.	ツチガエル  
    

    SQLはリレーショナルデータベースへの操作を定義するための言語で、問い合わせや登録など全て行うことができます。しかし、今回のような図鑑コンテンツでは前述したようにデータの更新がまず起こらないため、本格的なデータベースを別に用意するのは面倒です。もう少し手軽に使える方法がないか調べるうちに、「MasterMemory」が良さそうだと感じ、導入を行いました。

    次ページ:
    <4>MasterMemoryを使う

    [[SplitPage]]

    <4>MasterMemoryを使う

    MasterMemoryはCysharpが公開している、読み取り専用のインメモリデータベースです。ゲームでの利用を目的に設計されているため、検索が非常に高速になっているようです。

    ●GitHub - Cysharp/MasterMemory: Embedded Typed Readonly In-Memory Document Database for .NET Core and Unity.
    https://github.com/Cysharp/MasterMemory


    https://github.com/Cysharp/MasterMemory より

    これを使って前出のテーブルを検索できるようにしていきます。


    まず、Project Settingsを開き、「Api Compatibility Level*」を「.NET 4.x」に変更します。


    これをしないと、今後の作業で以下のようなエラーが発生します。

    FormatterNotRegisteredException:TagContents[] is not registered in this resolver.
    resolver:StandardResolver
    

    次に先ほどのGitHubページの導入手順に従って、MasterMemoryとMessagePackをダウンロードしてUnityに入れます。GeneratedフォルダとTablesフォルダを作成し、その中に以下のテーブル定義のスクリプトAnimal.csを作成します。

    ●DictSample/Assets/Tables/Animal.cs

    1.	using MasterMemory;  
    2.	using MessagePack;  
    3.	  
    4.	namespace CGWORLD  
    5.	{  
    6.	    [MemoryTable("animal"), MessagePackObject(true)]  
    7.	    public class Animal  
    8.	    {  
    9.	        [PrimaryKey]  
    10.	        public int Id { get; set; }  
    11.	        [SecondaryKey(0), NonUnique]  
    12.	        public string Classification { get; set; }  
    13.	        public string Name { get; set; }  
    14.	        public float Size { get; set; }  
    15.	  
    16.	        public Animal(int id, string classification, string name, float size) 
    17.	        {  
    18.	            Id = id;  
    19.	            Name = name;  
    20.	            Size = size;  
    21.	            Classification = classification;  
    22.	        }  
    23.	    }  
    24.	}  
    

    ここまで用意できたら、MasterMemoryのコードを生成するスクリプトを実行します。MasterMemoryのサイトには生成スクリプトをUnityのエディタに登録する方法が書いてありますが、ここでは以下のようなバッチで処理を行なっています。

    1.	@echo on  
    2.	  
    3.	set MMGEN="MasterMemory.Generator\win-x64\MasterMemory.Generator.exe"  
    4.	set MPC="MessagePackUniversalCodeGenerator\win-x64\mpc.exe"  
    5.	set NAME_SPACE=CGWORLD  
    6.	set SCRIPT_PATH="..\Assets"  
    7.	  
    8.	del /S /Q %SCRIPT_PATH%\Generated  
    9.	  
    10.	%MMGEN% -i %SCRIPT_PATH%\Tables -o %SCRIPT_PATH%\Generated -n %NAME_SPACE%  
    11.	%MPC% -i ..\Assembly-CSharp.csproj -o %SCRIPT_PATH%\Generated\MessagePack.Generated.cs  
    

    これでGeneratedフォルダの中に以下のようなコードが生成されます。


    ここで生成されたコードは自分で触ることはありません。このコードも前述のGitHubのサンプルコード置き場に上げてあります。

    以下はデータ登録部分です。11種類の動物の類、名前、サイズ(mm)を登録してあります。Sample2.unityは、爬虫類、哺乳類といった分類を選択することができ、大きさと名前でソートして表示することができるサンプルです。


    スクリプト部分はこれだけです。

    ●DictSample/Assets/Sample2/Sample2.cs

    1.	using System.Collections.Generic;  
    2.	using UnityEngine;  
    3.	using CGWORLD;  
    4.	using System.Linq;  
    5.	  
    6.	public class Sample2 : MonoBehaviour  
    7.	{  
    8.	    [SerializeField] UnityEngine.UI.Dropdown _classSelect;  
    9.	    [SerializeField] UnityEngine.UI.Dropdown _orderSelect;  
    10.	    [SerializeField] UnityEngine.UI.Button _search;  
    11.	    [SerializeField] UnityEngine.UI.Text _list;  
    12.	    void Start()  
    13.	    {  
    14.	        // 動物データを登録します  
    15.	        var databaseBuilder = new DatabaseBuilder();  
    16.	        databaseBuilder.Append(new List {  
    17.	                    new Animal(0, "哺乳類", "フクロギツネ", 500),  
    18.	                    new Animal(1, "哺乳類", "マレーグマ", 1200),  
    19.	                    new Animal(2, "哺乳類", "アイアイ", 400),  
    20.	                    new Animal(3, "鳥類", "アオサギ", 930),  
    21.	                    new Animal(4, "鳥類", "コノハズク", 200),  
    22.	                    new Animal(7, "爬虫類", "カミツキガメ", 300),  
    23.	                    new Animal(6, "爬虫類", "ムカシトカゲ", 600),  
    24.	                    new Animal(5, "鳥類", "セキセイインコ", 180),  
    25.	                    new Animal(8, "爬虫類", "キングコブラ", 2300),  
    26.	                    new Animal(9, "両生類", "ツチガエル", 40),  
    27.	                    new Animal(10, "両生類", "カメガエル", 50),  
    28.	                    new Animal(11, "両生類", "クロサンショウウオ", 150)});  
    29.	        var db = new MemoryDatabase(databaseBuilder.Build());  
    30.	  
    31.	        // 検索ボタンが押されたらDropdownメニューで選択されている条件の動物を表示する  
    32.	        _search.onClick.AddListener(() =>  
    33.	        {  
    34.	            var cls = db.AnimalTable.FindByClassification(_classSelect.options[_classSelect.value].text);  
    35.	            _list.text = string.Join("\n", (_orderSelect.value == 0) ?  
    36.	                cls.OrderBy(x => x.Name).Select(x => $"{x.Name} {x.Size}mm") :  
    37.	                cls.OrderBy(x => x.Size).Select(x => $"{x.Size}mm {x.Name}"));  
    38.	        });  
    39.	    }  
    40.	}  
    

    DatabaseBuilderは先ほどの自動生成の際に作られたクラスです。これに対してAnimalオブジェクトを追加し、データベースを構築します。データの問い合わせは34行目で行なっていて、以下と同じことです。

    1.	var cls = db.AnimalTable.FindByClassification("哺乳類");  
    

    FindByClassificationも自動的に生成されたメソッドです。テーブル定義のとき以下のように指定しました。

    1.	        [PrimaryKey]  
    2.	        public int Id { get; set; }  
    3.	        [SecondaryKey(0), NonUnique]  
    4.	        public string Classification { get; set; }  
    

    [PrimaryKey]と[SecondaryKey]属性を指定するとその変数が問い合わせ対象となり、FindBy変数名メソッドが生成されます。ひとつの分類(Classification変数)には動物が複数登録されるため、NonUniqueも指定します。

    [PrimaryKey]はそのデータを特定できる情報を指定します、今回の例ではなくても良いのですが一般的にあった方が便利なので追加しています。[SecondaryKey]は検索対象となる情報を指定します。[PrimaryKey]と[SecondaryKey]の組み合わせで、生成される問い合わせメソッドが変化します。

    くり返しになりますが、MasterMemoryを使う利点は、データベースプログラムを用意する必要がなく、Unityだけで完結できることだと思っています。もちろんMasterMemoryはたくさんのゲームで使われており、高速であるという利点がありますが、今回のような検索コンテンツの場合はリアルタイム性をシビアに求められません。

    MySQLなどのデータベースは高機能なのですが、展示の検索コンテンツは多数の同時アクセスやデータの更新が起こらないため、ほとんどが不要な機能です。別のプログラム(データベース)の連携はプログラム的にも複雑になり、PCの環境設定も面倒になります。

    MasterMemoryを使ってUnityで完結できれば、実行ファイル1つ渡せばどこでも動くため クライアントもチェックしやすく、トラブルを減らすこともできると思っています。

    <5>OR検索を使う

    SQLiteを紹介したときに、以下の条件で検索を行いました。

    1.	-- 爬虫類もしくはサイズが40mmの生き物を問い合わせる  
    2.	SELECT name FROM animals WHERE category='爬虫類' or size=40;  
    

    この「もしくは」が「OR検索」と呼ばれるものです。MasterMemoryで生成される問い合わせメソッドにはORで検索できるものはありません。

    そもそも複雑な問い合わせに対応するように設計されてはいないので、アプリケーション側で対応します。いくつか方法はあると思いますが、最もシンプルと思われる方法で処理してみました。Sample3.csがその実装です。まずデータベースに分類だけを登録した以下のようなテーブルを作成します。

    登録コードはこうなります。

    1.	databaseBuilder.Append((new List {  
    2.	    "哺乳類", "爬虫類", "鳥類", "両生類"  
    3.	}).Select((name, index) => new Classification(index, name)));  
    

    シーン中では4つのToggleコンポーネントがそれぞれ検索項目に対応するように並べてあります。


    下のコードでToggleがONになっているものだけ抜き出し、FindByClassificationメソッドで検索をかけています。

    1.	var res = Enumerable.Range(0, 4)
    2.	    .Where(x => _condition.transform.GetChild(x).GetComponent<Toggle>().isOn)  
    3.	    .SelectMany(x => db.Animal2Table.FindByClassification(x)); 
    


    コードがシンプルなのはLINQの恩恵も大きいのですが、データの登録、検索をとてもわかりやすく書くことができます。特別に他のデータベースを用意することなく実装できるので閲覧だけのコンテンツ制作には非常に向いていると思います。

    また、データベースの内容は以下のようにMessagePackとして保存することもできます。

    1.	using (var fs = new FileStream("test.mpac", FileMode.OpenOrCreate, FileAccess.Write))
    2.	{
    3.	    databaseBuilder.WriteToStream(fs);
    4.	}
    

    MessagePackはJSONと同じ感覚で使え、ファイル容量を大幅に小さくすることができます。コンテンツ実行時にセンサで収集したデータの保存用途としても有用だと思います。

    今回紹介した図鑑コンテンツの実装例はファイル構造を反映するもの、MasterMemoryを利用するもの、それとOR検索を行うものでした。これらの例は簡単なものですが、ここから、検索の方法を工夫したり、見やすくUIをつくり込んだりしていけば十分使えるものになると思います。

    またこのMessagePackとMasterMemoryを使った方法は図鑑コンテンツだけではなくデータビジュアライゼーションなどにも応用できると思いますので、ぜひご自分でいろいろ調べてみてください。

    Profile.

    高田稔則/Toshinori Takata(Codelight)
    Codelight株式会社 代表取締役・インタラクションエンジニア
    フリーランス、株式会社TBSテレビ等で映画CG制作、株式会社ソニー・コンピュータエンタテインメント(現 ソニー・インタラクティブエンタテインメント)でPS4のOSD開発などを経て2006年にCodelight株式会社を設立。インタラクティブコンテンツの制作を中核として、製造業向けのプロトタイプ開発なども行う
    www.codelight.co.jp