【Unity】Unity非対応の拡張子のファイルをアセットとして取り扱えるScripted Importerの使い方

Unity非対応の拡張子のファイルをアセットとして取り扱えるScripted Importerの使い方をまとめました。

Unity2020.2

Scripted Importerとは

Unityが対応していない拡張子のファイルをUnityにインポートすると、
以下のように意味を持たないDefaultAssetとなります。

f:id:halya_11:20210218092714p:plain
DefaultAsset

ScriptedImporterを使うと、Unityが対応していない拡張子のファイルがインポートされたときに独自の処理を書いてアセットを作成することができます。

なおScripted Importerは長らくExperimentalでしたが、マニュアルを見る限りUnity2020.2でExperimentalではなくなっています。

docs.unity3d.com

簡単なサンプル(公式サンプル)

ScriptedImporterの挙動を理解するためにまずは簡単なサンプルを動かしてみます。
以下はマニュアルにある公式のサンプルを少し変更してコメントを加えたものです。

using UnityEngine;
using System.IO;
using UnityEditor.AssetImporters;

[ScriptedImporter(1, "cube")]
public class ExampleImporter : ScriptedImporter
{
    // インポーターのInspectorから設定できる値
    [SerializeField]
    private float _scale = 1;

    public override void OnImportAsset(AssetImportContext ctx)
    {
        // ファイルから文字列を読み込んで座標に変換
        var position = JsonUtility.FromJson<Vector3>(File.ReadAllText(ctx.assetPath));

        // GameObjectを生成
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.transform.position = position;
        cube.transform.localScale = new Vector3(_scale, _scale, _scale);

        // Materialを生成
        var material = new Material(Shader.Find("Standard")) {color = Color.red};
        cube.GetComponent<MeshRenderer>().material = material;
        
        // GameObjectをメインアセットとして追加する
        // かならずAssetImportContextを通して追加すること(AssetDatabaseのAPIを叩かない)
        ctx.AddObjectToAsset("Main", cube);
        ctx.SetMainObject(cube);
        
        // Materialをサブアセットとして追加する
        ctx.AddObjectToAsset("Material", material);
    }
}

このインポーターは以下の処理を行います。

  • 拡張子が「.cube」のアセットに対して処理を行う
  • ファイルに記述されたJsonをVector3に変換し、それを座標として設定したPrefabを生成
  • MeshにはCubeを設定し、スタンダードシェーダをアサインしたマテリアルを設定
  • スケールはInspectorから設定できるように

これを使用するためには以下のような内容のファイルを.cube拡張子で作成します。

{"x":1,"y":1,"z":1}

このファイルをUnityにインポートすると以下のようなアセットが出来上がります。

f:id:halya_11:20210218094958p:plain
インポート結果

このアセットは普通のPrefabと同じように取り扱うことができます。

サブアセットを差し替えるためのリマップ機能

リマップ機能を使うと、インポートしたアセットのサブアセットを、同じ型の外部のアセットにリマッピングできます。
FBX Importerのマテリアルを上書きする設定のようなことが実現できます。

マッピングするには以下のようにAsseImporter.AddRemap()を使用します。

var importer = AssetImporter.GetAtPath(assetPath); 

// Remapを追加する
importer.AddRemap(new AssetImporter.SourceAssetIdentifier(typeof(Material), materialName), externalObject);

// インポートしなおす
AssetDatabase.WriteImportSettingsIfDirty(assetPath);
AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceUpdate);

さらにインポート処理をする側でもリマップされたオブジェクトを取得して明示的に処理する必要があります。
AddRemapされたオブジェクトの情報はGetExternalObjectMap()で取得できます。

[ScriptedImporter(1, "cube")]
public class ExampleImporter : ScriptedImporter
{
    public override void OnImportAsset(AssetImportContext ctx)
    {
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        ctx.AddObjectToAsset("Main", cube);
        ctx.SetMainObject(cube);
        var material = new Material(Shader.Find("Standard")) {color = Color.red};
        ctx.AddObjectToAsset("Material", material);
        
        // 対応するExternalObject(リマップされたオブジェクト)があったらそっちを使う
        if(GetExternalObjectMap().TryGetValue(new SourceAssetIdentifier(typeof(Material), "Material"), out var map)) 
        {
            material = (Material)map;
        }
        
        cube.GetComponent<MeshRenderer>().material = material;
    }
}

他のアセットが変更されたら自身をリインポートする(依存関係を設定する)

Scripted Importerでインポートしたアセットには他のアセットとの依存関係を設定できます。
以下ではこの方法についてまとめます。

他のAssetへの依存を設定する

他のアセットへの依存を設定するにはAssetImportContext.DependsOnSourceAsset()を使います。
以下のように設定すれば、otherAssetPathのアセットが更新されたときに自身がリインポートされます。
複数登録することも可能です。

ctx.DependsOnSourceAsset(otherAssetPath);

またAssetImportContext.DependsOnArtifact()というメソッドもあります。

docs.unity3d.com

独自の依存関係を定義する

また、AssetImportContext.DependsOnCustomDependency()を使用すると独自に依存関係を定義できます。
以下のように任意のタイミングで依存しているすべてのアセットをリインポートできます。

using UnityEngine;
using UnityEditor;
using UnityEditor.AssetImporters;

[ScriptedImporter(1, "cube")]
public class ExampleImporter : ScriptedImporter
{
    private const string CustomDependencyName = "CustomDependencyName";
    
    [MenuItem("Example/UpdateDependency")]
    public static void UpdateDependency()
    {
        // 第二引数のハッシュが更新されたら第一引数の名前に依存しているアセットがすべてリインポートされる
        // アセットインポート中にこのメソッドを呼ぶことはできない
        AssetDatabase.RegisterCustomDependency(CustomDependencyName, Hash128.Compute(Random.value));
        
        AssetDatabase.Refresh();
    }

    public override void OnImportAsset(AssetImportContext ctx)
    {
        // CustomDependencyを登録
        ctx.DependsOnCustomDependency(CustomDependencyName);

        // (略)
    }
}

インスペクタ拡張方法

インスペクタを拡張するにはScriptedImporterEditorを継承したクラスを作成します。

using UnityEditor;
using UnityEditor.AssetImporters;
using UnityEngine;

[CustomEditor(typeof(ExampleImporter))]
public class ExampleImporterEditor: ScriptedImporterEditor
{
    public override void OnInspectorGUI()
    {
        // プロパティを描画(serializedObject.ApplyModifiedProperties()はやらない)
        var scaleProp = serializedObject.FindProperty("_scale");
        EditorGUILayout.PropertyField(scaleProp, new GUIContent("スケール"));
        
        // Revert, Applyボタンを描画
        ApplyRevertGUI();
    }
}

プラットフォーム毎にインポート処理を分ける

プラットフォーム毎にインポート処理を分けるにはAssetImportContext.selectedBuildTargetを使います。

if (ctx.selectedBuildTarget == BuildTarget.Android)
{
    // Androidだけの処理
}

インポートエラー・警告

エラーログを出力するにはAssetImportContext.LogImportError()を使います。
警告にはAssetImportContext.LogImportWarning()を使います。

ctx.LogImportWarning("警告");
ctx.LogImportError("エラー");

参考

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com