投稿記事

Unityの記事 (7)

水無川旅館 2024/07/23 12:00

【Unity】宴4で暗号化した AssetBundle からリソースを読み込む方法

 こんにちは! 昨晩は宴のリソースを暗号化するということに取り組んでいました。その方法について軽く書いてみたいと思います!

TL;DR

 CustomLoadManager を使いましょう。

using Utage;

private void Awake() {
    AssetFileManager.GetCustomLoadManager().OnFindAsset += OnFindAsset;
}

private void OnFindAsset(AssetFileManager mangager, AssetFileInfo fileInfo, IAssetFileSettingData settingData, ref AssetFileBase asset) {
    // AssetBundle.LoadAsset で読み込む処理
    asset = null;
}

参考リンク

もうちょっと詳しく知りたい人のために

~前提知識~

 Unity は、基本的にエンジン側で暗号化処理はしてくれません。

 圧縮とかはしてくれますが、暗号化はプログラマに委ねられている設計です。

 で、そうなるとライブラリとしても「汎用的な暗号化処理」というのは提供しにくく、宴というライブラリでも暗号化処理は積極的にはしてくれない感じになっています。

暗号化してくれないとなにが困る?

 そういうわけで、基本的に Unity のアセットというのは「丸見え」の状態です。

 普通のプレイヤーが簡単に覗けるわけではありませんが、ツールを使えば簡単に見れるようになっています(※リバースエンジニアリングほどの高度な知識は必要ありません)

 しかし有料アセット等では「プレイヤーが容易には取り出せないこと」を規約として求めているケースも多く、これでは困ってしまいます。

 そういうわけで AssetBundle にアセットをまとめて、それを暗号化するという手法があります(Addressables 等もありますが、今回は割愛)

宴特有の難しさ

 宴はリソースを動的にロードする仕組みがあるので、これに手を加えなければならず、そこそこ大変です。

 宴にはリソースをサーバや AssetBundle から読み込む仕組みもあるのですが(参考リンク)、暗号化はしてくれません。

 そこで暗号化した AssetBundle を読み込む処理を自前で用意することになります。こうった際に便利なのが CustomLoadManager です。

まず AdvEngineStarter の設定は Local にして、 AssetBundle を StreamingAssets に配置する

 ちょっとテクいのですが

  • AdvEngineStartar の設定は StreamingsAssets ではなく Local にする
    • .scenarios.asset をローカルから読み込むため。未検証ですが Streaming Assets And Local Scenario 等でもいい気がします。
  • AssetBundle を StreamingsAssets に配置する

AssetBundle に含めるもの

  • <プロジェクト名>/Scenarios/<プロジェクト名>.scenarios.asset
  • Resources/<プロジェクト名>/ の以下全部
    • ※Resources/ は MyResources/ 等に名前を変える

作成した AssetBundle を暗号化

 作成した AssetBundle を暗号化します。これについては今回の記事では割愛します。

 AssetBundle 暗号化については以前の記事で触れていますので、よかったらどうぞ!

CustomLoadManager 解説

 ここまでできたら CustomLoadManager を使い、復号&ロード処理をします。

 冒頭でも書きましたが、 CustomLoadManager は以下のように使います。


using Utage;

private void Awake() {
    AssetFileManager.GetCustomLoadManager().OnFindAsset += OnFindAsset;
}

private void OnFindAsset(AssetFileManager mangager, AssetFileInfo fileInfo, IAssetFileSettingData settingData, ref AssetFileBase asset) {
    // AssetBundle.LoadAsset で読み込む処理
    asset = null;
}

 asset = null; としている部分で読み込んだアセットを設定してあげれば OK です。

AssetFileBase を継承したクラスをつくる

 これで完璧! なんて……そうは問屋が卸しません。

 AssetFileBase というクラスのサブクラスを代入しないといけないわけです。

 これがそこそこ大変です。ただこの部分は汎用的に書けたので、せっかくなのでフォロワー限定公開の特典コードにしてみます!

 なお、著作権は放棄していません。MPL 2.0 というライセンスで提供します。使用する場合は、クレジット表記・ライセンス表記はお願い致します。

 また無保証です(OSSでよくある条件です)。このコードを使うことで起きたいかなる事態にも責任は負いかねますので、ご了承ください。

特典コードの利用規約

 特典コードは Mozilla Public License, version 2.0 (MPL 2.0) で提供されています。 MPL 2.0 の日本語参考訳もあります。

フォロワー以上限定無料

AssetBundle 用 AssetFileBase のサブクラス

無料

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

水無川旅館 2024/07/18 05:40

【Unity】UniTaskで重い処理を並行で走らせたいならRunOnThreadPoolを使おう!

 前回の記事で、ひとつ書こうか悩んだことがあります。それは……、「重い処理を並行で走らせたい場合」のことです。

まぁまずは次のコードを見てくれ。こいつをどう思う?

 実行順序のクイズです。以下のコードは、どういう順序で実行されるでしょーか!?
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

using System;
using UnityEngine;
using Cysharp.Threading.Tasks;

public class TestScript : MonoBehaviour {
    async void Start() {
        Debug.Log("Start");
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        await UniTask.WhenAll(TestA(), TestB());
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        Debug.Log("End");
    }

    async UniTask TestA() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test A/{i}");
        }
    }

    async UniTask TestB() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test B/{i}");
        }
    }
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 …………答えは……………………

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

 Start → Test A/0 ~ Test A/65535 → Test B/0 ~ Test B/65535 → End です。

 TestA() と TestB() が同時に実行されると思ったあなた。不正解です。

キエエエエ! できてへんやんけー基本的なことがーーーー!!!

 前回の記事を書いたあと、このことについて書こうか悩んでいました。

https://twitter.com/bydriv/status/1813622351290081570

https://twitter.com/bydriv/status/1813624113950240968

 ↑IOに限らず「重い処理」だとこうなります。

 このことはちょっと専門的で、説明するのも難しいし、まぁ普通にコード書く分には困らないだろう……。と思っていたりもしました。

 が、ちょっと前回の記事を書いた後気になって眠れなくなってしまったため、筆を執った次第です。

とりあえず解決策 その① ループ処理なら UniTask.Yield を使う

using System;
using UnityEngine;
using Cysharp.Threading.Tasks;

public class TestScript : MonoBehaviour {
    async void Start() {
        Debug.Log("Start");
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        await UniTask.WhenAll(TestA(), TestB());
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        Debug.Log("End");
    }

    async UniTask TestA() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test A/{i}");
            await UniTask.Yield(PlayerLoopTiming.Update);
        }
    }

    async UniTask TestB() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test B/{i}");
            await UniTask.Yield(PlayerLoopTiming.Update);
        }
    }
}

とりあえず解決策 その② IOなら非同期IOを使う

 たとえばファイル入出力で、同期IOを使ってしまうと上記のような挙動になります。たとえば File.ReadAllBytes とかですね。

 多くのライブラリで非同期IO(ほにゃにゃらAsync)という関数が提供されているはずですので、それを使いましょう。 File.ReadAllBytesAsync とかがそれです。

とりあえず解決策 その③ どうしてもだめなら UniTask.RunOnThreadPool を使う

 タイトルでは「使おう!」と言っている割に「どうしてもだめなら」というのはズコーーーーという感じですが、まぁ基本的には最終手段だと思ってください。

 この関数は文字通りスレッドを立てます。

 ただ、スレッドなので立てすぎると変な挙動になる可能性があります。なので、最終手段、ということです。

using System;
using UnityEngine;
using Cysharp.Threading.Tasks;

public class TestScript : MonoBehaviour {
    async void Start() {
        Debug.Log("Start");
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        await UniTask.WhenAll(UniTask.RunOnThreadPool(TestA), UniTask.RunOnThreadPool(TestB));
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        Debug.Log("End");
    }

    async UniTask TestA() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test A/{i}");
        }
    }

    async UniTask TestB() {
        for (var i = 0; i < 65536; ++i) {
            Debug.Log($"Test B/{i}");
        }
    }
}

非同期って結局なんなのぜ? スレッドとは違うの?

 非同期というのは、基本的には「メインスレッド上で疑似的に並行処理をする仕組み」であり、スレッド自体はひとつなのです。

 この仕組みを実現するものを 非同期ランタイム と呼びます。

 前回の記事で、

「画像の1/N枚目を読み込む→プログレスバー更新処理→画像の2/N枚目を読み込む→プログレスバー更新処理→……→画像のN/N枚目を読み込む→完了処理」というように交互に進めるなど、工夫して実装する

 という実装パターンを紹介しましたが、これを高度に抽象化した機構だと思うとよいでしょう。

 await が呼ばれてから、次に await が呼ばれるまで をひとつの実行の単位として 同期的に 実行し、それが終わったら、次の await の処理に移る……。という感じに、次々とメインスレッドで処理を切り替えるようになっているわけです。

 そのため await が一切呼ばれない、極度に重い処理をする関数 は、完全にひとつの実行の単位として実行されてしまい、 メインスレッドが止まってしまう のです。

 そのため、ライブラリ側で ほにゃにゃらAsync という、非同期バージョンの関数が提供されているわけです。

それでもどうしても必要な場合もあるため、これは RunOnThreadPool で、スレッドで実行する

 基本的には重い処理は await をはさんでするのが基本。それがわかったところで、それでもなお、どうしても await なしで重い処理をする必要もあると思います。

 そういう場合は RunOnThreadPool を使う! ここまで覚えて帰ってくださいね。

 今日はここまでです。よかったらいいねしてね♥ おつかれさまでした!

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

水無川旅館 2024/07/18 02:04

【Unity】超便利! UniTask を使おう!

 こんばんは! 今日は内部的なソースコードの改善をしており、せっかくなのでそのことに関連した技術記事を書こうと思います。

 今回のお題は UniTask という非同期ライブラリ です!

インストール

 まぁなにはともあれインストールしましょう。 リリース一覧から最新の .unitypackage ファイルを探してダウンロードし、いつものようにインストールしてください。

そもそも非同期って?

 非同期自体はありふれた概念なんですが、知らない方も多いと思うので、軽く説明します。

 たとえば「ローディング画面」を考えます。ローディング画面ですることは主にふたつ。

  • 画面にプログレスバーを表示する。
  • バックグラウンドで、画像や音楽を読み込むなどの処理をする。

 ということです。

 重要なのは、このふたつを同時に実行しなければならない、ということです。

 多くのプログラミング言語は、一度にひとつの処理しかできません。ですから、それなりに高度なプログラミングをする必要があります。

 こういったものを実装する方法としては、いくつか種類があるのですが、

  • 「画像の1/N枚目を読み込む→プログレスバー更新処理→画像の2/N枚目を読み込む→プログレスバー更新処理→……→画像のN/N枚目を読み込む→完了処理」というように交互に進めるなど、工夫して実装する
  • スレッド と呼ばれる並列・並行処理の機構を使う
  • Unity なら、コルーチン(StartCoroutine)を使う

 などの方法があります。

 非同期は、技術的にはもっと詳細な内容はありますが、基本的にはこういった方法のひとつ、という捉え方をしておけばよいでしょう。

一番簡単な UniTask の使い方

 一番簡単な、と言いつつ結構複雑なんですが(

 とりあえず、以下のコードを見てください。このコードを実行すると、 Start → Test A/1 → Test B/1 → Test A/2 → Test B/2 → Test A/3 → Test B/3 → End と、 0.5 秒ごとに表示されます。

using System;
using UnityEngine;
using Cysharp.Threading.Tasks;

public class TestScript : MonoBehaviour {
    async void Start() {
        Debug.Log("Start");
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        await UniTask.WhenAll(TestA(), TestB());
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        Debug.Log("End");
    }

    async UniTask TestA() {
        Debug.Log("Test A/1");

        await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
        Debug.Log("Test A/2");

        await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
        Debug.Log("Test A/3");
    }

    async UniTask TestB() {
        await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
        Debug.Log("Test B/1");

        await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
        Debug.Log("Test B/2");

        await UniTask.Delay(TimeSpan.FromSeconds(1.0f));
        Debug.Log("Test B/3");
    }
}

今回使った機能の説明!

 上のコードで、以下の5つの要素を使っています。順に説明します。

  • async void
  • async UniTask
  • await
  • UniTask.Delay
  • UniTask.WhenAll

async void

 これは少し特殊な機能で、 Start, Awake, Update 等の Unity のイベントを非同期化するために使います(※)。

(※正確にはもっと汎用的な機能ですが、説明がややこしいので、この記事ではこう覚えてください。基本的に、非同期関数を定義する場合は後述の async UniTask を使ってください。 async void についてもっと詳しく知りたければ、多くの人が記事を書いてくださっているので、読まれるとよいでしょう)

async UniTask

 非同期関数を定義する構文です。戻り値がない場合は async UniTask 、ある場合は async UniTask<int> のように使います。

async UniTask F() {}
async UniTask<int> G() { return 0; }

await

 非同期関数の実行の終了を待つために使います。

 たとえば、

await UniTask.WhenAll(TestA(), TestB());

 というのは、 UniTask.WhenAll(TestA(), TestB()) の実行の終了を待つ、という意味です。


await TestA();
await TestB();

 とすれば、 TestA の実行がすべて終わってから TestB を実行するという意味になり、実行順は Start → Test A/1 → Test A/2 → Test A/3 → Test B/1 → Test B/2 → Test B/3 → End となります。

 TestA() と TestB() が同時に実行されてほしいのに、上記のように書いてしまうというのは本当にありがちなミスですので、気をつけましょう。 TestA() と TestB() を同時に実行するには、 WhenAll を使う必要がある、と覚えて帰ってくださいね。

await を書かなくても動くが…

 なお、 await が「待つ」動作をするので、書かなければ待たないことになります。即ち以下のように書くと WhenAll を使った場合と 似た 動作になりますが…

TestA();
TestB();

 これはあまりよくありません。実行順としては Start → Test A/1 → Test B/1 → End → Test A/2 → Test B/2 → Test A/3 → Test B/3 となり、 End が先に実行されてしまいます。

 じゃあ End がなかったらいいの? ……というと、いいっちゃいいと思うのですが、個人的にはあまりお勧めしません。少なくとも、このコードの意味をよく理解してから書いたほうがよいでしょう。

 非同期関数の呼び出し時には、原則として await をつける、と覚えたほうがいいと思います。

Delay

 指定の時間後に終了する非同期関数を生成します。 TimeSpan.FromSeconds(秒数) とセットで覚えてください。

// 0.5 秒待つ
await UniTask.Delay(TimeSpan.FromSeconds(0.5f));

 Delay 自体が待ってくれるわけではありません。あくまで「待つ」処理をするのは await のほうであり、 Delay 自体は指定の時間後に終了する非同期関数であるというだけです。

 ですので

UniTask.Delay(TimeSpan.FromSeconds(0.5f));

 と、 await を書かないと待ってくれませんので、注意しましょう。

 また、このことを利用して、

await UniTask.WhenAll(F(), TimeSpan.FromSeconds(0.5f));

 というように、 F の実行をしつつ、最低でも 0.5 秒は待つ、という処理をするというテクニックもあります。

WhenAll

 WhenAll は、ふたつの非同期関数を同時に実行し、その両方が終了するのを待つ関数です。

 非同期処理をする上で非常に大事な関数です! 頻出します。

 WhenAll で各関数の戻り値を使いたい場合には

var (x, y) = await UniTask.WhenAll(F(), G());

 と書きます(※両方とも UniTask<int> のように戻り値を持つ必要があります)。

~他にも色々~

 実践的には、もっといろいろ機能が使いたい場面も多いと思います。

UniTask.WaitUntil

WaitUntil は極めて実用的な関数で UniTask 非対応な様々なアセットを UniTask 化するときに頻出します。

 たとえば というアセットには IsWaitBootLoading という、エンジン自体の初期化が終了したかどうかを判定できるフラグがあるのですが、これを UniTask で待つ場合には以下のように書くことができます。

await UniTask.WaitUntil(() => !advEngine.IsWaitBootLoading);

ToCoroutine()

 実際には、 Unity のコルーチンと相互運用したい場合があるというのが現実であり、そういう場合に UniTask をコルーチン化することができます。

IEnumerator.ToUniTask

 逆に Unity のコルーチンを UniTask 化したいという場合には、 ToUniTask を使います。

UniTask.Yield

 これは少し難しいですが、 Unity のイベントにあわせて実行したい場合に便利です。既存のコルーチンを UniTask で書き直すときに最も汎用的なメソッドです。

 たとえば以下のようなコルーチンを考えます。

IEnumerator F(Func<bool> g) {
    while (g()) {
        yield return null;
    }
}

 すると、これは下記のように書き直すことができます。

async UniTask F(Func<bool> g) {
    while (g()) {
        await UniTask.Yield(PlayerLoopTiming.Update);
    }
}

 コルーチンの yield return null というのは、次のフレーム(Update の実行)まで処理を中断するという意味です。つまり while の各ループが 1 フレームに 1 回実行されることになります。

 await UniTask.Yield(PlayerLoopTiming.Update) も同様に、次のフレームまで待つ処理になります。さらに UniTask では、 PlayerLoopTiming という関数で色々なタイミングを指定することもできます。

 これを利用すれば、 Unity の Update を使わなくても毎フレーム更新する処理というのが書けます。

async UniTask F() {
    while (true) {
        // Update 相当の処理をする
        await UniTask.Yield(PlayerLoopTiming.Update);
    }
}

おわりに

 ひとまず、以上です!

 いつもは視覚的にわかりやすい進捗記事を書いているのですが、ここ数日は内部的な改善に尽くした関係上、こんな記事になりました。こんな記事も需要ありますかね?

 ゲーム本体の改善としては、この記事にあるように今までコルーチンを使っていた部分を UniTask 化した他、前回の記事のように AssetBundle を暗号化したり、最近では この記事 を参考に、宴のアセットを StreamingAssets から読み込むようにしたりしていました。

 こういった内部的な改善は、どうしてもわかりにくいので出せるものがなくなってしまい、気が滅入りますね……。とはいえ、改善というならなにか技術記事は書けるだろう、という思いもあり、今回思い切って書いてみた次第です。

 この手の記事も今後更新していきたいと思います!

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

水無川旅館 2024/07/16 12:00

【Unity】一番簡単な AssetBundle & 暗号化

 こんにちは! 今日は AssetBundle 暗号化について、記事を書いてみたいと思います!

 大前提……今回の記事を参考にする場合、自己責任でお願い致します。

 この記事を参考にして起きたいかなる事態にも責任は負いかねますので、ご了承願います。

一番簡単? Addressables じゃないの?

 AssetBundle より Addressables のほうが新しいですから、多くの場合、 Addressables を使ったほうがよいでしょう。ただ、 Addressables で暗号化をするのは非常に大変なのです。

(参考) AddressableでAssetBundleを暗号化して扱う

 一応、できなくはありません。ただ、 AssetBundleProvider というクラスのコードを丸々コピペして改造しなければならず、大変なのです。

 そう、コピペ、です。継承とかではありません。

「えっ!? 今日は Addressables 使っていいのか!?」
「ああ…どんどん使え」

もぐ…もぐ…もぐ…

「ただ今より暗号化実装を開始する!!」

 暗号化するなら生で AssetBundle を使ったほうが簡単です。

 今回は 一番簡単な ということで、生で AssetBundle を使うことにしました。

なにはともあれ暗号化しよう!

 とりあえず暗号化コードを書きましょう(Unity非依存の純粋な C# のコード)。本質的なコードはこれだけなので、ここまでできたら完成したも同然ですね!!

暗号化

using System.IO;
using System.Security.Cryptography;

public static void Encrypt(Aes algorithm, Stream plainTextStream, Stream cipherTextStream) {
    var encryptor = algorithm.CreateEncryptor(algorithm.Key, algorithm.IV);

    using (var cryptoStream = new CryptoStream(cipherTextStream, encryptor, CryptoStreamMode.Write)) {
        // 非同期にするには CopyToAsync を使えば OK
        plainTextStream.CopyTo(cryptoStream);
    }
}

復号

using System.IO;
using System.Security.Cryptography;

public static void Decrypt(Aes algorithm, Stream cipherTextStream, Stream plainTextStream) {
    var decryptor = algorithm.CreateDecryptor(algorithm.Key, algorithm.IV);

    using (var cryptoStream = new CryptoStream(cipherTextStream, decryptor, CryptoStreamMode.Read)) {
        // 非同期にするには CopyToAsync を使えば OK
        cryptoStream.CopyTo(plainTextStream);
    }
}

AssetBundle をビルド&暗号化する

 まずは AssetBundle に含めるアセットを設定します。 AssetBundle には色々なアセットが含められますが、今回はシーンを丸ごと含める想定です。


↑シーンのインスペクタから AssetBundle の名前を指定します。

 そして Editor フォルダ以下に次のコードを追加します。すると Assets > Build AssetBundles というメニューが表示されますので、クリック。 AssetBundle がビルドされます!

 これで Assets/AssetBundles平文の AssetBundle が保存されます。

private const string plainAssetBundleDirectory = "Assets/AssetBundles";

[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
    if(!Directory.Exists(plainAssetBundleDirectory)) {
        Directory.CreateDirectory(plainAssetBundleDirectory);
    }

    BuildPipeline.BuildAssetBundles(plainAssetBundleDirectory, 
                                    BuildAssetBundleOptions.None, 
                                    BuildTarget.StandaloneWindows);
}

AssetBundle を暗号化する

 次のコードを Editor 以下に追加すると Assets > Encrypt AssetBundles というメニューが現れますので、クリック。これで Assets/StreamingAssets/AssetBundles 以下に暗号化された AssetBundle が保存されます。

 StreamingAssets というのは、 Unity で特別扱いされるフォルダのひとつで、 AssetBundle とかを配置して読み込むのに使います。

// ASCII で 32 桁の文字列にすること!!
private const string KEY = "";
private const string plainAssetBundleDirectory = "Assets/AssetBundles";
private const string cipherAssetBundleDirectory = "Assets/StreamingAssets/AssetBundles";

[MenuItem("Assets/Encrypt AssetBundles")]
static void EncryptAllAssetBundles()
{
    if(!Directory.Exists(plainAssetBundleDirectory)) {
        Directory.CreateDirectory(plainAssetBundleDirectory);
    }

    if(!Directory.Exists(cipherAssetBundleDirectory)) {
        Directory.CreateDirectory(cipherAssetBundleDirectory);
    }

    var key = Encoding.ASCII.GetBytes(KEY);

    foreach (string plainFilePath in Directory.EnumerateFiles(plainAssetBundleDirectory)) {
        var fileName = Path.GetFileName(plainFilePath);
        var cipherFilePath = Path.Combine(cipherAssetBundleDirectory, fileName);
        var ivFilePath = Path.Combine(cipherAssetBundleDirectory, fileName + ".iv");

        using (Aes algorithm = Aes.Create()) {
            algorithm.Key = key;

            using (var ivFileStream = new FileStream(ivFilePath, FileMode.Create, FileAccess.Write)) {
                ivFileStream.Write(algorithm.IV, 0, algorithm.IV.Length);
            }

            using (var plainFileStream = new FileStream(plainFilePath, FileMode.Open, FileAccess.Read)) {
                using (var cipherFileStream = new FileStream(cipherFilePath, FileMode.Create, FileAccess.Write)) {
                    AES.Encrypt(algorithm, plainFileStream, cipherFileStream);
                }
            }
        }
    }
}

AssetBundle を復号する

 最後に AssetBundle を復号するコードを書きます。これで暗号化された AssetBundle を読み込むことができました! おつかれまでした!

using System.IO;
using System.Security.Cryptography;
using System.Text;

using UnityEngine;

[SerializeField] private string KEY = "";
[SerializeField] private string assetBundleName = "";

// Assets/StreamingAssets/AssetBundles のパス
var assetBundlesPath = Path.Combine(Application.streamingAssetsPath, "AssetBundles");

// AES の IV のパス
var assetBundleIvPath = Path.Combine(assetBundlesPath, assetBundleName + ".iv");

// 暗号化された AssetBundle のパス
var assetBundlePath = Path.Combine(assetBundlesPath, assetBundleName);

var key = Encoding.ASCII.GetBytes(KEY);
var iv = File.ReadAllBytes(assetBundleIvPath);

using (var cipherStream = new FileStream(assetBundlePath, FileMode.Open, FileAccess.Read)) {
    using (var plainStream = new MemoryStream()) {
        using (Aes algorithm = Aes.Create()) {
            algorithm.Key = key;
            algorithm.IV = iv;

            // 説明のため、同期
            // 実際には非同期にすると吉
            AES.Decrypt(algorithm, cipherStream, plainStream);
            AssetBundle.LoadFromStream(plainStream);
        }
    }
}

ちょっと待って! このままだと…

 このままですと、大きな問題があります。それは……

暗号化前のアセットもビルドに含まれてしまう

 ことです。

 暗号化するなら、元々のファイルはビルドから取り除かなければ意味がありません。

シーンをビルドから取り除く

 AssetBundle を読み込むだけの Boot シーン(仮称)を作成し、その他のシーンは Build Settings から取り除きます。

Before

After

 Boot シーンでは、 AssetBundle を復号して読み込む他、最初のシーン(Hijack の場合は GameMaster)を読み込みます。ちなみに、 GameMaster はシーン共通のオブジェクトを常駐させたり、他のシーン(Action, Adventure, LosAngeles, DeathValley)などを読み込んで動かすシーンです。

 これで Boot シーンが依存しているアセットだけがビルドに含まれるようになりました。今度こそ、おつかれさまでした!

なお、 Resources や C# のコード等に注意

 ほとんどのアセットは基本的に Build Settings に含まれるシーンから参照されていないとビルドには含まれませんが、 Resources や C# のコード、 Preloaded Assets に設定したアセット等は、シーンの存在に関係なくビルドに含まれてしまいます。そこは注意しましょう。

 きちんと何が含まれていて何が含まれていないかを確認するには、 Build Report Toolを使うと楽です。ちょうどセールになりそうなので、チェックするといいかもしれません!

 このあたりを厳密にやると本当に大変なので、今回の記事はここまでです。ただ、注意点としては気にかけていてくださいね。

無料特典コード

 無料特典で、今回のコードの非同期版&そのまま完璧にコピペで動く完全版もご用意しています! ぜひぜひ、よろしくお願いします~~!!

 なお、著作権は放棄していません。MPL 2.0 というライセンスで提供します。使用する場合は、クレジット表記・ライセンス表記はお願い致します。

 また無保証です(OSSでよくある条件です)。このコードを使うことで起きたいかなる事態にも責任は負いかねますので、ご了承ください。

特典コードの利用規約

 特典コードは Mozilla Public License, version 2.0 (MPL 2.0) で提供されています。 MPL 2.0 の日本語参考訳もあります。

フォロワー以上限定無料

無料プラン限定特典を受け取ることができます

無料

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

水無川旅館 2024/07/15 19:25

【Unity・アセット紹介】Text Animator for Unity を使ってみよう!

 こんにちは! 今日はアセットの紹介記事を書いてみたいと思います!

 今回は、 Asset Store にて有料販売している Text Animator for Unity の紹介です!

 Cult of the Lamb 等でも採用されているらしいです。あの特徴的な会話シーンはこれで実装されていたのですね!

 Hijack でも採用しています。 Text Animator for Unity を使うと、たとえば以下の動画のようなことができます!(5秒くらいからの書簡風の表現です!)

一番簡単な使い方! Text Animator コンポーネント

 Text Animator は、 Text Mesh Pro を拡張するコンポーネントです。まずは GameObject > UI > Text Mesh Pro から Text Mesh Pro オブジェクトをつくって、それに Text Animator コンポーネントを追加する形で使います!

前準備

 とりあえずなにはともあれ Text Mesh Pro を作成。

 Text Animator - Text Mesh Pro を Add Component で追加します。

Web アトモスフィア漂うタグ形式でエフェクトを追加

 Text Mesh Pro のテキストに、 HTML みたいなタグ形式で <shake>~</shake> などと書きます。

 使えるタグ一覧はここにあります:Built-in Effects list

(あとで説明しますが、 BehaviorsAppearances and Disappearances は違うので注意してください。今回使うのは Behaviors です)

 あと、ついでに width / height とかも見やすいよう変更しておいてください。私は 1920x1040 サイズの画面に対して、半分の 960x540 のサイズの Text Mesh Pro を中央に表示することにしました。

 これで Play ボタンを押すか、 Text Animator の Preview in Edit Mode というボタンを押します。

 正常に動作すれば、以下の動画のように表示されるはずです!

テキストを文字ごとに表示しよう! Typewriter - By Character コンポーネント

 Text Animator だけですと、文字を揺らしたりはできるのですが、「文字を1文字ずつ表示する」といったことはできません。それをするためには、 Typewriter - By Character コンポーネントを使います。

再び Web アトモスフィア漂うタグを追加

 Typewriter 用のタグも追加します。ただし今度は {fade}~{/fade} のように {~} で囲うことに注意してください。

(前述した Appearances and Disappearances とはこのことです)

 使えるタグ一覧はここにあります(再掲):Built-in Effects list

 消える場合は {#fade}~{/#fade} のように指定しますが、これはタグを指定しただけでは動作せず、後々説明するプログラムから操作しないと動きません。

ノーコード用の設定 Typewriter Starts Automatically

 後で説明しますが、 Typewriter はプログラムで操作するのが基本です。しかし、とりあえず動かしてみたいという場合には、 Typewriter Starts Automatically という設定が使えます!

 この設定は後でオフにしてください。

 この状態で Play すると、以下のように表示されるはずです!

Typewriter をプログラムから操作してみよう!

 特に「文字が1文字ずつ消える動作」を実現するには、プログラムを書くことが不可欠です!

 だいたいこんな感じのコードになります。

using Febucci.UI;

var typewriter = GetComponent<TypewriterByCharacter>();

// テキストが表示されたら即消す
// 必要なら WaitForSeconds などで待つ
UnityAction onTextShowed = null;
onTextShowed = () => {
  typewriter.onTextShowed.RemoveListener(onTextShowed);
  typewriter.StartDisappearingText();
};
typewriter.onTextShowed.AddListener(onTextShowed);

// テキストを表示する
// 消すタグ {#fade}~{/#fade} も一緒に指定する
var text = "hello world";
typewriter.ShowText($"{{#fade}}{{fade}}{text}{{/fade}}{{/#fade}}");

Hijack 内部でマジで使われている特典コード

 このあたりの処理は汎用的に書けますので、 Hijack 内部のコードを一部抽象化して公開します…!

 なお、著作権は放棄していません。MPL 2.0 というライセンスで提供します。使用する場合は、クレジット表記・ライセンス表記はお願い致します。

 また無保証です(OSSでよくある条件です)。このコードを使うことで起きたいかなる事態にも責任は負いかねますので、ご了承ください。

特典コードの利用規約

 特典コードは Mozilla Public License, version 2.0 (MPL 2.0) で提供されています。 MPL 2.0 の日本語参考訳もあります。

フォロワー以上限定無料

無料プラン限定特典を受け取ることができます

無料

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

« 1 2

記事のタグから探す

月別アーカイブ

限定特典から探す

記事を検索