tanaka's Programming Memo

プログラミングについてのメモ。

AsyncOperationHandleについて

Addressable Asset SystemでシーンをLoadした時の戻り値はAsyncOperationHandle<T>です。LoadSceneAsync()で返ってくるのと違うので、以下はどうやったらよいのやら。

  • 自動的にアクティブにならないように読み込んだシーンをアクティブにする方法
  • 通信エラーを考慮した処理の組み立て方

ということで調べたことのメモです。まずは以下を読んでみました。

docs.unity3d.com

目次

意訳

AsyncOperationHandle構造体は、Addressablesの色々なAPIの戻り値で利用されます。主に、アクセス状況や結果にアクセスするのに使われます。処理結果は、AsyncOperationHandle.Release()を呼んだり、Unloadするまで残ります。

処理が完了すると、AsyncOperationHandle.StatusAsyncOperationStatus.Succeeded(成功)かAsyncOperationStatus.Failed(失敗)になります。成功していたら、AsyncOperationHandle.Resultを通じて処理の結果にアクセスできます。

処理が完了したかどうかはStatusをチェックするか、AsyncOperationHandle.Completeを使って処理が完了した時に呼び出すコールバックを登録します。

AsyncOperationHandleが返したAddressablesAPIにアクセスするためのリソースが不要になったら、Addressables.Releaseメソッドを呼び出して解放します。詳しくはこちら( Custom Operations | Package Manager UI website )。

Typed vs Typeless

多くのAddressables APIは、ジェネリックAsyncOperationHandle<T>を返します。これにより、AsyncOperationHandle.CompletedイベントのAsyncOperationHandle.Resultの型を指定して安全に扱えます。一方、非ジェネリックAsyncOperationHandleもあります。ジェネリックと非ジェネリックはコンバートして相互に利用できます。ただし、もし非ジェネリックなハンドルを、異なる型のジェネリックにキャストすると実行時に例外エラーが発生します。

読み込み完了のイベント例

AsyncOperationHandle.Completedを使って読み込み完了時に呼び出すコールバックを登録する例です。

private void TextureHandle_Completed(AsyncOperationHandle<Texture2D> handle)
{
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Texture2D result = handle.Result;
        // これ以降、Textureを利用できます
    }
}

void Start()
{
    AsyncOperationHandle<Texture2D> textureHandle = Addressables.LoadAsset<Texture2D>("mytexture");
    textureHandle.Completed += TextureHandle_Completed;
}

AsyncOperationHandleが返すIEnumeratorで待つ

AsyncOperationHandleIEnumeratorを返すので、コルーチンで読み込みの完了を待つことができます。

public IEnumerator Start()
{
    AsyncOperationHandle<Texture2D> handle = Addressables.Load<Texture2D>("mytexture");
    yield return handle;
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Texture2D texture = handle.Result;
        // 以降、Textureが使えます

        // Textureの利用が終わったら、リソースを解放します
        Addressables.Release(handle);
    }
}

Async awaitで待つ

AsyncOperationHandle.Taskプロパティーを使って、Async awaitで処理を待つこともできます。

public async Start()
{
    AsyncOperationHandle<Texture2D> handle = Addressables.Load<Texture2D>("mytexture");
    await handle.Task;
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Texture2D texture = handle.Result;
        // 以降、Textureが使えます

        // Textureの利用が終わったらリソースを解放します
        Addressables.Release(handle);
    }
}

意訳、以上。

シーンを有効にするには

AsyncOperationHandleの説明には載っていませんでした。スクリプトマニュアルの方にありました。

読み込みが完了した時に渡されるSceneInstanceが持つActivate()メソッドを、読み込みたくなったタイミングで呼び出せばシーンを有効にできるそうです。

おまけ。LoadSceneAsync()の定義と引数について。

public static AsyncOperationHandle<SceneInstance> LoadSceneAsync(object key, LoadSceneMode loadMode = null, bool activateOnLoad = true, int priority = 100)
Type Name 説明
System.Object key 読み込むシーンを指定するAsset Addressです。
LoadSceneMode loadMode 読み込みモードです。LoadSceneMode.Singleなら、全てのシーンを閉じてから新しいシーンを読み込みます。LoadSceneMode.Additiveなら、現在のシーンはそのままに新しいシーンを追加読み込みしてマルチシーンにします。
System.Boolean activateOnLoad falseにするとシーンの読み込みが完了しても自動的には有効になりません。バックグラウンドでシーンを読み込みたい時などに利用します。有効にしたいタイミングで、SceneInstanceのActivate()メソッドを呼び出すことで有効にできます。
System.Int32 priority シーン読み込みの非同期処理における優先順位を指定します。

第1引数がIResourceLocationでシーンを渡すメソッドもあります。

メモリ管理について

Memory Mangement | Package Manager UI website より。

LoadとUnloadをセットで正しく呼び出せば使用していたメモリーは正しく解放されます。SceneLoadingについては、Addressables.UnloadSceneAsync()メソッドに解放したいシーンを渡して解放するか、Singleモードで別のシーンを読み込むと前のシーンとハンドルが両方解放されます。

補足

Addressables.UnloadSceneAsync()の第2引数にfalseを渡すと、シーンをUnloadしても、ハンドルは解放しなくなります。シーンを再利用したい時などに利用するためのもののようです。迂闊に使わない方がよさそうです。

まとめ

以上でおおよそ把握できました。シーンをバックグラウンドで読み込んであとでアクティブにするには、読み込み時にactivateOnLoadfalseにしてシーンを読み込んで、読み込みが完了した時に返されるSceneInstanceActivate()を呼び出せばシーンを有効にできます。

エラー処理は、処理の完了を待って、戻り値のStatusで成功と失敗を見分けます。

シーンの読み込みと解放については、シーンを切り替える際にAddressables.UnloadSceneAsync()で追加シーンを解放するか、Singleモードで新しいシーンを読み込みます。

こんな感じでいけそうです。

参考URL

ProTexでプロシージャルにUI用のイメージを作る

毎度作品を作っていて、ボタンなどのUIの画像をUI Builderから持ってきているのですが、なんかもっといい方法ないかとうっすら思っていたところ、ご存知AssetSaleLove@汗人柱さんのツイートが目に飛び込んできました。

本来のテクスチャー用途で利用する方法は、VR発掘隊さんが以下の記事でまとめてくださっています。

qiita.com

テクスチャーには画像を生成せずに利用する方法があるのですが、Imageコンポーネント向けのスクリプトが用意されていないので、UIで使うには画像化が必要になります。せっかくプロシージャルで形状を作ったのだから画像ファイルを作らずに使いたい!ということで、ちょちょっとスクリプトを用意してみました。ボタンで使えそうな角丸四角形の作り方と設定方法をご紹介します。

目次

やりたいこと

以下のようなボタン用の角丸四角形のイメージを、画像ファイルの用意なしに作ってみます。

f:id:am1tanaka:20190904160903p:plain
作りたい角丸ボタン

要件

ProTexの要件は、Unity2018.3.0以降です。今回試した環境は以下の通りです。

  • Windows10
  • Unity2019.1.5
  • ProTex 1.0

角丸四角形を作る

角丸四角形をProTexエディターで作成します。

  • Unityを起動して、適当なプロジェクトを作成するか開きます
  • 以下のアセットをゲットして、プロジェクトにインポートします

assetstore.unity.com

ProTexTextureを作成

ProTexでテクスチャーを作成するには、ProjectウィンドウからProTexTextureアセットを作成します。

  • ProjectウィンドウのCreateボタンをクリックして、ProTex > ProTexTextureを選択します
  • RoundRectという名前にしておきます

以上でテクスチャーを作成するためのアセットができました。

形状を作る

形状を作るビジュアルグラフを開きます。

  • 作成したRoundRectアセットをクリックして選択します
  • Inspectorウィンドウの右上のOpenボタンをクリックすると、ビジュアルエディターが開きます

f:id:am1tanaka:20190718152911p:plain
ノードエディターを開く

ProTexウィンドウの左側のワークスペースエリアを右クリックするとノードリストが表示されます。それぞれがどのようなものかは、Assetsフォルダー内にあるProTexManual.pdfNODES REFERENCEを眺めると、おおよそ把握できると思います。

f:id:am1tanaka:20190724140413p:plain
ノードリスト

角丸四角形を作ります。

  • 右クリックしたメニューからShape > Rectangleを選択します
  • 作成したノードをクリックして選択します

右下にパラメーターが表示されます。スライドバーを操作して効果を確認するとよいでしょう。

f:id:am1tanaka:20190724141007p:plain
パラメーター

以下を設定します。

  • WidthHeightをどちらも1にして、最大サイズにします
  • Roundness1にして、角を丸めます
  • Falloff0.05ぐらいにして、縁をくっきりさせつつ滑らかにします
  • AlphaOperationにして、図形以外の場所を透明にします
  • 以上設定したら、Apply Changesボタンをクリックします
  • Rectangleoutをドラッグして、Outputcolorにドロップします

f:id:am1tanaka:20190724141428p:plain
ノードをつなぐ

  • 左上のSave Assetボタンを押して保存します

以上で角丸四角形ができました!

UIでProTexを使う

作成したProTexTextureをUIで直接使えるようにします。以下の手順に従って、2つのスクリプトをプロジェクトに追加してください。

イメージにProTexTextureをバインドするスクリプトを追加

  • ProjectウィンドウでCreateボタンをクリックして、C# Scriptを選択して、名前をProTexImageBinderにします
  • ProTexのImageをバインドするためのスクリプト · GitHubを開きます
  • Rawボタンをクリックして、[Ctrl]+[A]キーですべて選択して、[Ctrl]+[C]キーでコピーします
  • Unityに切り替えて、作成したProTexImageBinderスクリプトをダブルクリックしてエディターで開きます
  • [Ctrl]+[A]キーを押してから[Ctrl]+[V]キーを押して、コピーしてあったコードを貼り付けます
  • 上書き保存します

以上で、バインド用のスクリプトができました。続けて、エディター用のスクリプトを追加しておきます。

  • Projectウィンドウから、ProTex > Editorフォルダーを開いて、Scriptsフォルダーを右クリックします

f:id:am1tanaka:20190724154400p:plain
EditorのScriptsフォルダーを右クリック

  • Create > C# Scriptを選択したら、ProTexImageBinderInspectorの名前にします
  • ProTexImageBinder用のエディタースクリプト · GitHubを開きます
  • Rawボタンをクリックして、[Ctrl]+[A]キーですべて選択して、[Ctrl]+[C]キーでコピーします
  • Unityに切り替えて、作成したProTexImageBinderInspectorスクリプトをダブルクリックしてエディターで開きます
  • [Ctrl]+[A]キーを押してから[Ctrl]+[V]キーを押して、コピーしてあったコードを貼り付けます
  • 上書き保存します

以上で環境ができました。ボタンに設定をします。

ボタンにProTexを適用する

  • HierarchyウィンドウからCreateボタンを押して、UI > Buttonなどで、ボタンを作ります
  • 作成したボタンに、ProTexImageBinderスクリプトドラッグ&ドロップして、アタッチします

f:id:am1tanaka:20190724154941p:plain
スクリプトをアタッチ

  • 先に作成したRoundRectアセットをドラッグして、ProTexTexture欄にドロップします

f:id:am1tanaka:20190724155203p:plain
ProTexTextureを設定

以上で、ProTexが生成したテクスチャーがボタンのImageに設定されます。

利用したいテクスチャーの大きさは`Texture Sizeからはじまるコンボボックスで選択します。サイズはPlayすると正式なものになります。Playをした時に、ぼやけないなるべく小さい値に設定するとよいでしょう。

f:id:am1tanaka:20190904160903p:plain
出来上がり!!

まとめ

ProTexで画像を作り、スクリプトを2つ追加して、UIのボタンにProTexTextureを反映させることができました。なかなか楽ちんで、アイコンの図形などもProTexで作成してます。

回転角度の自由度が少ないとか、数値が直に入力できないとか、細かい部分にあともう一歩機能があれば・・・というところもあるのですが、無料でこれは凄くありがたいアセットです。作者さんに感謝!!

参考URL

Lighting Settingsを別のシーンにコピーするエディター拡張

※情報が古くなっていたので現行版に併せて修正しました(2023/2/10)

LightingSettingsのコピー

この記事はUnityゆるふわサマーアドベントカレンダー2019の8/25分のブログです。

24日は @reximology さんの Unity 内部のコンパイラを追ってみた - らんどなテックブログ でした。明日は @UnagiHuman さんのOculusQuestとRealsenseを接続してPointCloudを見る - Qiitaです!

あるシーンのライト設定(Lighting Settings)を、別のシーンにコピペするエディター拡張をご紹介します。

github.com


目次

概要

1年ほど前の1週間ゲームジャムで溶岩番というパズルゲームを公開して高評価をいただきました。

溶岩番 | フリーゲーム投稿サイト unityroom

現在、そのスマホ版の製作を進めているのですが、せっかくなのでライトを奇麗にしたい。しかし、スマホだし軽くもしたい。ということで、ステージごとにシーンを作って、ライトを焼きつけることにしました。

そこで問題になるのがライトの設定(Lighting Settings)です。最近は、コンポーネントのプリセット値を保存して、他のコンポーネントで利用できるようになりましたが、ライトの設定ウィンドウだとこの機能が(多分)ありません。手動で書き写すか、シーンファイルをテキストエディターで開いて該当箇所をコピペすることはできますがあまりに面倒です。

何かないかと探したところ、Unityフォーラムでエディター拡張が紹介されているのを見つけました!使い方をご紹介します。

対応バージョン

2023/2/10現在、Unity 2019.3以降対応で2021LTSでの動作を確認しています。Editor/CopyLightingSettings.csが必要なコードなのでこれを動作バージョンに対応させれば動くということです(2023/2/10修正)

インポート

Unity2019.3以降ではPackage Managerで手軽に入れられるようになっていました。

以上で完了です。しばらく待つとインストールが完了して使えるようになります。

(以上、2023/2/10加筆)

BitBucketでコードが公開されているので、プロジェクトにスクリプトを作成してコードを貼り付ければ使えるようになります。

  • Unityを起動して、利用したいプロジェクトを開きます
  • 既存のものでも、新規にフォルダーを作成してもよいので、任意の場所にスクリプトを保存しておくためのEditorフォルダーを作成します

Editorという名前のフォルダーを用意

  • Editorフォルダーの中に、C# Scriptを新規で作成して、CopyLightingSettingsという名前にします

CopyLightingSettings

以上で、問題なければ実装完了です。

使い方

使い方が以下の動画で紹介されています。

youtu.be

以下、手順です。

  • コピー元のシーンを開くか、アクティブにします

コピー元のシーンをアクティブにする

  • Windowメニューから、Renderingを選択すると、Copy Lighting Settingsメニューが追加されているので選択します

Lighting Settingsのコピー

これでライト設定がコピーされたので、貼り付けます。

  • コピー先のシーンを開くか、アクティブにします

ライト設定をしていないので、壁や床が暗い

  • Windowメニューから、Rendering > Paste Lighting Settingsを選択します

設定をペーストして、ベイク

今回はライトのベイクをしたいのでペースト後にGenerate Lightingボタンをクリックしてライトマップを生成しました。これで完了です。

コピー完了!!

コピー前は以下のようなデフォルト設定でした。

元の設定

ペースト後は以下のように設定されています。

ペースト後

Past Lighting Settins in open Scenes

貼り付けメニューには、Past Lighting Settins in open Scenesというのもあります。こちらで貼り付けると、開いているすべてのシーンに同じライト設定が貼り付けられます。あるライト設定を多くのシーンに貼り付けたい場合に、マルチシーンでシーンを開いておいて、このメニューでまとめて貼り付けると便利そうです。ベイクは切り替えながらやらないといけませんけど。

まとめ

それほど真面目にライティングをやってこなかったのでライトをコピーできないのは面倒っぽいなと思うぐらいでしたが、いよいよ使う段階になったのでこれでかなり手間が省けそうです。コピペとベイクをエディター拡張で自動化したらさらにいい感じにできそうですが、それはまたいずれということで。

本当は、この手の設定はScriptableObjecctでアセット化して欲しいです。いずれUnityさんが対応してくれることを願いつつ。

もっとよい方法などございましたら、コメントなどでご教示いただければ嬉しいです!以上、2019年度の真夏のゆるふわアドカレの8/25の記事でした!

次は、 @UnagiHuman さんのOculusQuestとRealsenseを接続してPointCloudを見る - Qiita

です^^

参考URL

youtu.be

Definitive Stylized Waterで海る

Definitive Stylized Waterで海る

夏といえば海!ということで、少し古いモバイル端末でも動作する水面を描いてくれるアセットDefinitive Stylized Waterの記事です。

Definitive Stylized Water - Asset Store

この記事は、Unity アセット真夏のアドベントカレンダー 2019 Summer!の4日目の記事です。

目次

アセットの概要

Definitive Stylized Waterはトゥーン調の水面を描画することができるアセットです。VoxelorerBirdのマップ画面の水面を描画したくて購入しました。水深に応じて色を変化させたり、水面の波紋を描いたりしてくれます。

水面が動きます

UnityAssetStoreJapanさんの紹介ツイート。

この手のものだとStylized Water Shaderの方が実績があるような印象がありましたが、OpenGL ES2に対応しているのでこちらの方を選びました。うちにあったかなり古いAndroid4.2.2のSH-08Eでも動きましたし、速度も綺麗さもいい感じでお気に入りのアセットです^^

設定の仕方や問題の解決策、設定の概要などをご紹介します。

動作条件

  • AssetStoreの記載ではUnity5.4.5以降対応。Unity 2017 & 2018も動作確認済みとなっています
    • Unity2019.1.12でも動作しました
  • モバイル版は、OpenGL ES2以降に対応です
  • カメラのProjection設定がOrthographicだと動作しません

標準版とモバイル版

フル機能の標準版に加えて、機能制限をして機能が弱いモバイル端末でも動作するモバイル版が同梱されています。設定項目の違いは以下のような感じです。

モバイル版と標準版の設定項目

御覧の通り、右の標準版の方が設定は豊富です。主な違いは以下の通りです。

  • 水面下のオブジェクトが揺らいで見えるかの有無(Opacity)
  • 水面の波による変形の有無(WAVES)
  • 鏡面反射(REFLECTIONS)の有無

標準版がDeferredレンダリングで、モバイル版がForwardレンダリングです。Deferredレンダリングに対応しているモバイル端末であれば標準版も動作します。両方組み込んでおいて、実行時に機能を確認してどちらを使うかを選択するのもありかなと考えています。

iPhoneSEでもアセット付属の標準版サンプルが60fpsで動きましたので、iOS向けなら標準版で問題なさそうです。モバイル版はちょっと弱いAndroid用という感じです。

水面を作る

自分のプロジェクトにDefinitive Stylized Waterをインポートして、水面を表示します。このブログの操作はUnity2019.1.12で行っています。別のバージョンの場合は操作位置が異なる場合があります。

  • 対応するバージョンのUnityで、水面を表示したいプロジェクトを新規に作成するか読み込みます
  • Asset StoreでDefinitive Stylized Waterを購入、ダウンロードしてインポートします

Definitive Stylized Water - Asset Store

  • カメラのProjection設定はPerspectiveにしてください。Orthographic(平行投影モード。遠近感がなくなる主に2D用の設定)には非対応です
    • Orthographicを利用していた場合は、Perspectiveに変更して視野角を小さくして対応するとよいでしょう

ProjectionはPerspectiveに

  • 必要に応じて地形を配置します。以下の例では、Definitive Stylized Waterに付属の灯台のモデル(Lighthouseプレハブ)を座標0, 0, 0の位置に配置しました

海底を配置

  • 水面プレハブをシーンに配置します。標準版はStylizedWaterShaderフォルダーの直下にあるWaterPlaneです。モバイル版はStylizedWaterShader > MobileVersionフォルダー内のMobileWaterPlaneです。どちらか利用したい方をHierarchyウィンドウかSceneウィンドウにドラッグ&ドロップします

水面プレハブの場所

Mobile版の水面を設定

水面が真っ白になった時の対処

この段階で、水面が真っ白になる可能性があります。

水面が真っ白

考えられる原因はすでに述べていますが、以下の二つです。

  • CameraRendering Pathが、水面シェーダーと合っていない
  • CameraProjectionが、Orthographicになっている

Sceneウィンドウでは、Isoモードでも白くなります。

標準版での水面が真っ白になった時の対処

標準版は、Rendering PathDeferredである必要があります。手っ取り早いのはカメラで設定することです。

  • Main Cameraを選択して、Inspectorウィンドウで以下を確認して、必要なら設定します
    • ProjectionPerspective
    • Rendering PathDeferred

標準版のCameraの設定

まだ白いかも知れませんが、構わずPlayしてみてください。

標準版の水面

以上で設定完了です。

モバイル版での水面が真っ白になった時の対処

モバイル版は、Rendering PathForwardである必要があります。カメラにアタッチするためのスクリプトがあるのでひと手間増えます。

  • Main Cameraを選択して、Inspectorウィンドウで以下を確認して、必要なら設定します
    • ProjectionPerspective
    • Rendering PathForward

モバイル版のカメラの設定

  • StylizedWaterShaderフォルダーの中のCodeフォルダーに入っているEnableCameraDepthInForwardスクリプトを、カメラにドラッグ&ドロップしてアタッチします

カメラにスクリプトをアタッチ

まだ白いかも知れませんが、構わずPlayしてみてください。

モバイル版の水面

以上で設定完了です。

Sceneウィンドウの水面が白い

Sceneウィンドウの水面が白いままだった場合、Isoモードになっているものと思われます。右上のGizmoIsoになっていると思いますので、クリックしてPerspに変更してください。

SceneウィンドウのカメラがIsoモードになっていても白くなる

以上で設定完了ですが、水面は白いままの場合があります。設定したらとりあえずPlayしてみてください。設定ができていればPlayすれば正しく描画されます。

水面の位置はPosition、水面の大きさはScaleで調整すればOKです。

モバイル版の設定

モバイル版の設定概要です。標準版の設定概要はこちら

水面の設定はマテリアルで調整できます。MobileWaterPlaneオブジェクトのMesh Rendererに設定されているマテリアルがそうです。

水面用マテリアル

モバイル版と標準版の水面シェーダーが設定されているマテリアルは以下の位置にあります。必要に応じて複製して、水面にアタッチして利用するとよいでしょう。

水面の設定をするためのマテリアル

Color0, 1, 2

Color0, 1, 2は、水深ごとの色味です。以下はColor0の変更例で、水深0の部分の色です。

Color0, 1, 2

Color1Gradient1の水深の色、Color2Gradient2より深いところの色です。

Gradient1, 2

Color0, 1, 2と組み合わせて使う設定です。変更に応じて色が適用される深さが変わっています。マイナスになると無効になります。

Gradient

FresnelColor

Fresnelと書いてフレネルです。理科で習ったのではないかと思いますが(カリキュラム的に習わないコースもあるかもですが)、定義はwikiより以下の通りです。

屈折率が異なる物質間の界面に入射すると、一部は反射し、一部は透過(屈折)する。このふるまいを記述するのがフレネルの式である。 フレネルの式 - Wikipedia

といいつつ、このアセットではカメラから遠い水面の色を変化させる設定になっています。フォグの水面版といった感じです。

Fresnel

Color2だと深い部分全体の色が設定されますが、Fresnelの方は遠方の水面の色が変わっているのが観察できると思います。

FresnelExp

フレネルの距離です。値を小さくすればより近くからフレネルの色が適用され始めます。負の値が設定できますが、色味が明らかにおかしくなります^^;

FresnelExp

Foam

フォーム=泡の設定です。水面や浅いところの白い筋の模様の設定です。MainとSecondaryの2種類を設定することで、立体感のある揺らぎが演出されます。以下、MainFoamの設定です。

MainFoam

SecondaryFoamも基本的に同様です。

Textureは水面を表現するための画像データと思われます。ここをいじる必要はありません。

モバイル版の設定は以上です。

標準版の設定

標準版についてはオフィシャルの動画が分かりやすいです。

youtu.be

設定の概要は以下の通りです。

COLOR GRADIENT

水深に応じた色や、水面の設定を行う項目です。後ろに(*)が書いてある項目は、モバイル版にはない機能です。

  • COLOR GRADIENT 色の勾配
    • 左の3つの色がモバイル版のColor0, 1, 2、右の2つのスライドバーがGradientに該当します
  • Fresnel フレネル
    • 左の色がFresnelColor、右のスライドバーがFresnelExpに該当します
  • Light Color Intensity(*) ライトの色の強度
    • 水の色が、シーンのライトの色の影響を受ける度合いです。0にするとDirectional Lightの色や角度による光の具合の影響を受けなくなります
  • Specular Intensity(*) 反射光の強度
    • 太陽などの光の反射の強さです。0にすると光の反射がなくなり、1にすると強く反射します(Directional Lightが水面に反射して、カメラにうつる位置にないと影響が分からない可能性があります)
  • Roughness(*) 粗さ
    • 光の反射の拡散度合いです。Roughness0だとDirectional Lightの位置による太陽がはっきりと描画されます。1にするとぼんやりと拡散します
  • Water Opacity(*) 水の不透明度
    • この下のOpacity Depthとセットで利用して、水中のものが屈折して見える度合いと深さを設定します
    • 1にすると、Opacity Depthの値に関わらず、水中の屈折が見えなくなります
    • 0にすると、Opacity Depthで設定されている深さまで、水中の屈折が見えるようになります
  • Opacity Depth(*) 透明度の深さ
    • この値の深さまで、水中のものが歪んで描画されます

FOAM 泡

モバイルの設定とほぼ同じです。SpeedIntensityなど、微調整用のパラメーターが増えています。

Always Visibleにチェックを入れると、Intensityの設定に関わらず、水面に縞模様を描画します。トゥーン感を出す場合に有効そうです。

Secondary FoamのAlways Visible

WAVES(*) 波

モバイル版にない設定です。水面に波を発生させます。

  • Amplitude 振幅
    • 波の幅です
  • Speed
    • 波が進む速度です
  • Intensity
    • 波の高さです
  • Direction
    • 波の方向です
  • Vertex Offset
    • チェックを入れると、実際に水面の高さが変化して波打つようになります
    • チェックを外すと、水面の高さは変わらず、光や屈折の描画の変化のみで波を表します

REFLECTIONS(*) 反射

モバイル版にはない設定です。空や水上のオブジェクトを水面に映すかを設定できます。

  • Real time reflections
    • チェックを外すと、水面に水上のオブジェクトが映らなくなります
  • Intensity
    • 映り込みの度合いです
    • 0だと無効になります
  • Wave Distortion 波によるねじれ
    • Waveの動きでどれぐらい映り込みの像をゆがめるかの設定
    • WaveIntensityと関連するようです。1にすると自然に動く感じがします
  • Turbulence Distortion 乱流によるねじれ
    • 波より細かい水面の凹凸を描画する強さを設定します。大きい値にすると映り込みが強くなります
  • Turbulence Scale 乱流のスケール
    • 水面の凹凸のスケールです。値を大きくすると、凹凸が細かくなります

TEXTURES

水面の動きや泡のテクスチャーです。泡のテクスチャーを自前で用意して差し替えると、水面の模様が変えられます。

水面のエフェクト

VFXフォルダーにいくつかエフェクトが入っています。これも何気に便利です。

FoamParticles

泡立っている感じのエフェクトです。

FoamParticles

FoamUp

水しぶきのエフェクトです。

FoamUp

RadialSplash

放射状に広がる波紋です。

RadialSplash

SmallSplash

小さいものが落ちた時のようなしぶきと波紋です。

SmallSplash

WaterSplash

しぶきと波紋が一緒に出現するエフェクトです。

WaterSplash

川などを作る

付属の水面プレハブは平面ですが、ProBuilderなどで川となるメッシュを作成して水面マテリアルなどをアタッチすれば、傾斜のある水面を作ることができます。

川用のメッシュ

背景に乗せるとこんな感じになります。

モバイル版の場合はStylizedWaterMobileマテリアルを設定するだけでOKです。標準版を使う場合は、StylizedWaterマテリアルをアタッチした上で、Codeフォルダー内のWaterReflectionスクリプトをアタッチして鏡面反射に対応させます。

WaterReflection

※水が流れ落ちているエフェクトは、Unity公式のParticle Pack | VFX Particles | Unity Asset Storeのものを利用しています。

まとめ

Definitive Stylized Waterの設置方法や設定、付属のエフェクト、平面以外のメッシュを水面にする方法をご紹介しました。トゥーン系のポップな水面を作るのに適していて、少々古いAndroid端末でも動作するのが嬉しいです。標準版であれば、リアルな水面を作ることも可能です。波も生成できるので楽しいです。

暑い夏に、ナイスな海を作ってみてはいかがでしょうか。

関連リンク

Definitive Stylized Waterのアセットストア

Definitive Stylized Water - Asset Store

Definitive Stylized Waterの紹介動画

youtu.be

Definitive Stylized Waterの紹介ツイート

滝の部分に使ったパーティクルアセット

assetstore.unity.com

Photon BoltでPhysicsを利用する(ver1.2.9)

f:id:am1tanaka:20181129195310g:plain
作例

こちら( Photon BoltでPhysicsのボールは動くのか - tanaka's Programming Memo )の記事の内容が古くなっていたので、2019/7/28現在の最新場である1.2.9に対応させて書き直しました。

目次

Photon Boltとは

マルチプレイヤーゲームのネットワーク部分を担当してくれるUnity用のネットワークエンジンです(ビジュアルスクリプティングのBoltの話ではございません!!Exit Gamesが提供しているネットワークエンジンの一つで、最近流行っている人が集まって対戦するようなリアルタイムネットワークゲームを開発するのに向いています。

詳しくは以下の公式サイトや、アセットストアをどうぞ。

www.photonengine.com

注意点!

Photon Boltの最大の注意点の一つは、現時点でWebGLに対応していない!!ということだと思います。1週間ゲームジャムとかでは使えませんのでご注意を!PUNの方は使えます。

目的

学園祭のような人が集まる場所で、同じ教室内のLAN環境で、エアホッケーのネットワーク対戦ゲームを動かしたいというのが今回の出発点です。ビルドターゲットはWindowsmacなどのPCです。最初のマッチング時にPhotonクラウドを通す必要があるのでインターネット接続は必要です。

エアホッケーならRigidbodyvelocityを同期できれば簡単に作れそうです。しかし、Photon Boltにはvelocityを同期するためのState設定がありません。そこで、Commandを使うことで対応させました。その構築手順です。

前提

はじめようを読んで、Photon Bolt Freeのインストールと概要を確認しておくのが望ましいです。

ブログではUnity2018.4.4f12019.1.5で動作確認しました。ブログ執筆時のPhoton BoltのUnityのサポートバージョンは2017.4.26以降です。

考え方

Photon BoltでPC間で同期したいデータは、Stateを作って、それにデータを割り当てます。そこにRigidbodyvelocityとかを同期する設定があればいいのですがありません。UNetにはあったので油断していました。

Photon Boltでは、Commandsを使って、クライアントからホストに操作情報を送らせて、サーバーでまとめて制御した結果をクライアントに戻して反映させる、という仕組みがあります。ざっくりこんな感じ。

f:id:am1tanaka:20181129142906p:plain
(public) RollerBallBoltChart | Sketchboard

1カ所でまとめて物理シミュレーションを行うので操作に対する反応は少し遅れますが、全てのプレイヤーの動きを一致させることができます。

今回は、CommandsでマウスのX-Yの移動量を送信して、プレイヤーの座標を結果(Result)として返すようにしました。

プロジェクトを作成

まずはProjectを作成します。作業中、指示通りにやっているのに動かない場合は、Unityを閉じてプロジェクトを開き直すとか、Unityが2017.4以降かを確認してください。

アカウントの入手とApp IDの取得

Sign In | Photon Engineを開いて、Photonアカウントの作成と、Bolt用のアプリを作成して、App IDを生成してください。

フィールドを読み込む

作例用のプレイヤーやフィールドのオブジェクトを用意しました。まずはそれらを組み込みます。

  • 新規にPhotonBoltPhysicsなどの名前でUnityのプロジェクトを作成してください。Unityのバージョンは2017.4以降の安定版がよいでしょう。このブログではUnity2018.4.4を使っています
  • こちらを開いてください
  • 最新版のPhotonBoltPhysics@v?.?.?.unitypackageをクリックしてダウンロードします
  • ダウンロードが完了したら、UnityのProjectウィンドウにドラッグ&ドロップして、インポートします

以上で、ゲーム用のオブジェクトが読み込まれます。Scenesフォルダー内のGameシーンをダブルクリックすると、フィールドとプレイヤー2つ、ボールが確認できます。

f:id:am1tanaka:20190728141043p:plain
Gameシーン

Photon Boltの初期設定

  • Asset StoreからPhoton Bolt Freeをダウンロードしてインポートします

Photon Boltは、同時接続20接続(マッチング後にサーバーを利用しなくなったアカウントも含みます)などの制約以内であれば、無料で利用できます。開発中や、今回のような試用の場合はFree版でいけます。

エラーが出るので、直すために設定をします。

  • Boltメニューがあるか確認します。見当たらなかったら、Unityを閉じてプロジェクトを開きなおします
  • BoltメニューからWizardを選んで、ウィンドウが開いたらNextをクリックします
  • PhotonのWebページで作成したApp IDをコピーして、Photon Bolt App ID or Email欄に入力します
  • 変なリージョンに行かれると接続ができなかったりしたので、テスト時はRegion[jp] Japan :: Tokyoにしておきます
  • NAT Punchthrough Enabledはチェックを入れたままにしておきます
  • 以上選択したらNext

f:id:am1tanaka:20181129125930p:plain

  • Sampleを有効にすると、公式のサンプルを見ることができます。今回は不要なので、そのままNextをクリックします
  • Doneをクリックします
  • Bolt Setup Completeダイアログが表示されます。コンパイルが必要なので、Yesをクリックします

以上でエラーが解消されます。エラーがまだ表示されていたら、ConsoleウィンドウのClearボタンをクリックすれば消えます。

Menuシーンの作成

サーバーとクライアントのどちらでログインするかを決めるためのボタンとサーバー接続のコードを実装したMenuシーンを作成します。

  • デフォルトで作成されるSampleSceneがあったら、名前をMenuに変更します
    • Unity2017だとシーンはないので、New Sceneで新しいシーンを作成して、名前をMenuにします
  • 作成したMenuシーンをダブルクリックして切り替えます
  • HierarchyウィンドウのMain Cameraをクリックして選択します
  • InspectorウィンドウのAdd Componentをクリックして、New scriptで新しいスクリプトを作成して、名前をMenuにします
  • 作成したMenuスクリプトをダブルクリックして開きます
  • 以下のスクリプトに置き換えます。Photon BoltアセットのGettingStartedサンプルに含まれているMenu.csを元にして、シーン名をGameに変更したものです
using System;
using System.Collections;
using System.Collections.Generic;
using Bolt.Matchmaking;
using UdpKit;
using UnityEngine;

namespace Bolt.Samples.GettingStarted
{
    public class Menu : Bolt.GlobalEventListener
    {
        bool ShowGui = true;

        private void Awake()
        {
            Application.targetFrameRate = 60;
            BoltLauncher.SetUdpPlatform(new PhotonPlatform());
        }

        void OnGUI()
        {
            if (!ShowGui) { return; }

            GUILayout.BeginArea(new Rect(10, 10, Screen.width - 20, Screen.height - 20));

            if (GUILayout.Button("Start Server", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)))
            {
                // START SERVER
                BoltLauncher.StartServer();
            }

            if (GUILayout.Button("Start Client", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)))
            {
                // START CLIENT
                BoltLauncher.StartClient();
            }

            GUILayout.EndArea();
        }

        public override void BoltStartBegin()
        {
            ShowGui = false;
        }

        public override void BoltStartDone()
        {
            if (BoltNetwork.IsServer)
            {
                string matchName = Guid.NewGuid().ToString();

                BoltMatchmaking.CreateSession(
                 sessionID: matchName,
                 sceneToLoad: "Game"  // 元のファイルからここを変更
                );
            }
        }

        public override void SessionListUpdated(Map<Guid, UdpSession> sessionList)
        {
            Debug.LogFormat("Session list updated: {0} total sessions", sessionList.Count);

            foreach (var session in sessionList)
            {
                UdpSession photonSession = session.Value as UdpSession;

                if (photonSession.Source == UdpSessionSource.Photon)
                {
                    BoltNetwork.Connect(photonSession);
                }
            }
        }
    }
}

Playすると、以下のようにメニューが表示されます。

f:id:am1tanaka:20190728162702p:plain
Menu画面

まだ設定などが足りないので、ボタンを押してもエラーになります。Playを停止して設定を続けます。

ビルド設定

チャプター1 | Photon Engineを参考にビルド設定をします。

  • FileメニューからBuild Settingsを選択します
  • ProjectメニューからMenuシーンをドラッグして、Scenes In Build欄にドロップします
  • 同様に、Gameシーンもドラッグ&ドラッグでScenes In Build欄に追加します

f:id:am1tanaka:20190728161910p:plain
ビルドシーン

  • Player Settings...ボタンをクリックします
  • Resolution and Presentation欄をクリックして開いて、以下を設定します
    • Fullscreen ModeWindowsに設定
    • Default Screen Width640に設定
    • Default Screen Height360に設定
    • Run In Backgroundにチェックを入れます
    • Display Resolution DialogDISABLEDにします

以上は開発用の設定です。開発が完了したら適宜変更してください。

  • Build Settingsウィンドウを閉じます
  • Boltメニューから、Compile Assemblyを選択します。これをやらないと、シーンがBoltに認識されないので動きません

以上設定したらPlayして、Start Serverボタンをクリックしてください。Gameシーンに切り替わります。

f:id:am1tanaka:20190728164339p:plain
Gameシーン

Gameシーンの設定を全くしていないので操作はできません。停止して、Gameシーンの実装をします。

Gameシーンを作る

ネットワークに接続して、Gameシーンに切り替えることができたので、Gameシーン本体の実装を始めます。

Stateの作成

Boltでネットワークに登場させるゲームオブジェクトは、同期するデータを定義したStateを持ちます。今回はTransformだけ同期します。そのためのTransformStateを作成します。

  • BoltメニューからAssetsを選択します
  • Bolt Assetsウィンドウの何もない部分を右クリックして、New Stateを選択します
  • 以下の通り設定します

f:id:am1tanaka:20190728165116p:plain
TransformStateの作成

  • 設定したら、Bolt Editorウィンドウは閉じます

利用するプロパティはTransform型のTransformだけです。

SpaceはデフォルトがLocalだったのでそのままにしました。今回は使っていませんが、StateCommandなどの設定にCompression(圧縮)の項目があって、そこで値の範囲を制限して送信するデータ量を減らすことができます。SpaceLocalにしておくことで、設定した範囲を越える移動は親のオブジェクト側で行うような工夫ができそうです。

捕捉: クライアントの動きを滑らかにする設定(補間とNetwork Rate)

Smoothing AlgorithmNoneのままにしました。Interpolation(内挿)とExterpolation(外挿)の違いは以下にあります。

Interpolation vs. Extrapolation | Photon Engine

ざっくりと、Interpolationは過去のデータを利用して動きを滑らかに見せます。過去の場所の間に補間するので、突飛な場所に移動することはありません。ただし、少し古い座標に表示されることになります。

Exterpolationは過去の動きの傾向から、次の座標を予想して動かします。レスポンスは速いですが、予想が外れた場合はいたことがない座標に移動してしまいます。

今回のようにマウス操作&Physicsベースだと、動きの予想がしずらいのでExterpolationでの予測精度があまりよくありませんでした。また、Interpolationは遅延が気になります。

ということで、Noneのままにして補間はしないことにしました。そのままではガタついて見えてしまいますが、これはStateの送受信の頻度が原因です。最後の方の設定の調整のところで調整します。

Commandの作成

今回の肝であるクライアントからマウスの移動量を送り、座標を返してもらうためのCommandを作成します。

  • Bolt Assetsウィンドウの余白を右クリックして、New Commandを選択します
  • 以下のように設定します

f:id:am1tanaka:20190728165856p:plain
RollerBallBoltCommandの作成

  • 以上設定したら、Bolt Editorウィンドウと、Bolt Assetsウィンドウを閉じます

入力のMouseVector型で、動かす時に楽をするためにXZで送ることにしました。

結果はXYZ全て利用するVector型で、そのままローカル座標に放り込みます。効果がよくわかりませんでしたが、なんとなく奇麗に動いている気がするのでSmooth Corrections(スムーズに補正する)にチェックを入れました。あとは以前の設定にもあったのでTeleport Threshold10を設定しました。

ボールプレハブの作成

ゲームオブジェクトをネット対応させます。ネットワーク上で共有するオブジェクトにはBoltEntityスクリプトをアタッチして、ネットワークに接続した時にBoltNetwork.Instantiate()で生成します。そして、Stateのデータをオブジェクトのプロパティーと関連付けます。

  • Projectウィンドウから、Gameシーンをダブルクリックしてシーンを切り替えます
  • ProjectウィンドウのRollerBallBolt > Prefabsフォルダーを開いて、Ballプレハブをクリックして選択します
  • InspectorウィンドウでOpen Prefabボタンをクリックします(Open Prefabボタンがない場合はそのまま編集します)
  • InspectorウィンドウのAdd Componentボタンをクリックして、Bolt Entinyを検索などして見つけてアタッチします

アタッチした直後はエラーになります。以下を設定します。

  • Ballプレハブを再び開いて、State欄をITransformStateにします

f:id:am1tanaka:20190728170730p:plain
Stateを設定

  • BoltメニューからCompile Assemblyを選択してコンパイルします

以上でエラーがなくなります。

  • その他の設定も確認して、以下のようにします。TagBallを予め割り当てています

f:id:am1tanaka:20181129195533p:plain
Ball Inspector

  • InspectorウィンドウからAdd Component > New scriptを選択して、Ballという名前のスクリプトを作成します
  • Ball.csは以下の通りです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PhotonBoltPhysics
{
    public class Ball : Bolt.EntityBehaviour<ITransformState>
    {
        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }
    }
}

アタッチされた時にtransformStateTransformに割り当てているだけです。

プレイヤーの作成

プレイヤーも同様にネットワーク対応させていきます。

以下は設定済みです。

  • Player1Player2とも、TagPlayer
  • Sphere Colliderをアタッチして、良い場所と大きさに調整
  • Rigidbodyをアタッチして、Use Gravityを外し、Collision DetectionContinuous DynamicFreeze PositionYにチェック、Freeze Rotationを全てチェック

ネットワーク対応させるために、以下を設定します。

  • ProjectウィンドウからPlayer1プレハブをクリックして選択します
  • InspectorウィンドウにOpen Prefabボタンがあったらクリックします
  • InspectorウィンドウのAdd Componentボタンをクリックして、Bolt Entityを選択してアタッチします
  • InspectorウィンドウのStateITransformStateに設定します
  • BoltメニューからCompile Assemblyを選択して実行したら、Player1プレハブを開き直します
  • 新規スクリプトを作成して、Playerの名前にします
  • Player.csスクリプトの中身は以下の通りです。
using System.Collections;
using System.Collections.Generic;
using Bolt;
using UnityEngine;

namespace PhotonBoltPhysics
{

    public class Player : Bolt.EntityBehaviour<ITransformState>
    {

        [Tooltip("送られてきた操作を速度に変換するレート。Mouse XとMouse Yは-1~1に正規化されているので、動作環境の解像度は考えなくて構いません。"), SerializeField]
        float input2Speed = 20f;

        Rigidbody rb;

        float _x;
        float _y;

        #region System Loop

        private void Awake()
        {
            rb = GetComponent<Rigidbody>();
        }

        private void Update()
        {
            PollMouse();
        }

        #endregion System Loop

        #region Bolt Events

        /// <summary>
        /// transformをIPlayerStateのTranformに割り当てます。
        /// </summary>
        public override void Attached()
        {
            state.SetTransforms(state.Transform, transform);
        }

        /// <summary>
        /// 操作権を持つプレイヤー(Player1はホスト、
        /// Player2はクライアント)で呼び出されます。
        /// 操作をBoltEntityに渡します。
        /// </summary>
        public override void SimulateController()
        {
            IRollerBallBoltCommandInput input = RollerBallBoltCommand.Create();

            Vector3 data = new Vector3(_x, 0, _y);
            input.Mouse = data;

            entity.QueueInput(input);
        }

        /// <summary>
        /// オブジェクトのオーナーで呼び出されます。
        /// これはPlayer1, 2ともにホストで呼び出されます。
        /// 入力を受け取って動かします。
        /// Player2からは、クライアントに結果を送信します。
        /// </summary>
        /// <param name="command">送られてきた操作コマンド</param>
        /// <param name="resetState">操作権を持っていたらtrue</param>
        public override void ExecuteCommand(Command command, bool resetState)
        {
            RollerBallBoltCommand cmd = (RollerBallBoltCommand)command;

            if (resetState)
            {
                // Player2。送られてきたコマンドのデータを反映させます
                transform.localPosition = cmd.Result.Position;
            }
            else
            {
                // 入力を使ってオブジェクトを動かします
                rb.velocity = cmd.Input.Mouse * input2Speed;

                // ホストとクライアントの双方で呼び出されます
                // 現在の座標を送信します
                cmd.Result.Position = transform.localPosition;

            }
        }

        #endregion Bolt Events

        #region My Methods

        void PollMouse()
        {
            _x = Input.GetAxis("Mouse X");
            _y = Input.GetAxis("Mouse Y");
        }

        #endregion My Methods

    }
}

Advencedチュートリアルをベースに改造したものです。ポイントは以下の通りです。

  • Bolt.EntityBehaviour<ITransformState>を継承します
  • Update()でマウスの移動量を記録しておきます
  • アタッチされた時に、transformStateTransformに割り当てるコードをAttached()メソッドに実装します
  • SimulateController()は、操作をシミュレートする際にBoltから呼び出されるメソッドです。ここで、Update()で記録しておいたマウスの移動量をRollerBallBoltCommandInputMouseに設定して、QueueInput()で登録します
  • ExecuteCommand(command, resetState)に、コマンドを実行するための処理を実装します。commandは送受信するためのRollerBallBoltCommandインスタンスresetStateはサーバーかクライアントのどちらで動いているかを表していて、trueの時はコントローラー(クライアント)側の処理、つまり、送られてきた結果を自分のローカル座標に反映させます。falseの時は、オーナー(サーバー)側の処理で、QueueInput()で蓄えられた入力データを速度に反映させて、オブジェクトを動かし、結果をcommandに渡します

Player2の作成

同様にPlayer2も設定します。以下、ざっとした手順です。

  • Player2プレハブを開きます
  • Bolt Entityをアタッチ
  • 作成済みのPlayerスクリプトをアタッチ
  • StateITransformStateに設定
  • BoltメニューからCompile Assemblyを選択

以上でプレイヤーの設定完了です。

フィールド

枠や地面などのフィールドは、ネットワークで共有する必要はないのでそのまま配置しておけばOKです。

f:id:am1tanaka:20181129203141p:plain

オブジェクトを配置する機能の作成

公式のチュートリアルでは、ランダムに座標を設定したりしてスクリプト側で座標を作っていましたが、最初から登場させたい場所にゲームオブジェクトを配置して、そこにInstantiateした方が楽だろうということで作った機能です。

  • 新規に空のGame Objectを作成して、NetworkSceneManagerなどの名前にします
  • 新しいスクリプトを作成して、NetworkSceneManagerなどの名前にします

スクリプトは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PhotonBoltPhysics
{
    public struct OBJECT_RECORD
    {
        public int id;
        public Vector3 position;
    }

    public class NetworkSceneManager : MonoBehaviour
    {
        public static NetworkSceneManager Instance
        {
            get;
            private set;
        }

        public List<OBJECT_RECORD> playerPositions {
            get;
            private set;
        }
        public OBJECT_RECORD ballPosition
        {
            get;
            private set;
        }

        private void Awake()
        {
            Instance = this;

            playerPositions = new List<OBJECT_RECORD>();

            GameObject[] gos = GameObject.FindGameObjectsWithTag("Player");
            OBJECT_RECORD or;
            foreach(GameObject go in gos)
            {
                or.id = go.GetComponent<BoltEntity>().PrefabId.Value;
                or.position = go.transform.position;
                playerPositions.Add(or);
                Destroy(go);
            }

            GameObject ball = GameObject.FindGameObjectWithTag("Ball");
            or.id = ball.GetComponent<BoltEntity>().PrefabId.Value;
            or.position = ball.transform.position;
            ballPosition = or;
            Destroy(ball);
        }

        /// <summary>
        /// BoltEntity.prefabId.Valueを受け取って、該当する
        /// プレハブIDで記録された座標を返します。
        /// </summary>
        /// <param name="id">プレハブID</param>
        /// <returns>座標を返します</returns>
        public Vector3 GetPlayerPosition(int id)
        {
            foreach(OBJECT_RECORD or in playerPositions)
            {
                if (or.id == id)
                {
                    return or.position;
                }
            }
            return Vector3.zero;
        }
    }
}

Awake()で、PlayerBallのタグが付いたオブジェクトを検索して、プレハブIDと座標を記録したらDestroyします。サーバーでオブジェクトを生成する際に、プレハブIDで記録しておいた座標を検索して、その座標にゲームオブジェクトを配置しています。

ネットワーク管理スクリプト

最後に、シーンが始まった時に、プレイヤーやボールを生成するためのオブジェクトとスクリプトです。これは、ゲームオブジェクトにはアタッチしません。新規にスクリプトを作成したら、PhotonBoltPhysicsServerCallbacksなどの名前にします。コードは以下の通りです。

using UnityEngine;

namespace PhotonBoltPhysics
{
    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]
    public class PhotonBoltPhysicsServerCallbacks : Bolt.GlobalEventListener
    {
        /// <summary>
        /// このプログラムがシーンを読み込んだ時の処理
        /// </summary>
        /// <param name="map"></param>
        public override void SceneLoadLocalDone(string map)
        {
            // プレイヤー1を生成して、操作を担当します
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player1);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.PrefabId.Value);
            be.TakeControl();

            // ボールを作ります
            be = BoltNetwork.Instantiate(BoltPrefabs.Ball);
            be.transform.position = NetworkSceneManager.Instance.ballPosition.position;
        }

        /// <summary>
        /// クライアントがシーンを読み込んだ時に報告されるコールバック
        /// リモート用のプレイヤーを生成
        /// </summary>
        /// <param name="connection"></param>
        public override void SceneLoadRemoteDone(BoltConnection connection)
        {
            // プレイヤー2を生成して、操作を接続先に任せます
            BoltEntity be = BoltNetwork.Instantiate(BoltPrefabs.Player2);
            be.transform.position = NetworkSceneManager.Instance.GetPlayerPosition(be.PrefabId.Value);
            be.AssignControl(connection);
        }
    }
}

クラスは、Bolt.GlobalEventListenerを継承します。

    // :
    [BoltGlobalBehaviour(BoltNetworkModes.Server, "Game")]

上記の属性により、このクラスはサーバー、かつ、Gameシーンのみで実行されます。

SceneLoadLocalDone()は、サーバーのGameシーンが準備できた時にサーバーで実行するコードです。Player1Ballを生成して、Player1はサーバー自身が制御するのでTakeControl()を呼び出します。

SceneLoadRemoteDone()は、クライアントのGameシーンが準備できた時にサーバーで実行するコードです。Player2を生成して、操作はクライアントが担当するので、AssignControl()で接続先(connection)に割り当てます。

制作過程は、おおよそ以上の通りです。

動作確認

Photon Boltでは、開発を楽にするために実行形式のファイルを自動的にビルドして、ホストとクライアントで起動してくれる機能があります。それを使ってGameシーンをネットワークで実行してみます。

  • BoltメニューからScenesを選択します
  • Bolt Scenesウィンドウで、Clients欄を1にします。これで、クライアントアプリが1つ起動するようになります
  • Gameと書いてある欄の右のDebug Startをクリックします
  • Saveするかの確認ダイアログが表示されたら、Saveをクリックします

f:id:am1tanaka:20181128160121p:plain

ビルドが始まって、しばらく待つとアプリが1つ起動します。Unityでサーバー、クライアントアプリでクライアントが動きます。マウスを動かすと、アクティブなウィンドウのプレイヤーが操作できます。

f:id:am1tanaka:20190728185734p:plain
完成!!

設定の調整

Photon Boltの設定がデフォルトのままなので、クライアント側の動きがガタガタで、オブジェクトに雷マークが表示されています。これらを直すための設定は以下の通りです。

  • Windowメニューから、Bolt > Settingsを選択します
  • Network Rate1にします
    • これで、状態の同期を1フレームごとに行うようになるので、クライアントの動きがスムーズになります
    • ネット環境によっては、動きがおかしくなることがあるので、その時は2にしたり3に戻したり調整してみてください
  • 必要に応じて、Miscellanceous欄の以下のチェックを外します
    • Show Debug Info
      • オブジェクトに表示されるBoltアイコンが消えます
    • Show Help Buttons
      • ヘルプボタンを非表示にします
    • Visible By Default
      • 情報ウィンドウをデフォルトで非表示にします

f:id:am1tanaka:20190728185925p:plain
本当に完成!!

まとめ

Physicsで制御するゲームオブジェクトを、Photon Boltを使ってネットワーク上で共有することができました。

UNetに比べると手順はやや多い印象ですが、動いたあとの安心感があります。サービス停止の心配ないし...。

エアホッケーブロック崩しピンボール、コインプッシャーなど、Physicsを使って簡単に作れるタイプのゲームをネットワーク対応させるのに、今回の手法は有用と思います。

WebGLが使えたらなぁ...と願いつつ。

最後の最後に: Physicsの同期について

Photon Unity Networking(PUN)では、PhysicsのRigidbodyのvelocityを同期するためのクラスPhotonRigidbodyViewがあります。

Photon Unity Networking: PhotonRigidbodyView Class Reference

Photon Boltでも、Statevelocity用のVector3を追加して速度の同期を試みることはできます。ただ、このAPIリファレンスの解説にある通り、Rigidbodyのvelocityを同期するだけではネットワーク間のオブジェクトの動きが変わってしまう可能性があります。

今回のように、物体の衝突による動きが直接ゲーム性に関わる場合、PC間の挙動の違いでボールの飛ぶ方向が変わるのは困ります。座標を送って補正したとしても、動きが不自然になることが予想されます。そのため、Commandsで操作を送ってサーバーがまとめて処理する方式にしました。

Rigidbodyのvelocityを使えば、各PC上で座標計算ができるので動きを綺麗にすることができます。弾などの単純な軌道で、衝突時に跳ね返る必要がないようなオブジェクトの場合は、Rigidbodyのvelocityを同期する手法は使えると思います。どのような動きをさせたいかで採用する技術が変化してくるので、研究のし甲斐があると思います。今回と同じ内容のものを、PUNのvelocity同期で実装したらどうなるかも見てみたいところです。

参考URL

www.photonengine.com

assetstore.unity.com

CameraPlayのChromaticalの効果を出し続ける

CameraPlayは画面を揺らしたり、波紋を広げたりと、印象的な演出をするのにとても助かるアセットです。PostProcessingと組み合わせると素晴らしいです。

f:id:am1tanaka:20190711202054p:plain
CameraPlay - Chromatical

assetstore.unity.com (面倒なのでアフィリエイトは設定してません^^;)

使える演出の数々はUnity AssetStoreまとめさんやコガネブログさんの記事をご覧ください。

www.asset-sale.net

baba-s.hatenablog.com

1週間ゲームジャムで投稿した跳ね玉で利用したところ演出を評価してくださる方が増えまして、YOKETORU改2019でも利用して同様なお声を頂戴しました。効果を実感しています。

そんなCameraPlayですが、思った通りの設定がなかったりします。例えば古いモニターのようなかっこいい画面にできるChromaticalの使い方は以下の2通りです。

// time秒間で、演出して消える
CameraPlay.Chromatical(float time);

// 3秒間で、演出して消える
CameraPlay.Chromatical();

公式リファレンス

演出する秒数を変えられはしますが、シーン中ずっと効果を持続させるための設定がありません。今回はエンディングで利用したのですが、最初から最後までずっとこれになって欲しいのです。ということで改造したのでやりかたをご紹介します。

目次

準備

実行環境は以下の通りです。

  • Windows10
  • Unity2019.1.5
  • Visual Studio 2019 Community Edition
  • CameraPlay 1.3.2

空の3Dプロジェクトを作成して、Camera Playをインポートしました。Camera Play > Example > ExampleSceneシーンにサンプルシーンがあるので、それをもとに動作環境を構築しました。

  • Camear_GUIを無効にして、新しい空のゲームオブジェクトを作成して、ChromaticalTestというスクリプトを作成してアタッチしました。以下のような感じになります

f:id:am1tanaka:20190711133710p:plain
動作環境の作成

まずは現状のまま動作させてみます。

  • 作成したChromaticalTestスクリプトをダブルクリックしてエディターで開きます
  • Start()メソッドを以下のようにします
    void Start()
    {
        CameraPlay.Chromatical();        
    }

Playすると、3秒間で効果が表れて消えるのが確認できます。

f:id:am1tanaka:20190711202702g:plain
Chromaticalデフォルト動作

使い方はめちゃくちゃ簡単です。

効果を維持するための機能を追加する

CameraPlay_ChromaticalクラスのプロパティーTimer0.5にすれば、常時効果が常に続くようになります。解説は後述しています。もともとはprivateなので、これをpublicにして公開します。

  • ProjectウィンドウからCamera Play > Scriptsフォルダーを開いて、CameraPlay_Chromaticalをダブルクリックして、エディターで開きます
  • 20行目付近のTimerの宣言を、以下のようにpublicに変更します
// 20:
    [HideInInspector] public float Timer = 1f;

あとは利用する側で、Chromaticalを発動させたい間、上記のTimer0.5f-Time.deltaTimeを代入し続ければOKです。

  • 作成したChromaticalTestスクリプトをエディターで開きます
  • 以下のようにします
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChromaticalTest : MonoBehaviour
{
    CameraPlay_Chromatical CP = null;

    void Update()
    {
        if (CP == null)
        {
            CameraPlay.Chromatical();
            CP = CameraPlay.CurrentCamera.GetComponent<CameraPlay_Chromatical>();
        }
        if (CP != null)
        {
            CP.Timer = 0.5f - Time.deltaTime;
        }
    }
}

以上で完了です。Playすると、常にChromaticalがかかり続けます。

f:id:am1tanaka:20190711203020g:plain
ずっとChromaticalが持続

参考 CameraPlay_Chromaticalクラスを読む

参考までに、改造に至った手順を書いておきます。

まずは、どのようなコードで動いているのか覗いてみます。

  • Visual Studioで開いているChromaticalTestスクリプトに追加したコードのChromatical()の部分を右クリックして、定義へ移動を選びます
  • CameraPlayソースコードが開いて、Chromatical()メソッドの中身が確認できます。やっているのは以下の処理です
    • CurrentCameraが無効な時、現在のメインカメラを設定
    • カメラにCameraPlay_Chromaticalコンポーネントをアタッチ
    • アタッチしたCameraPlay_ChromaticalインスタンスDurationプロパティに、演出する秒数(デフォルトなら3)を代入

以上です。あとは、CameraPlay_Chromaticalクラス側が演出をしているということになるので、クラスの中身を見てみます。

  • どれでもよいので、ソースコード上のCameraPlay_Chromaticalの部分を右クリックして、定義へ移動を選びます
  • CameraPlay_Chromaticalクラスのファイルが開くので目を通します
  • OnRenderImage()メソッド内で演出の管理をしているのが確認できます

ざっくりと、以下のような感じです。

  1. シェーダーが有効か(nullじゃない)を確認
  2. TimeXは1から始めて、経過秒を足して、100を超えたら0に戻しています。シェーダーに渡しているようですが、他のパラメーターに関与してないので、ひとまず無視
  3. Timerは、経過時間です。開始直後が0Duration秒経過すると1になるようになっています
  4. _Fadeは、Timerをそのまま代入しているだけです
  5. _Fade0から1に変化する間に、fresultにアニメーションカーブを利用して、滑らかに0から1、そしてまた1から0と変化する値を代入しています
  6. fresultの値を、フェードのパラメーターとしてシェーダーに渡しています

以上から、常にfresult1になるようにすればよいことが分かります。

方法としては、常時Chromaticalとなる別のクラスを新設することが考えられますが、面倒なので単にCameraPlay_ChromaticalTimerを公開して、そこに値を代入する方法を採りました。

まとめ

Chromaticalをずっと適用できるようになりました。CameraPlayは綺麗に作られているのでソースコードを追いやすいと思います。表現の幅を拡張する一助になれば幸いです!

参考URL

Unity2019のWebGLビルドのエラー対策

f:id:am1tanaka:20190628215704p:plain

UnityのWebGLビルドではあれこれやられていましたが、ついに最後の砦を攻略できました。既知の問題はニムサイトさんに詳しいです。

nimushiki.com

日本語が含まれず、保存先が自分のドキュメント下で、最近のUnityであれば大体は問題ないのですが、学校の半数以上のマシンでビルドが通っていませんでした。エラーはil2cpp.exeがなんちゃらかんちゃら、というやつです。これの原因と対処方法です。

(2019/6/30 エラーの例を追記)

目次

エラーメッセージ

この問題で発生するエラーは以下のようなものです。この後にまだ2つほど続きます。冒頭のエラーに以下のようなil2cpp.exeが含まれていたら、この問題である可能性があります。

Failed running C:\Program Files\Unity\Hub\Editor\2019.1.5f1\Editor\Data\il2cpp/build/il2cpp.exe --convert-to-cpp --dotnetprofile="unityaot" --compile-cpp --libil2cpp-static --platform="WebGL" --architecture="EmscriptenJavaScript" --configuration="Release" --outputpath="C:\Users\student\Documents\webgl\Assets /../Temp/StagingArea/Data\Native\build.bc" --cachedirectory="C:\Users\student\Documents\webgl\Assets..\Library/il2cpp_cache" --compiler-flags="-Oz -DIL2CPP_EXCEPTION_DISABLED=1 " --emit-method-map --additional-libraries=(中略) --profiler-report --map-file-parser="C:/Program Files/Unity/Hub/Editor/2019.1.5f1/Editor/Data/Tools/MapFileParser/MapFileParser.exe" --directory=C:/Users/student/Documents/webgl/Temp/StagingArea/Data/Managed --generatedcppdir=C:/Users/student/Documents/webgl/Temp/StagingArea/Data/il2cppOutput

原因

最初にWebGLビルドをする際に、以下のようなnode.exeがブロックされているという警告が表示されます。

f:id:am1tanaka:20190628212408p:plain
node.exeをブロックしたとの警告

この時に、うっかりキャンセルをクリックしたのが原因です。これにより、node.exeはブロックするものとWindowsが理解してしまうため、それ以降のビルドは無条件にエラーになっていたのでした。

ということで、手動でnode.exeの動作を許可すれば解決します!!

確認したバージョン

  • Unity2019.1.5
  • Windows10

解決手順

操作には、管理者権限が必要です。

  • Windowsメニューから設定を開きます

f:id:am1tanaka:20190628211857p:plain
設定を開く

  • 更新とセキュリティをクリックします

f:id:am1tanaka:20190628212603p:plain
更新とセキュリティをクリック

  • Windowsセキュリティをクリックします

f:id:am1tanaka:20190628212705p:plain
Windowsセキュリティ

f:id:am1tanaka:20190628212755p:plain
ファイアウォールとネットワーク保護

f:id:am1tanaka:20190628212841p:plain
ファイアウォールによるアプリケーションの許可

  • 設定の変更ボタンをクリックします

f:id:am1tanaka:20190628213034p:plain
設定の変更

  • Nキーを押して、Node.jsの設定を見つかった場合
    • 詳細をクリックします

f:id:am1tanaka:20190628214132p:plain
詳細を確認

  • 対象のUnityのバージョンのフォルダーかを確認します。以下のようになっていれば、Unity2019.1.5でWebGLビルドが成功するはずです

f:id:am1tanaka:20190628214201p:plain
パスを確認

  • 対象のUnityフォルダーのNode.jsの前のチェックが外れていたら、チェックしてください

f:id:am1tanaka:20190628214352p:plain
チェック

Node.jsの設定があった場合、ここまでの設定でビルドが通るようになります。

Node.jsの設定がなかったり、パスが別のバージョンのものしかなかったりする場合は、続けて設定します。

Node.jsの設定がない場合

  • 別のアプリの許可ボタンをクリックします

f:id:am1tanaka:20190628213124p:plain
別のアプリの許可

  • 参照をクリックします

f:id:am1tanaka:20190628213833p:plain
アプリの追加の参照

  • WebGLビルドで使いたいUnityのインストールフォルダー内のnode.exeを指定します。例えば、デフォルトの場所にインストールしたUnity2019.1.5を指定したい場合は以下の通りです
    • C:\Program Files\Unity\Hub\Editor\2019.1.5f1\Editor\Data\Tools\nodejs\node.exe
  • 追加ボタンをクリックします

以上で完了です。以下のようにNode.jsの設定が追加されていればOKです。

f:id:am1tanaka:20190628213953p:plain
登録完了

この設定をしてもエラーが出る場合

既知の問題が原因と思われます。以下を確認してみてください。

  • Unityのプロジェクトが、日本語を含むパスの下にあったら、全て半角英数のみのパスに移動する
  • Unityのプロジェクト内に、日本語ファイルがあったら、全て半角英数にリネームする
  • Unityのプロジェクトが、自分のドキュメントフォルダーの外にあったら、ドキュメントフォルダーの下に移動する
  • Unityを一度閉じて、プロジェクトを開き直してビルドしなおす(結構これで成功することがあります)

なんだかんだで日本語パスと日本語ファイル名が一番のネックです。PC用ビルドなどは通ってしまうので、安心しているとWebGLでコケますのでご注意を!!

参考URL

nimushiki.com