Unity でスクリーンセーバーを作る方法を模索してみた

[次の日付の記事](https://qiita.com/amenone_games/items/25c857608af0138eb646)


はじまり
========

Unity でHLSL書いてシェイダーアートを作ってみたいなーと漠然と思いつつ、どうせならスクリーンセーバーとして動かせないものかと漠然と思いながらあれこれ調べてみました。
するとどうも Windows と Android でビルド方法が見えそうだったので試してみることにしました。

目的
====

Unity で以下のプラットフォーム向けにスクリーンセーバーをビルドする設定や手順を調べる。
(シェイダーアートを書くのはここではやりません。)

- Windows x86-64
- Android

環境
====

- Windows 11 22H2
- Android 14
- Unity 2022.3.14f1

下調べ…スクリーンセーバーの仕組みを調べる
==========================================

そもそもUnity以外のスクリーンセーバーがどのように作られてるのか調べないと手順が見えてこないのでOSごとに調べます。

Windows スクリーンセーバー
--------------------------

Windows のスクリーンセーバーの拡張子は .scr ですが、中身は実行可能ファイルです。
最低限 **.exe の拡張子を .scr に変えて開けば実行**できます。
しかしコマンドライン引数が特別仕様で、この仕様に合った実行ファイルを作らないと実用に耐えません。

つまり、**スクリーンセーバー仕様のコマンドライン引数を解釈する.exe** を拡張子 .scr でビルドすれば動きます。
この .exe を自作すれば UnityPlayer をスクリーンセーバーにできそうですね。

Windows SDK (C/C++) で作ればコマンドライン引数をいい感じに解釈してくれる SCRNSAVW.LIB[^win32screensaver_winsdk] をリンクできますが、 .NET を使って C# でゴリゴリ書く[^win32screensaver_csharp]実装も理論上は可能です。どっちでいこうかなー?

[^win32screensaver_winsdk]: スクリーン セーバーの処理 - Win32 apps | Microsoft Learn (2023年12月4日閲覧) https://learn.microsoft.com/ja-jp/windows/win32/lwef/screen-saver-library
[^win32screensaver_csharp]: Creating a Screen Saver with C# (2023年12月4日閲覧) https://sites.harding.edu/fmccown/screensaver/screensaver.html

Android Daydream スクリーンセーバー
-----------------------------------

Daydream といえばVRプラットフォームの Daydream VR がありましたが、 Android 4.2 が新しかったころ Daydream という名前はスクリーンセーバーの機能名でした。
Android のスクリーンセーバーは充電中に表示します。

その仕組みを大雑把に説明すると Android API の android.service.dreams.DreamService というクラスを継承した**フォアグラウンドサービスを実装**すること。

Unity は普通にビルドすると1個のアクティビティからなるアプリを実装しますが、そもそもプログラムの始まりがアクティビティではないのでフォアグラウンドサービスを自作する必要があります。

逆に言えばスクリーンセーバーのフォアグラウンドサービスを自作[^androidscreensaver]することで動かせるようになるはずですね。
標準の GameActivity が使ってるはずの **com.unity3d.player.UnityPlayer** のクラスの仕様がマニュアルに載ってないのが曲者の雰囲気ですが……

[^androidscreensaver]: Android Developers Blog: Daydream: Interactive Screen Savers (2023年12月4日閲覧) https://android-developers.googleblog.com/2012/12/daydream-interactive-screen-savers.html

その他
------

macOS のスクリーンセーバーも実行可能バンドルを特定手順でつくればスクリーンセーバーになりそうですが、細かい手順[^macosscreensaver]もさることながら「UserDefaults を使わずに ScreenSaverDefaults で設定を保存せよ」という制約が嫌な感じです。
Unity の中間成果物をいじって PlayerPrefs の実装を魔改造しないと PlayerPrefs 封印する羽目になりそう……?
サブマシンの Mac が古いので別の機会に調べましょう。

<!-- iOS はスクリーンセーバーという名前の機能を持たないらしいので無視 -->

[^macosscreensaver]: Screen Saver | Apple Developer Documentation (2023年12月4日閲覧) https://developer.apple.com/documentation/screensaver

実装
====

Android
-------

作成したばかりのまっさらなプロジェクトを試しに Android Studio プロジェクトとしてエクスポートしてみます。

![Build Settings で Platform を Android にすると、 Export Project というチェックボックスがあり、ここにチェックを入れてると Build ボタンが Export ボタンに変化するのでこれを押します。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99321/fb3f1e61-5e5c-e3b4-5e28-71885d1c0b0e.png)

これを Android Studio で開いてみると、*いつもの UnityPlayerActivity* のソースコードを開くことができました。

![Windows なら Ctrl+Shift+N、macOS なら ⇧⌘O で検索すると早いですよ!](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99321/0889383f-2353-3832-3085-1dadbc722d52.png)


この中で com.unity3d.player.UnityPlayer をあれこれしているので、これの継承関係を Android Studio で調べてみるとどうやら**FrameLayout のサブクラス**のようです。
ならば Android 公式のサンプルコード[^androidscreensaver]における自作FrameLayoutとおおむね同様に使えそうです。

Android 公式ブログの *BouncerDaydream* と unityLib に収録されている *UnityPlayerActivity.java* の定義を見ながら見様見真似でサービスをつくっていきます。

&rarr; [UnityPlayerDreamService](https://github.com/TkoolerLufar/UnityScreenSaver/blob/dee3f5b2b3a2128a3ce1db61439059715ccc228d/Assets/Plugins/Android/UnityPlayerDreamService.kt)

**できましたー!**

しかしここで新たな問題発生です。 Unity 側から *currentActivity* が取得できないのです。

元来 UnityPlayer は Activity と一緒に使う前提で組まれているのですが、スクリーンセーバーの描画領域を提供するアクティビティは**非公開**のようです。

なのでスクリーンセーバー起動時 UnityPlayer.currentActivity は null にせざるを得ません。

Android API を使う Unity パッケージが動かなくなることがあります。**外部パッケージとの相性**は特に注意しましょう。

それでも自分の書いたコードで使いたい場合のために、代わりに Daydream のサービスを取得するための [UnityPlayerDream.Companion.getCurrentService()](https://github.com/TkoolerLufar/UnityScreenSaver/blob/dee3f5b2b3a2128a3ce1db61439059715ccc228d/Assets/Plugins/Android/UnityPlayerDream.kt#L5C1-L6C1) を実装してみました。
そもそもアクティビティではなくサービスなのでいろいろと勝手が異なりますが、ないよりはマシでしょう。
マネージドコードでスクリーンセーバーを終了するサンプルを作ってみました。

&rarr; [サンプルコード](https://github.com/TkoolerLufar/UnityScreenSaver/blob/dee3f5b2b3a2128a3ce1db61439059715ccc228d/Assets/QuitTimerScript.cs#L63-L78)

早速 LogCat をつないで動かしてみると終了間際に <ruby>NullPointerException<rp>(</rp><rt>ぬるぽ</rt><rp>)</rp></ruby> が発生してるのでまだ粗削りですが、とりあえず動くようになりました。

もう少し頑張れば、いつもの UnityPlayerActivity と両方入れてアクティビティで起動したら設定画面を開く、みたいな芸当もできそうです。

Windows
-------

C++ と Windows SDK で書こうか迷いましたが、 Unity 使いに読んでもらうなら C# で書いた方がいいだろうと判断したので .NET で書くことにします。

有志の情報[^win32screensaver_csharp]によると、本実行してほしいときには `/S` という引数を受け取るようです。
では「引数が `/S` だったら UnityPlayer を全画面で開く Windows アプリ」を .NET でサクッと書いてみましょうか。
なお本実行の際は Unity が自分でウィンドウを開くため、スクリーンセーバーの**終了処理を Unity で実装**する必要があります。
マウスの動き（と、ついでにタッチイベント）を毎フレーム見て変化があったら終了するようにしてみましょう。
プレビューと本実行の区別は後述。

ところで Windows 版 UnityPlayer が受け取る引数[^unityplayerargs]に `-parentHWND` というのがありまして、ここに10進数でウィンドウハンドルを渡すと、**そのウィンドウに** Unity の画面をレンダリングするようになるらしいです。[^unitywithwin32apps]
プレビューの時はこれを使います。

あとは引数が無い場合と `/C` の場合は設定画面を開くべきなのですが、 Windows 公式の中にこういうのがあるのでオミットしちゃいます。

![ブランクスクリーンセーバーの設定を開こうとすると「設定できるオプションはありません」というメッセージが表示されるだけになっています。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99321/b776d470-558e-81ea-360e-2ed6e9b25c33.png)

フォームアプリのパブリッシュで手間取ったり、プレビュー後に本実行すると映像が低解像度になるバグが発生したりしましたが、紆余曲折を経て以下のコードができました!

&rarr; [Program.cs](https://github.com/TkoolerLufar/UnityScreenSaver/blob/dee3f5b2b3a2128a3ce1db61439059715ccc228d/ExternalPrograms/Windows/UnityScreenSaverLauncher/UnityScreenSaverLauncher/Program.cs)

`/P` の処理に何か書いてあるのがわかるでしょうか?
プレビューと本実行を区別するため、起動時にレジストリをいじって **PlayerPrefs** を書き換えています。
レジストリの場所は例によって公式ドキュメント[^playerprefs]を参考にしました。

こいつをビルドして出てきた.exeを拡張子 .scr に変えて Unity のビルドに混ぜたら完成です!

[^unityplayerargs]: Unity スタンドアロンプレイヤーのコマンドライン引数 - Unity マニュアル (2023年12月11日閲覧) https://docs.unity3d.com/ja/2023.2/Manual/PlayerCommandLineArguments.html

[^unitywithwin32apps]: Unity - Manual: Integrating Unity into Windows applications (2023年12月11日閲覧) https://docs.unity3d.com/Manual/UnityasaLibrary-Windows.html

[^playerprefs]: PlayerPrefs - Unity スクリプトリファレンス (2023年12月11日閲覧) https://docs.unity3d.com/ja/2023.2/ScriptReference/PlayerPrefs.html

仕上げ
------

最後に Unity 側で終了処理を書いたら実装完了!

&rarr; [QuitTimerScript (名前と機能が合ってないのはご容赦ください)](https://github.com/TkoolerLufar/UnityScreenSaver/blob/85b29de5305fc874de9273a1389f08ec00dc914a/Assets/QuitTimerScript.cs#L29-L55)

検証結果
========

- Android: プラグイン書いたら無事に作成できました
- Windows: 起動用のscrを作ったら無事に作成できました

感想
====

「右も左もわからないものを調べながらなんとか作る」感覚を久しぶりに味わいました。 (※普段はUnity使ってない)
最近新しいメンバーが時々入ってくるので、運用資料の更新とかコードレビューの時はこの心を思い出しながら行いたいです。

以上、スクリーンセーバーのつくり方でした。
これで当初の目的だったシェイダーアート・スクリーンセーバーが現実圏内になりました!

git リポ
========

&rarr; https://github.com/TkoolerLufar/UnityScreenSaver.git

有償アセットについて (10月1日追記)
==================================

unity screen saver などでググるとスクリーンセーバー実装用の**有償アセット**が見つかります。

https://assetstore.unity.com/packages/tools/input-management/screensaver-maker-113525?locale=ja-JP&srsltid=AfmBOoq2skAObt5ow59UzyyRRbZCkL2VbNv7B6TXlpVOQ0bKp-vKLJiJ

ただしあちらとは手順や細かい仕様、また対応バージョンが異なるので、うまくいかなければこちらも参考にどうぞ。

(ダンピングが成立するとよくないので出しゃばらないようメモ)

その後
======

&rarr; [後日談](/tkooler_lufar/items/d3fc59c1872a752fb2bf)

参考文献
========

脚注にまとめました。