はじめに
今回はScripted Importer
について紹介したいと思います。
Scripted Importer は Unity スクリプティング API の一部です。Scripted Importer を使用すると C# でカスタムアセットインポーターを作成できます。これにより、Unity でネイティブにサポートされていないファイル形式の独自のサポートを加えることができます。
やり方
以下の2つを満たしたクラスを作成します。
- 抽象クラス
ScriptedImporter
の継承 ScriptedImporter
属性をつける
// 「.sample」という拡張子に対応,バージョンは「1」 [ScriptedImporter(version: 1, ext: "sample")] public class SampleImporter : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) { // 登録された拡張子に一致するファイルがAssetPipelineによって検知された時に呼ばれる } }
OnImportAsset
の引数にあるAssetImportContext
はインポートの入出力情報が含まれます。
Defines the import context for scripted importers during an import event.
This class carries both input and output information for the OnImportAsset() task.
// Google翻訳
インポート イベント中のスクリプト化されたインポーターのインポート コンテキストを定義します。このクラスは、OnImportAsset() タスクの入力情報と出力情報の両方を運びます。
AssetImporters.AssetImportContext - Unity スクリプトリファレンス
よく使いそうなのは以下あたりでしょうか。
名前 | 意味 |
---|---|
ctx.assetPath | インポートされたファイルのパス |
ctx.AddObjectToAsset | インポート操作の結果にオブジェクトを追加する |
ctx.SetMainObject | Main Object (Main Asset )を選択する |
ctx.DependsOnSourceAsset | 依存先のアセットを指定する |
ctx.DependsOnCustomDependency | 依存先のアセットを指定する(DependsOnSourceAsset との挙動の違いは未調査) |
ctx.selectedBuildTarget | どのプラットフォームを対象にしているか |
ctx.LogImportWarning | 警告をログに出力する |
ctx.LogImportError | エラーメッセージをログに出力する |
MainAssetとSubAsset
エディタ拡張をされている方なら馴染みがあるかもしれませんが、UnityのAsset
はMainAsset
とSubAsset
に分かれます。
Unity - Scripting API: AssetDatabase.IsMainAsset
Unity - Scripting API: AssetDatabase.IsSubAsset
公式ドキュメントに分かりやすいような記載があまり見つけられませんでしたが、Main assets and sub-assets
という説明がありました。(調べた感じ英語のみ存在するページ?)
Because Unity can store multiple serialized objects within the same asset file, Unity has a concept of the main asset within any asset file. When Unity creates asset files that contain a single asset, such as a material, the main asset is always that single asset. For other types containing more than one serialized asset object, the main asset is always the first asset added to the file, unless otherwise specified with the SetMainObject method.
You can sometimes see sub-assets in the Project window of the Editor, if those sub-assets are of certain types. For example, looking at this FBX asset file containing a “Space Frigate” model in the Project window, its view has been expanded to reveal that it has a material and a mesh
as sub-assets.
// Google翻訳
Unity は複数のシリアル化されたオブジェクトを同じアセット ファイル内に格納できるため、Unity には、任意のアセット ファイル内のメイン アセットの概念があります。 Unity がマテリアルなどの単一のアセットを含むアセット ファイルを作成する場合、メインのアセットは常にその単一のアセットです。複数のシリアル化されたアセット オブジェクトを含む他のタイプの場合、SetMainObject メソッドで特に指定しない限り、メイン アセットは常にファイルに追加される最初のアセットです。サブアセットが特定のタイプである場合、エディタのプロジェクト ウィンドウにサブアセットが表示されることがあります。たとえば、プロジェクト ウィンドウで「Space Frigate」モデルを含むこの FBX アセット ファイルを見ると、ビューが展開されて、マテリアルとメッシュがあることがわかります
サブアセットとして。
Unity - Manual: Customizing the Asset Database workflow
public override void OnImportAsset(AssetImportContext ctx) { // 登録された拡張子に一致するファイルがAssetPipelineによって検知された時に呼ばれる var text = File.ReadAllText(ctx.assetPath); var textAsset = new TextAsset(text); // ↓以下MainAssetとSubAssetの定義 // 最初にctx.AddObjectToAssetしたオブジェクトがMainAssetとして登録される(ctx.SetMainObjectでMainAssetを順番関係なく指定できる) ctx.AddObjectToAsset(identifier: "MainAsset", obj: textAsset); ctx.AddObjectToAsset("SubAsset1", new TextAsset("SubAsset1")); ctx.AddObjectToAsset("SubAsset2", new TextAsset("SubAsset2")); ctx.AddObjectToAsset("SubAsset3", new TextAsset("SubAsset3")); }
MainAsset
の下にSubAsset
の一覧がProject
ビューに表示されます。

このコードは意味をなしていませんが、例えばPrefab
のMaterial
をSubAsset
として表示するようにするなんてことに利用できます。

[ScriptedImporter(1, "cube")] public class CubeImporter : ScriptedImporter { public float m_Scale = 1; public override void OnImportAsset(AssetImportContext ctx) { var cube = GameObject.CreatePrimitive(PrimitiveType.Cube); var position = JsonUtility.FromJson<Vector3>(File.ReadAllText(ctx.assetPath)); cube.transform.position = position; cube.transform.localScale = new Vector3(m_Scale, m_Scale, m_Scale); // 'cube' は ゲームオブジェクトで、自動的にプレハブに転換されます // ( 'Main Asset' だけがプレハブになります) ctx.AddObjectToAsset("main obj", cube); ctx.SetMainObject(cube); var material = new Material(Shader.Find("Standard")); material.color = Color.red; // NOTE : 公式ドキュメントにこれを追加しました cube.GetComponent<Renderer>().material = material; // アセットには、インポート内で一貫した固有のID文字列を割り当てられる必要があります ctx.AddObjectToAsset("my Material", material); // インポート出力としてコンテキストに渡されないアセットは破棄する必要があります var tempMesh = new Mesh(); DestroyImmediate(tempMesh); } }
ScriptedImporter - Unity マニュアル
こちらは公式ドキュメントのコードを一部改変した例です。具体的に変更した箇所は以下の通り。
// NOTE : 公式ドキュメントにこれを追加しました cube.GetComponent<Renderer>().material = material;
TextAssetの暗号化1(これは間違った手法)
こちらの記事を参考にさせてもらいながら、TextAsset
を暗号化してみます。
Unity製アプリにおいてアセットを暗号化する手法 | QualiArtsエンジニアブログ
[ScriptedImporter(version: 1, ext: "sample")] public class SampleImporter : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) { using var stream = File.OpenRead(ctx.assetPath); var textAsset = SampleConverter.Read(stream); ctx.AddObjectToAsset(identifier: "MainAsset", obj: textAsset); } } public static class SampleConverter { public static TextAsset Read(Stream input) { if (input == null) throw new ArgumentNullException(nameof(input)); var length = (int)input.Length; var target = length <= 1024 ? stackalloc byte[length] : new byte[length]; input.Read(target); input.Dispose(); Decrypt(target); return new TextAsset(Encoding.UTF8.GetString(target)); } public static void Write(Stream output, TextAsset textAsset) => Write(output, Encoding.UTF8.GetBytes(textAsset.text).AsSpan()); public static void Write(Stream output, string text) => Write(output, Encoding.UTF8.GetBytes(text).AsSpan()); public static void Write(Stream output, Span<byte> bytes) { if (output == null) throw new ArgumentNullException(nameof(output)); if (bytes == null) throw new ArgumentNullException(nameof(bytes)); Encrypt(bytes); output.Write(bytes); output.Dispose(); } private static void Encrypt(Span<byte> value) => Reverse(value); private static void Decrypt(Span<byte> value) => Reverse(value); private static void Reverse(Span<byte> value) => value.Reverse(); }
XOR
だったりAES
だったりと暗号化の仕方はありますが、ひとまず一番簡単そうなバイナリを逆順にする操作をしてみます。
ちなみに.sample
を作成するエディタ拡張も作っておきました。
namespace Editor { public class SampleConverterWindow : EditorWindow { private string _path; [MenuItem("Tools/SampleConverter")] private static void Init() { var instance = GetWindow<SampleConverterWindow>(); instance._path = Application.dataPath; instance.minSize = new Vector2(700, 80); } private void OnGUI() { EditorGUILayout.LabelField("Select File", _path); if (GUILayout.Button("Select File")) { var selectedPathName = EditorUtility.OpenFilePanel( "Select File", "", ""); if (!string.IsNullOrEmpty(selectedPathName)) _path = selectedPathName; } if (GUILayout.Button("Convert .sample")) { if (!File.Exists(_path)) return; var target = File.ReadAllBytes(_path); var outputPath = Path.ChangeExtension(_path, ".sample"); using var writer = File.OpenWrite(outputPath); SampleConverter.Write(writer, target); AssetDatabase.Refresh(); } } } }
この場合test.txt
とtest.sample
の中身・meta
は以下のようになります。


.sample
の中身はしっかりと暗号化されていますが、Unityエディタ上では同じようにTextAsset
として利用することができます。


Asset
暗号化の目的はユーザーにファイルの中身を見られないことです。典型的なやり方としてAsset Studio
が挙げられるので、それの対策ができているか確認してみます。
【Unity】AssetStudioでUnity製のゲーム・AssetBundleの中身を覗き見・エクスポートする(悪用はしないように) - はなちるのマイノート

普通に見えてますね・・・・・・。
勝手にFile
がビルドに同梱されているのかと勘違いしていましたが、変換後のTextAsset
が同梱されるのですね。
確かにScriptedImporter
がAssembly-CSharp-Editor.dll
に含まれるので当然ちゃ当然か...。
TextAssetの暗号化2(これは正しい手法)

namespace Editor { [ScriptedImporter(1, "sample2")] public class Sample2Importer : ScriptedImporter { public override void OnImportAsset(AssetImportContext ctx) { // 登録された拡張子に一致するファイルがAssetPipelineによって検知された時に呼ばれる using var stream = File.OpenRead(ctx.assetPath); var textAsset = SampleConverter.Read(stream); // MainAssetとしてバイナリ逆順にしたTextAssetを登録する ctx.AddObjectToAsset("MainAsset", textAsset); } } }

このTextAsset
を利用するときは復号化の処理を挟んでからでなければいけません。
例えばインスペクターからTextAsset
を参照してそのままテキストとして出力とかしたら、NGです。(複数人で行う場合は要注意)
public class Test : MonoBehaviour { [SerializeField] private Text textObj; [SerializeField] private TextAsset textAsset; private void Start() { // NG : textAsset.textを復号化してからでないと暗号化されたままのテキストが表示されてしまう。 textObj.text = textAsset.text; } }
さいごに
ファイル自体がアプリに含まれるなら結構いいのかなと思ったのですが、正直予想外の使い方がされる確率が高そうであんまり実用的ではないような気がします。
画像データや音楽データなんかもScriptedImporter
で暗号化できたらなとか密かに思っていたのですが、AssetBundle
にして暗号化が一番無難そうですかね。
何か他に良い手法がないかとかも調べてみれたらと思います。