- 投稿日:2021-03-05T20:35:13+09:00
EC2でWebRTC Ayameサーバー立ち上げ
この記事について
3令和12月頃で、Ayame Signaling ServerをMRTK-WebRTC-Ayameのため利用するつもりでしたが、残念ですが接続が全然できなかった。いつもこんな感じで、結構悔しくって、Tarukosuさんのコードを他のdllで試しても全く結果が変わりませんでした。もしかしたら、接続ができる前にTimeout短い過ぎて、接続が切れる可能性を考えました。それで、自分のサーバーでAyame Signaling Serverを立ち上がったほうがいいじゃないかなと思っていました。
環境構築
アプリ
Windows 10
AWSアカウント
Unity 2019.4.xWebRTC Signalling Server Ayame
Ayame YAMLファイル - WebRTCが繋がりにくい時、このファイルが重要です。設定
インスタンスと接続して、RDPを選んでください。
サーバー立ち上げ
Windows 10でリモートデスクトップ接続アプリを開いてください。
オプションの表示を開いてください。接続設定はRDPファイルから開いてください。
接続前に、ファイル転送をできるためローカルソースを開いてください。
ローカルデバイスとリソースで詳細を開いてください。後、ドライブでチェック入れてください。
終わったら、接続してください。
Ayameサーバーをダウンロードしてください。EC2のOSを対応できる、一つ選択してください。今回の記事はWindows OSを使いましたので、ayame_windows_amd64.exe.gzをダウンロードしました。
exeファイルを引き抜くして、YAMLファイルと一緒のフォルダーで入れました。
EC2でエクスプローラーのNetworkを開いて、tsclientでダウンロードしたAyameサーバーのフォルダーをEC2へコピーしてください。今回の記事でAyameサーバーのフォルダーはデスクトップへコピーしました。
Command Prompt (CMD)を開いて、Ayameフォルダーへ移動して、ayame_windows_amd64.exeを発生します。
繋がりにくい時、EC2のFirewallを消してください。まだ繋がりにくい場合は、yamlファイルでwebhook_request_timeoutをちょっと上げてください。
Unityプロジェクトの設定
- 新しいいUnityプロジェクトを作成してください。このサンプルで、Unity 2019.4.16を使います。
- こちらのリンクで、Tarukosuさんが説明したプロジェクト設定を従いください。
- 投稿日:2021-03-05T20:19:16+09:00
Unity投影シャドウ効果をどのように実現するか
序文
Unityエンジンに付属するシャドウ機能は優れたShadowMapです。この記事では、プロジェクターを使用してシャドウを生成する、シャドウの別の実現法を紹介します。
一.機能の実現
1.メイン光源のシャドウ投影をオフにします
上の図に示すように、シャドウ投影を使用する場合は、メイン光源をオフにしてシャドウを投影する必要があります。2.Projectorのセットアップ
図に示すように、Projectorコンポーネントを追加してから、ProjectorのGameObjectの方向を調整します。
3.コアコードを書く
上の図に示すように、ProjectorShadowスクリプトを書きます。
3.1最初にRenderTextureを作成します
RenderTexturemShadowRT = new RenderTexture(mRenderTexSize, mRenderTexSize, 0, RenderTextureFormat.R8); mShadowRT.name = "ShadowRT"; mShadowRT.antiAliasing = 1; // アンチエイリアスをオフにする mShadowRT.filterMode = FilterMode.Bilinear; mShadowRT.wrapMode = TextureWrapMode.Clamp; // wrapmodeをClampに設定する必要があるまず、このRenderTextureの形式はR8であり、この形式で作成されたテクスチャのメモリ占用が最小であることに注意してください。
実行時にテクスチャをチェックします。
2048×2048のテクスチャを作成する場合、メモリは4MBしかありません。次に、AntiAliasingを1に設置します。つまり、アンチエイリアスを無効になさせます。WrapModeをClampに設置します。最後に実行する時のパラメータを次の図に示します。図中のDepthBufferに対して、コードは設定されていませんが、デフォルトでオフになっています。この種のプロジェクションシャドウによって作成されたRenderTextureは、DepthBufferを使用する必要がないためです。オフにする必要があります。
3.2 Projectorの設置
projector初期化mProjector = GetComponent<Projector>(); mProjector.orthographic = true; mProjector.orthographicSize = mProjectorSize; mProjector.ignoreLayers = mLayerIgnoreReceiver; mProjector.material.SetTexture("_ShadowTex", mShadowRT);ここでは、主にプロジェクターを正投影に設定することです。同時に、次の図に示すように、プロジェクターのサイズを設定し、プロジェクターの無視レイヤーを設定します。
プロジェクターのサイズを23に設定します。無視レイヤーをUnitに設定して、つまり、ゲームで作成されるすべての単位です。3.3投影Cameraの作成
camera初期化mShadowCam = gameObject.AddComponent<Camera>(); mShadowCam.clearFlags = CameraClearFlags.Color; mShadowCam.backgroundColor = Color.black; mShadowCam.orthographic = true; mShadowCam.orthographicSize = mProjectorSize; mShadowCam.depth = -100.0f; mShadowCam.nearClipPlane = mProjector.nearClipPlane; mShadowCam.farClipPlane = mProjector.farClipPlane; mShadowCam.targetTexture = mShadowRT;作成したCameraのClearFlagsをクリアカラーに設定します。
CameraのクリーニングカラーBackgroundColorを黒に設定します。
Cameraも正投影であるはずです、正投影のサイズもProjectorのサイズと同じである必要があります。
CameraのDepthを-100に設定します。つまり、メインCameraよりも早くレンダリングされることを意味します。
Cameraの近裁断面と遠裁断面の設置は、プロジェクターの近裁断面と遠裁断面の設置と同じであります。
CameraのTargetTextureは、作成されたRenderTextureに設定されます。つまり、CameraはすべてのオブジェクトをこのRenderTextureにレンダリングします。
3.4レンダリング方法の選択
これは、この記事の鍵と感じます。いくつかの記事を参考にして、最後に2つの方法をまとめました。個人的には、その中のCommandBufferを使用する方法は、実際のプロジェクトに適していて、レンダリング効率を上げることができると思います。
まずはコート実現をご覧ください。
private void SwitchCommandBuffer() { Shader replaceshader = Shader.Find("ProjectorShadow/ShadowCaster"); if (!mUseCommandBuf) { mShadowCam.cullingMask = mLayerCaster; mShadowCam.SetReplacementShader(replaceshader, "RenderType"); } else { mShadowCam.cullingMask = 0; mShadowCam.RemoveAllCommandBuffers(); if (mCommandBuf != null) { mCommandBuf.Dispose(); mCommandBuf = null; } mCommandBuf = new CommandBuffer(); mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf); if (mReplaceMat == null) { mReplaceMat = new Material(replaceshader); mReplaceMat.hideFlags = HideFlags.HideAndDontSave; } } }3.4.1 CommandBufferを使用しない場合
主には下記の2行のコード
mShadowCam.cullingMask = mLayerCaster; mShadowCam.SetReplacementShader(replaceshader, "RenderType");CameraがどのレイヤーのGameObjectをレンダリングしますか、またはCameraレンダリングがどのShaderを使用して取り替えますかを設置します。
次の図に示すように、Cameraは作成されたすべてのUnitのみをレンダリングします。
Cameraが使用したShaderには、一つの普通な頂点/フラグメントShaderを使用して処理できます。Shader "ProjectorShadow/ShadowCaster" { Properties { _ShadowColor("Main Color", COLOR) = (1, 1, 1, 1) } SubShader { Tags{ "RenderType" = "Opaque" "Queue" = "Geometry" } Pass { ZWrite Off Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag struct v2f { float4 pos : POSITION; }; v2f vert(float4 vertex:POSITION) { v2f o; o.pos = UnityObjectToClipPos(vertex); return o; } float4 frag(v2f i) :SV_TARGET { return 1; } ENDCG } } }このShaderは白を出力するためのものであり、同時に書き込みの深度を閉じます。裁断は使用しません。
3.4.2 CommandBufferを使用する場合
主に次のコード
mShadowCam.cullingMask = 0; mShadowCam.RemoveAllCommandBuffers(); if (mCommandBuf != null) { mCommandBuf.Dispose(); mCommandBuf = null; } mCommandBuf = new CommandBuffer(); mShadowCam.AddCommandBuffer(CameraEvent.BeforeImageEffectsOpaque, mCommandBuf); if (mReplaceMat == null) { mReplaceMat = new Material(replaceshader); mReplaceMat.hideFlags = HideFlags.HideAndDontSave; }CameraのCullingMaskを0に設定し、つまり、Cameraはオブジェクトを何もレンダリングせず、すべてのレンダリングはCommandBufferによって実行されます。次に、CommandBufferを作成し、それをCameraのCommandBufferリストに追加します。
CommandBufferレンダリングに必要なMaterialを作成します。Materialが使用するShaderは上記の「ProjectorShadow / ShadowCaster」です。
フレームごとに更新すると、
private void FillCommandBuffer() { mCommandBuf.Clear(); Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam); List<GameObject> listgo = UnitManager.Instance.UnitList; foreach (var go in listgo) { if (go == null) continue; Collider collider = go.GetComponentInChildren<Collider>(); if (collider == null) continue; bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds); if (!bound) continue; Renderer[] renderlist = go.GetComponentsInChildren<Renderer>(); if (renderlist.Length <= 0) continue; // 目に見えるrenderはありますか // あれば、GameObject全体をレンダリングします bool hasvis = false; foreach (var render in renderlist) { if (render == null) continue; RenderVis rendervis = render.GetComponent<RenderVis>(); if (rendervis == null) continue; if (rendervis.IsVisible) { hasvis = true; break; } } foreach(var render in renderlist) { if (render == null) continue; mCommandBuf.DrawRenderer(render, mReplaceMat); } } }ゲームで作成されたすべての単位をトラバースします。最初に、視錐台を通して投影Cameraに表示されない単位を削除します。
主に次の2行のコードです。Plane[] camfrustum = GeometryUtility.CalculateFrustumPlanes(mShadowCam); bool bound = GeometryUtility.TestPlanesAABB(camfrustum, collider.bounds);投影Cameraの視錐台を計算して取得し、関数を介して、単位のColliderは視錐台範囲にあるかどうかを判断します。これて、現在のフレームカメラが見えるUnitを発見できます。
次に、次の判断を行います。
Renderer[] renderlist = go.GetComponentsInChildren<Renderer>(); if (renderlist.Length <= 0) continue; // 目に見えるrenderはありますか // あれば、GameObject全体をレンダリングします bool hasvis = false; foreach (var render in renderlist) { if (render == null) continue; RenderVis rendervis = render.GetComponent<RenderVis>(); if (rendervis == null) continue; if (rendervis.IsVisible) { hasvis = true; break; } }視錐台内のUnitに対して、すべてのRenderをトラバースして、このRenderで行えるかどうかを判断します。このUnitのRenderは一つしか見えない場合、この単位をレンダリングします(ここでRenderは見えるかどうかを考えせず、各Renderを個別にレンダリングします。なぜなら、レンダリングされたUnitの完成性を確保したい、部分的にレンダリングしたくないということです。全体がレンダリングされるか、レンダリングされないかのどちらかです)。
したがって、問題は、Unitが見えるのはいつか、見えないのはいつか、どうやって知るのかということです。 次のコードを見ることができます。
private bool mIsVisible = false; public bool IsVisible { get { return mIsVisible; } } void OnBecameVisible() { mIsVisible = true; } void OnBecameInvisible() { mIsVisible = false; }このスクリプトは各レンダーの下でハングします。このRenderがカメラに見られると、UnityエンジンはOnBecameVisible関数をコールします。見られないと、OnBecameInvisible関数をコールします。
現在、このDemoでは、投影CameraがCommandBufferを使用する場合、Cameraはオブジェクトを何もレンダリングしません。Main Cameraのみが全てのRenderをレンダリングします。ですから、Visibleが見える時に、このRenderは画面に表示されますが、Visibleが見えない時には表示されないと理解できます。
まとめをします。フレームごとに更新する時、最初に投影Cameraを介してレンダリングできるUnitを選出して、このオブジェクトは同時にMain Cameraに見られるかどうかを判断します。両方満たされている場合は、mCommandBuf.DrawRenderer(render、mReplaceMat);関数を使用して、作成されたRenderTextureにオブジェクトをレンダリングします。
3.5プロジェクターShaderはどのように実現されていますか?
投影Shaderは実際に、一つのシャドウを受けるShaderであります。具体的な実現方は下記のように、
ZWrite Off ColorMask RGB Blend DstColor Zero Offset -1, -1 v2f vert(float4 vertex:POSITION) { v2f o; o.pos = UnityObjectToClipPos(vertex); o.sproj = mul(unity_Projector, vertex); UNITY_TRANSFER_FOG(o,o.pos); return o; } float4 frag(v2f i):SV_TARGET { half4 shadowCol = tex2Dproj(_ShadowTex, UNITY_PROJ_COORD(i.sproj)); half maskCol = tex2Dproj(_FalloffTex, UNITY_PROJ_COORD(i.sproj)).r; half a = shadowCol.r * maskCol; float c = 1.0 - _Intensity * a; UNITY_APPLY_FOG_COLOR(i.fogCoord, c, fixed4(1,1,1,1)); return c; }Vertで、o.sproj = mul(unity_Projector, vertex);で投影位置を計算して取得します。Fragで、UNITY_PROJ_COORD(i.sproj)を介して投影テクスチャ座標を計算します。次に、最終的な色をブレンドします。
下図のように、
Maskマップを追加しました。このMaskマップを使用すると、シャドウ辺縁をより適切に処理でき、シャドウ辺縁はフェードインおよびフェードアウトの効果があります。4.ゲームを実行します
効果図を以下に示します。同じ観点で、CommandBufferを使用してレンダリングするかどうかを切り替えます。同じ効果の下で、CommandBufferを使用したバッチの方が優れており、それに応じてパフォーマンスも向上します。(上の画像はCommandBufを使用しておらず、下の画像はCommandBufを使用しています)
CommandBufレンダリングを使用していません場合
CommandBufレンダリングを使用する場合
二.プロジェクトDemoのアドレス
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2021-03-05T20:05:34+09:00
アニメーションボーンノードのバッチ処理
今回の主な話題:アニメーションボーンノードのバッチ処理、パーティクルシステムのクラッシュ、iOSでカスタムShaderが異常する、エディターで100フレームに達する可能の場合に実機で常に30フレーム未満状況、多言語でのテキストコンポーネントのサイズとテキストの長さの適応。
アニメーション
Q1: アニメーションシステムでOptimize GameObjectオプションをオンにすると、不要なボーンが消えます。ボーンの下に多くの特殊効果のハンギングポイントをぶら下げました。また、 Extra Transforms to Exposeを介してハンギングポイントを公開します。しかし、モデルほどに手動的に選択することはめんどくさいすぎで、何かバッチ処理方法はありますか?
①
これは、Asset Postprocessorを介して、インポートする時にモデルをトラバースすることができ、特定プレフィックスに一致するノードパスをModel ImporterのExtra Exposed Transform Paths属性に追加して、Optimize Transform Hierarchyを引用してノード最適化効果を実現します。そう理解していますが、正しいかどうかがわかりません。
②
補足します。
ノード名がもう知っている場合、直接割り当てることができます。つまり、常にOnPreprocessModelを使用します。具体的なルールはプロジェクトチーム内で合意したら大丈夫です。
public static string[] NodeNames = new string[] { "node_head", "node_arm", "node_leg", }; void OnPreprocessModel() { ModelImporter importer = assetImporter as ModelImporter; importer.optimizeGameObjects = true; //以下のノードが実際のボーンにない場合、エラーが報告される場合がありますが、使用には影響しません。 importer.extraExposedTransformPaths = EWEquipmentsBase.nodeNames; }③
上記の両方の答えは有効ですが、もっと簡単な方法があります。
最初に、Extra Transforms to Expose大体の原理について話しましょう。(絶対的に正しいではありません)UnityEditor.dll関連の実現を観察したら、以下のことはわかりやすいです。
1)開発者がExtraTransformstoExposeを変更した後、UnityはfbxのmetaファイルのextraExposedTransformPaths属性に情報を保存します。
2)Unityがfbxファイルに基づいてPrefabを生成すると、metaファイルのextraExposedTransformPathsの各Stringに基づいて対応する名前の空のGameObjectを作成します。親子関係を考慮する必要はありません。
3)実行時に、(推測)PrefabがInstantiateされた後、AnimatorコンポーネントがAwakeする時にAnimator.Rebind方法をトリガーします。この方法はTransformの子ノードをトラバースし、子ノードのnameをAvatarにあるボーン情報と比較してから、自動的にノードをバインドします。
これも、Optimizedした後にAnimatorコンポーネント上にAvatarが必ずある原因であります(OptimizedしていないGameObjectはAnimatorコンポーネントのAvatarを空にさせらあれます)。Avatarをさらに調査したい場合は、InspectorをDebugモードに変更してチェックすることはできます。
上記の分析に基づいて、このプロセスを模倣し、直接にOptimizedした後のPrefabの下に空のGameObjectを作成し、空のGameObjectの名前を合意されたボーンハンギングポイント名に変更したら、ExtraExposedTransformPathsと同じ機能を実現できます。
さらに、Animator.Rebindメソッドを使用して、実行時にハンギングポイントを動的に追加および削除機能を実現できます。 これの利点は、特殊効果担当と計画担当がPrefabを繰り返し変更する必要がないことです(もちろん、オフラインにすることもできます)。
二つ目の利点は、Prefabに全てのハンギングポイントをバインドする必要はないことです。配置テーブルで実際に使用されるハンギングポイントのみを作成し、ハンギングポイントGameObjectをオブジェクトプールに入れてリサイクルしたら、キャラクターが多い、またはデフォルトでハンギングポイントが多い場合には、メモリを最適化することもできます。欠点は、Animator.Rebindに時間がかかるため、Instantiateでコールすることを考慮できることです。
動的にハンギングポイントをバインドする例、
private static readonly string[] s_BindPoints = new string[] { "node_head", "node_arm", "node_leg", }; private Animator m_Animator = null; private void Awake() { m_Animator = GetComponent<Animator>(); Assert.IsNotNull(m_Animator); foreach (var bindPoint in s_BindPoints) { GameObject bindPointObj = new GameObject(bindPoint); bindPointObj.layer = gameObject.layer; bindPointObj.transform.SetParent(transform); } m_Animator.Rebind(); // important! }
クラッシュ
Q2: 低確率のランダムクラッシュ問題を皆さんに聞きたいです。クラッシュのスタック情報は次のとおりです。
インターネットで「Abortmessage: ‘* Assertion at mini-arm.c:2634、condition `pdata.found == 1’」を検索しました。誰かがDLLに関連していると言われており、Androidプラットフォームではローカルnative codeに関するはずです(現在のプロジェクトでliblua53.soと他のluaに関する.soライブラリを使用しています)。mini-arm.cのソースコードを見ました、確かにmono_code_managerに関するものを処理しています。誰か経験のある友達はいませんか?経験を教えてください。
目前、さまざまなパラメータを使用して.soを再コンパイルしてテストしようとしています…同時に、IL2CPPを採用してポジショニングを試す予定もあります。
先週、私たちのプロジェクトはインターネットで7日間の小規模(約2000人のプレイヤー)クローズドテストを実施しました。この期間中、クラッシュ問題の処理過程と原因は依然として非常に深刻でした。
以前、「libmono.so “Abort message: Assertion at mini-arm.c:2634, condition `pdata.found == 1’ not met” 」の低確率のランダムクラッシュ問題は私をよく困らせました。主に、この問題はmonoでクラッシュしますから、クラッシュのスタック情報はlibunity.soやlibmain.soと完全に関係ありません。Unityに基づくロジックレイヤーコードももちろんです。クローズドテストの終わり(11月30日)に迫って、28日にmonoに簡単な処理を一回しました。monoコードmini-arm.c:2634のアサーションg_assert(pdata.found == 1);をコメントアウトし、一般的なエラーメッセージに変えて出力し、コンパイル後にプロジェクトに直接送信します(この変更は無害であります)。そしてプロジェクトにあるスタックポイントの最適化を開始しましたが、この変更を追跡し続けませんでした。その時はそれをただの小さな確率の出来事と考えました。
11月29日にリリース版を提出した後、夕方にフィードバックされた、このバージョンのクラッシュ率は少し高いと感じたが、実際の統計はなかったとのことでした。このフィードバックをもらった後、注意を払わず、他の問題に対処し続けました(本当にその時にbuglyを見に行かなかったことを後悔しています)。
クローズドテストは11月30日の正午に正式に開始されました。午後4時から5時以降、同僚から「クラッシュ率が少し高い。平日のテストにはクラッシュしないけど今日はもう2〜3回クラッシュしました。」というフィードバックがくれました。中の一人が「15分自動戦闘すると必ずクラッシュする。」と言いました。この問題について聞いたとき、緊張してbuglyを急いで調べたところ、クラッシュ率が7.x%に達し、通常のテストの2.x%の統計よりもはるかに高いことがわかりました。見てみると、すべてがパーティクルシステムのクラッシュに集中していることがわかりました。
これらのクラッシュを確認した後、過去2日間の最適化を思い出しました。ジャム問題の最適化したところ、パーティクルシステムコンポーネントParticleSystemにローカルキャッシュ引用を行いました。スキル特殊効果を再生するたびにGetGetComponentsInChildren(true)を実行する必要はありません。同時に、スキル特殊効果パーティクルシステムが停止しているときにstop()をコールし、pause()に変更します(スキル特殊効果は再生後にpoolにリサイクルされるので、pause()をコールしても残差はありません。この時、データを解放する必要はありません、シーンを切り替えると実際に特殊効果のデータを解放します)。このとき、クラッシュスタックからの情報を見て、最初に考えた変更は、stop()がpauseを変更したことによって引き起こされたクラッシュであります。プログラマーにiOSで自動戦闘してこの問題を再現することを頼みました。やっぱり2回続けて再現されました。8〜12分以内に必ずクラッシュし、クラッシュスタックはbuglyでの統計と同じです。
このとき、すぐにiOSで直接コンパイルテストを変更し、pause()をstop()に復元して、テストを続行しました…予期しないことは、問題がまだ存在することです。自動戦闘すると15分内に必ずクラッシュします。さらに考えると、stop()は当時のパーティクル更新とパーティクル放出だけを停止しますが、データは残っています。コードと変更し続き、stop()がコールされた後、すぐにclear()をコールしてテストします。気のめいるのは、まだ自動戦闘すると15分内に必ずクラッシュします。特殊効果の再生コードの上下ロジック処理を詳しく確認しましたが、問題は見つかりませんでした。通常のクラッシュ率はそれほど高くありません。思い出して、この2日間で残っている変更は、ParticleSystemローカル引用キャッシュを追加することだけなので、直接にiOSで「再生した後にすぐローカル引用を空に置く」に変更し、コンパイルして実機テストに同期します。30分、または40分以上テストしていて、再びクラッシュすることはありません。pause()コールに戻しても、クラッシュしませんでした。
この結果には少し驚きました。考えもしなかったので、すぐに外部ネットワークのホットアップデートに修正しました…やっぱり翌日に外部ネットワークのクラッシュ率は低下しましたが、まだ存在しています。しかし、心の中で問題はどこにあるのはもうわかっています。UI特殊効果の再生はまだ処理していませんから(スキル特殊効果とUI特殊効果は分離しており、スキルには高配置と低配置の設定があり、初期化解析は違います)、同じようにUI特殊効果を変更した後、クラッシュ率は1-2%まで低下し続きます。残ったクラッシュは依然としてUIパーティクルシステムに関連しており、主に一部のUI機能ビジネスレイヤーが特殊効果のリサイクルのインターフェイスをアクティブにコールしなかったことが原因です。
今回のクローズドテストが終わった後、クラッシュ情報を振り返ると、最も厄介なmonoクラッシュ問題は28日に終了しました。つまり、28日にmonoコードを変更したから、「libmono.so “Abort message: ‘* Assertion at mini-arm.c:2634, condition `pdata.found == 1’ not met” 」というスタックデータでもうクラッシュしなくなりました。顧みると、以前の低確率のクラッシュ問題は、UIパーティクルシステムによって引き起こされました。一部のビジネスレイヤロジックは、リリースインターフェイスをアクティブにコールせず、mono-arm.cのクラッシュラインg_assert(pdata.found == 1)でスタック情報が上位層への転送を中断します。
Unityのバージョンは5.6.5P4であります。キャッシュパーティクルシステムコンポーネントがすぐにリリースされない原因は不明です。Unityの根本的なバグであるか、パーティクルシステムコンポーネントParticleSystemロジックへの上下コールに問題がある可能性があります。
Shader
Q3: カスタムShaderはAndroidスマートフォンでは正常ですが、iOSでは異常であり、多くのエラーが表示され、アセットがすべて黒くなります。
①
1行目は、不完全を示しています。頂点シェーダーがないことと言っているはずです。
2行目は、Passが削除されたことを示しています。
3行目は、使用可能なSubshadersまたはFallbacksがないことを示しています。まとめすると、Shaderは空のShaderであり、使用できません。そのため、取り替えを行いました。その表現は、全て黒くなり、マテリアルが失ってではありません。
ShaderForgeを使用したことはありませんが、アーティストさんがShaderForgeからエクスポートしたShaderを見ることはあり、プラットフォームのコンパイルオプションが含まれています。このコンパイルオプションが間違っている可能性があり、ShaderForgeのコンパイルオプション設置を確認しなければなりません。
②
まずはiOSプラットフォームに切り替え、間違ったShaderを選択すると、「Shader is not supported on this GPU」を表示可能性があります。「Compile and Show Code」をクリックして、「Compile errors generating this shader.」は表示するはずです。
この場合は、GraphicsAPIがサポートしていないFeatureを使用していることを意味します。
フレームレート
Q4: 最近、あるプロジェクトを実行しました。時間コストを確認したところ、ただ十何秒かかりましたが、実機では30フレームを超えることはできませんでした。コードにはApplication.targetframerateを制限することはありませんが、原因は何ですか?
30フレームの垂直同期制限がオンになっているかもしれません。60フレームに変更すること、垂直同期をオフにすること、または自分でApplication.targetframerateでフレームを制限することができます。
UI
Q5: 多言語で、Textコンポーネントサイズとテキスト長さの適応はどうやって行いますか?同じテキスト、同じフォントサイズ、異なる言語では、長さ(文字数)とテキストサイズ(ピクセルの幅と高さ)が異なります。テキストがTextコンポーネントの表示範囲を超えないようにするにはどうすればよいですか?視覚効果が合理的であることを確認する方法(テキストが小さすぎたり大きすぎたりせず、位置が偏りすぎないようにする)? 自動化させる方法とは何ですか?
まず、1種の言語に応じてテキストボックスの大きさを決めます。英語に基づいて計画することをお勧めします。漢字が2文字である場合があり、他の言語に翻訳すると長い文字列になります。 英語はもっと似合うものです。
1つの方法は、テキストボックスのサイズとBestfitを固定して、最小フォントと最大フォントを設定することです。Bestfitを展開しないと、このやり方は少し乱暴です。私たちの前のプロジェクトはそのような乱暴でありました。
もう1つの方法は、ContentSizeFitterを介して、1文字のサイズを計算し、自分が設置した最小と最大フォントに応じて、拡張スクリプトを使用して水平方向と垂直方向に動的に拡張することです。
一般的に、極端な状況から逃れることはできないので、このような非常に特殊な処理を行うためにスクリプトを拡張します。
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。
UWA公式サイト:https://jp.uwa4d.com
UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2021-03-05T19:48:12+09:00
AddressablesとCloud Content Delivery
Unity謹製のCDN
個人で使うには十分だと感じました。
当たり前ですがUnityとの相性が抜群です。導入手順
はりきって書こうと思ったら、ダッシュボードにすべてが書かれていました。
バッチファイルを用意する
以下のようなコマンドをucd.exeと同階層に置きました。
ucd auth login [ログインID] ucd config set bucket [Bucket ID] ucd entries sync [同期させるターゲットパス] echo n | ucd releases create -b [Bucket ID]バンドルのビルドスクリプトを書く
新規の場合
AddressableAssetSettings.BuildPlayerContent();Updateの場合
var aaSettings = AddressableAssetSettingsDefaultObject.Settings; if (aaSettings != null && aaSettings.BuildRemoteCatalog) { var id = aaSettings.profileSettings.GetProfileId("Default"); aaSettings.activeProfileId = id; string path = ContentUpdateScript.GetContentStateDataPath(false); if (System.IO.File.Exists(path)) { ContentUpdateScript.BuildContentUpdate(aaSettings, path); } }上記を組み合わせれば、ビルド後にバッチを走らせることができます。
- 投稿日:2021-03-05T11:54:02+09:00
[Unity3D] セーブシステム
1, Scriptフォルダを作成し、SavingSystemとSavingWrapperスクリプトを作成する
2, 各スクリプトに下記コードを書く
SavingWrapperusing UnityEngine; namespace RPG.Saving { public class SavingWrapper : MonoBehaviour { const string defaultSaveFile = "save"; private void Update() { if (Input.GetKeyDown(KeyCode.S)) { GetComponent<SavingSystem>().Save(defaultSaveFile); } if (Input.GetKeyDown(KeyCode.L)) { GetComponent<SavingSystem>().Load(defaultSaveFile); } } } }SavingSystemusing System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { print("Saving to " + saveFile); } public void Load(string saveFile) { print("Load to " + saveFile); } } }3, PersistentObjectsにSavingSystemオブジェクトを作成し、各スクリプトをコンポーネントする
セーブファイルを作成できるようにする
1, 下記スクリプトを追加する
SavingSystemusing System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { print("Saving to " + GetPathFromSaveFile(saveFile)); //変更 } public void Load(string saveFile) { print("Load to " + GetPathFromSaveFile(saveFile)); //変更 } //追加 private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }セーブとロードを実行する
1, 下記スクリプトを記述する
SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { Transform playertransform = GetPlayerTransform(); byte[] buffer = SerializeVector(playertransform.position); stream.Write(buffer, 0, buffer.Length); } } public void Load(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Loading from " + path); using (FileStream stream = File.Open(path, FileMode.Open)) { byte[] buffer = new byte[stream.Length]; stream.Read(buffer, 0, buffer.Length); Transform playertransform = GetPlayerTransform(); playertransform.position = DeserializeVector(buffer); } } private Transform GetPlayerTransform() { return GameObject.FindWithTag("Player").transform; } private byte[] SerializeVector(Vector3 vector) { byte[] vectorBytes = new byte[3 * 4]; BitConverter.GetBytes(vector.x).CopyTo(vectorBytes, 0); BitConverter.GetBytes(vector.y).CopyTo(vectorBytes, 4); BitConverter.GetBytes(vector.z).CopyTo(vectorBytes, 8); return vectorBytes; } private Vector3 DeserializeVector(byte[] buffer) { Vector3 result = new Vector3(); result.x = BitConverter.ToSingle(buffer, 0); result.y = BitConverter.ToSingle(buffer, 4); result.z = BitConverter.ToSingle(buffer, 8); return result; } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }シリアライズのための BinaryFormatter
1, SerializableVector3.csを作成する
2, 各スクリプトに下記コードを記述する
SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { Transform playerTransform = GetPlayerTransform(); BinaryFormatter formatter = new BinaryFormatter(); SerializableVector3 position = new SerializableVector3(playerTransform.position); formatter.Serialize(stream, position); } } public void Load(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Loading from " + path); using (FileStream stream = File.Open(path, FileMode.Open)) { Transform playerTransform = GetPlayerTransform(); BinaryFormatter formatter = new BinaryFormatter(); SerializableVector3 position = (SerializableVector3)formatter.Deserialize(stream); playerTransform.position = position.ToVector(); } } private Transform GetPlayerTransform() { return GameObject.FindWithTag("Player").transform; } private byte[] SerializeVector(Vector3 vector) { byte[] vectorBytes = new byte[3 * 4]; BitConverter.GetBytes(vector.x).CopyTo(vectorBytes, 0); BitConverter.GetBytes(vector.y).CopyTo(vectorBytes, 4); BitConverter.GetBytes(vector.z).CopyTo(vectorBytes, 8); return vectorBytes; } private Vector3 DeserializeVector(byte[] buffer) { Vector3 result = new Vector3(); result.x = BitConverter.ToSingle(buffer, 0); result.y = BitConverter.ToSingle(buffer, 4); result.z = BitConverter.ToSingle(buffer, 8); return result; } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }SerializableVector3using System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Saving { [System.Serializable] public class SerializableVector3 { float x, y, z; public SerializableVector3(Vector3 vector) { x = vector.x; y = vector.y; z = vector.z; } public Vector3 ToVector() { return new Vector3(x, y, z); } } }SaveableEntityの作成
2,各スクリプトに下記コードを記述する
SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, CaptureState()); } } public void Load(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Loading from " + path); using (FileStream stream = File.Open(path, FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); RestoreState(formatter.Deserialize(stream)); } } private object CaptureState() { Dictionary<string, object> state = new Dictionary<string, object>(); foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { state[saveable.GetUniqueIdentifier()] = saveable.CaptureState(); } return state; } private void RestoreState(object state) { Dictionary<string, object> stateDict = (Dictionary<string, object>)state; foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { saveable.RestoreState(stateDict[saveable.GetUniqueIdentifier()]); } } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }SaveableEntityusing System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Saving { public class SaveableEntity : MonoBehaviour { public string GetUniqueIdentifier() { return ""; } public object CaptureState() { print("Capturing state for " + GetUniqueIdentifier()); return null; } public void RestoreState(object state) { print("Restoring state for " + GetUniqueIdentifier()); } } }UUIDs生成
1, 下記スクリプトの追加
SaveableEntityusing System.Collections; using System.Collections.Generic; using UnityEngine; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { print("Capturing state for " + GetUniqueIdentifier()); return null; } public void RestoreState(object state) { print("Restoring state for " + GetUniqueIdentifier()); } private void Update() { if (Application.IsPlaying(gameObject)) return; print("Editing"); } } }SerializedFieldsの編集
1, 下記スクリプトを追加
SaveableEntityusing System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { print("Capturing state for " + GetUniqueIdentifier()); return null; } public void RestoreState(object state) { print("Restoring state for " + GetUniqueIdentifier()); } private void Update() { if (Application.IsPlaying(gameObject)) return; if (string.IsNullOrEmpty(gameObject.scene.path)) return; SerializedObject serializedObject = new SerializedObject(this); SerializedProperty property = serializedObject.FindProperty("uniqueIdentifier"); if (string.IsNullOrEmpty(property.stringValue)) { property.stringValue = System.Guid.NewGuid().ToString(); serializedObject.ApplyModifiedProperties(); } } } }SaveableEntitiesでのシリアライズ
1, 下記のコードを追加
SaveableEntityusing System.Collections; using System.Collections.Generic; using RPG.Core; //追加 using UnityEditor; using UnityEngine; using UnityEngine.AI; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { return new SerializableVector3(transform.position); //追加 } public void RestoreState(object state) { SerializableVector3 position = (SerializableVector3)state; //追加 GetComponent<NavMeshAgent>().enabled = false; //追加 transform.position = position.ToVector(); //追加 GetComponent<NavMeshAgent>().enabled = true; //追加 GetComponent<ActionScheduler>().CancelCurrentAction(); //追加 } private void Update() { if (Application.IsPlaying(gameObject)) return; if (string.IsNullOrEmpty(gameObject.scene.path)) return; SerializedObject serializedObject = new SerializedObject(this); SerializedProperty property = serializedObject.FindProperty("uniqueIdentifier"); if (string.IsNullOrEmpty(property.stringValue)) { property.stringValue = System.Guid.NewGuid().ToString(); serializedObject.ApplyModifiedProperties(); } } } }複数シーン保存①
1, 下記コードを追加
SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { SaveFile(saveFile, CaptureState()); } public void Load(string saveFile) { RestoreState(LoadFile(saveFile)); } private Dictionary<string, object> LoadFile(string saveFile) { string path = GetPathFromSaveFile(saveFile); print("Loading from " + path); using (FileStream stream = File.Open(path, FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); return (Dictionary<string, object>) formatter.Deserialize(stream); } } private void SaveFile(string saveFile, object state) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, state); } } private Dictionary<string, object> CaptureState() { Dictionary<string, object> state = new Dictionary<string, object>(); foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { state[saveable.GetUniqueIdentifier()] = saveable.CaptureState(); } return state; } private void RestoreState(Dictionary<string, object> state) { foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { saveable.RestoreState(state[saveable.GetUniqueIdentifier()]); } } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }複数シーン保存②
1, 下記コードを追加する
SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; namespace RPG.Saving { public class SavingSystem : MonoBehaviour { public void Save(string saveFile) { Dictionary<string, object> state = LoadFile(saveFile); //追加 CaptureState(state); //追加 SaveFile(saveFile, state); //変更 } public void Load(string saveFile) { RestoreState(LoadFile(saveFile)); } private Dictionary<string, object> LoadFile(string saveFile) { string path = GetPathFromSaveFile(saveFile); //追加 if (!File.Exists(path)) { return new Dictionary<string, object>(); } using (FileStream stream = File.Open(path, FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); return (Dictionary<string, object>)formatter.Deserialize(stream); } } private void SaveFile(string saveFile, object state) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, state); } } private void CaptureState(Dictionary<string, object> state) //変更 { foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { state[saveable.GetUniqueIdentifier()] = saveable.CaptureState(); } } private void RestoreState(Dictionary<string, object> state) { foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { string id = saveable.GetUniqueIdentifier(); //変更 //追加 if (state.ContainsKey(id)) { saveable.RestoreState(state[id]); } } } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }シーン間のチェックポイント
1, 各スクリプトに各コードの記述
Portalusing System; using System.Collections; using RPG.Saving; //追加 using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; namespace RPG.SceneManagement { public class Portal : MonoBehaviour { enum DestinationIdentifier { A, B, C, D, E } [SerializeField] int sceneToLoad = -1; [SerializeField] Transform spawnPoint; [SerializeField] DestinationIdentifier destination; [SerializeField] float fadeOutTime = 1f; [SerializeField] float fadeInTime = 2f; [SerializeField] float fadeWaitTime = 0.5f; //private void Start() //{ // transform.parent = null; // DontDestroyOnLoad(gameObject); //} private void OnTriggerEnter(Collider other) { if (other.tag == "Player") { StartCoroutine(Transition()); } } private IEnumerator Transition() { if (sceneToLoad < 0) { Debug.LogError("Scene to load not set."); yield break; } transform.parent = null; DontDestroyOnLoad(gameObject); Fader fader = FindObjectOfType<Fader>(); SavingWrapper savingWrapper = FindObjectOfType<SavingWrapper>(); //追加 yield return fader.FadeOut(fadeOutTime); savingWrapper.Save(); //追加 yield return SceneManager.LoadSceneAsync(sceneToLoad); savingWrapper.Load(); //追加 Portal otherPortal = GetOtherPortal(); UpdatePlayer(otherPortal); yield return new WaitForSeconds(fadeWaitTime); yield return fader.FadeIn(fadeInTime); Destroy(gameObject); } private void UpdatePlayer(Portal otherPortal) { GameObject player = GameObject.FindWithTag("Player"); player.GetComponent<NavMeshAgent>().enabled = false; //追加 player.GetComponent<NavMeshAgent>().Warp(otherPortal.spawnPoint.position); player.transform.rotation = otherPortal.spawnPoint.rotation; player.GetComponent<NavMeshAgent>().enabled = true; //追加 } private Portal GetOtherPortal() { foreach (Portal portal in FindObjectsOfType<Portal>()) { if (portal == this) continue; if (portal.destination != destination) continue; return portal; } return null; } } }SavingWrapperusing UnityEngine; namespace RPG.Saving { public class SavingWrapper : MonoBehaviour { const string defaultSaveFile = "save"; private void Start() { Load(); } private void Update() { if (Input.GetKeyDown(KeyCode.S)) { Save(); } if (Input.GetKeyDown(KeyCode.L)) { Load(); } } public void Load() { GetComponent<SavingSystem>().Load(defaultSaveFile); } public void Save() { GetComponent<SavingSystem>().Save(defaultSaveFile); } } }ISaveableコンポーネント
1,各スクリプトに下記コードを記述する
SaveableEntityusing System.Collections; using System.Collections.Generic; using RPG.Core; using UnityEditor; using UnityEngine; using UnityEngine.AI; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { Dictionary<string, object> state = new Dictionary<string, object>(); foreach (ISaveable saveable in GetComponents<ISaveable>()) { state[saveable.GetType().ToString()] = saveable.CaptureState(); } return state; } public void RestoreState(object state) { Dictionary<string, object> stateDict = (Dictionary<string, object>)state; foreach (ISaveable saveable in GetComponents<ISaveable>()) { string typeString = saveable.GetType().ToString(); if (stateDict.ContainsKey(typeString)) { saveable.RestoreState(stateDict[typeString]); } } } private void Update() { if (Application.IsPlaying(gameObject)) return; if (string.IsNullOrEmpty(gameObject.scene.path)) return; SerializedObject serializedObject = new SerializedObject(this); SerializedProperty property = serializedObject.FindProperty("uniqueIdentifier"); if (string.IsNullOrEmpty(property.stringValue)) { property.stringValue = System.Guid.NewGuid().ToString(); serializedObject.ApplyModifiedProperties(); } } } }Moverusing System.Collections; using System.Collections.Generic; using RPG.Core; using UnityEngine; using UnityEngine.AI; using RPG.Saving; namespace RPG.Movement { public class Mover : MonoBehaviour, IAction, ISaveable { [SerializeField] Transform target; [SerializeField] float maxSpeed = 6f; NavMeshAgent navMeshAgent; Health health; private void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); health = GetComponent<Health>(); } void Update() { navMeshAgent.enabled = !health.IsDead(); UpdateAnimator(); } public void StartMoveAction(Vector3 destination, float speedFraction) { GetComponent<ActionScheduler>().StartAction(this); MoveTo(destination, speedFraction); } public void MoveTo(Vector3 destination, float speedFraction) { navMeshAgent.destination = destination; navMeshAgent.speed = maxSpeed * Mathf.Clamp01(speedFraction); navMeshAgent.isStopped = false; } public void Cancel() { navMeshAgent.isStopped = true; } private void UpdateAnimator() { Vector3 velocity = navMeshAgent.velocity; Vector3 localVelocity = transform.InverseTransformDirection(velocity); float speed = localVelocity.z; GetComponent<Animator>().SetFloat("fowardSpeed", speed); } public object CaptureState() { return new SerializableVector3(transform.position); } public void RestoreState(object state) { SerializableVector3 position = (SerializableVector3)state; GetComponent<NavMeshAgent>().enabled = false; transform.position = position.ToVector(); GetComponent<NavMeshAgent>().enabled = true; GetComponent<ActionScheduler>().CancelCurrentAction(); } } }ISaveablenamespace RPG.Saving { public interface ISaveable { object CaptureState(); void RestoreState(object state); } }UUIDの重複排除
1, 下記コードを追加する
SaveableEntityusing System; using System.Collections; using System.Collections.Generic; using RPG.Core; using UnityEditor; using UnityEngine; using UnityEngine.AI; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; static Dictionary<string, SaveableEntity> globalLookup = new Dictionary<string, SaveableEntity>(); //追加 public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { Dictionary<string, object> state = new Dictionary<string, object>(); foreach (ISaveable saveable in GetComponents<ISaveable>()) { state[saveable.GetType().ToString()] = saveable.CaptureState(); } return state; } public void RestoreState(object state) { Dictionary<string, object> stateDict = (Dictionary<string, object>)state; foreach (ISaveable saveable in GetComponents<ISaveable>()) { string typeString = saveable.GetType().ToString(); if (stateDict.ContainsKey(typeString)) { saveable.RestoreState(stateDict[typeString]); } } } private void Update() { if (Application.IsPlaying(gameObject)) return; if (string.IsNullOrEmpty(gameObject.scene.path)) return; SerializedObject serializedObject = new SerializedObject(this); SerializedProperty property = serializedObject.FindProperty("uniqueIdentifier"); if (string.IsNullOrEmpty(property.stringValue) || !IsUnique(property.stringValue)) //追加 { property.stringValue = System.Guid.NewGuid().ToString(); serializedObject.ApplyModifiedProperties(); } globalLookup[property.stringValue] = this; //追加 } //追加 private bool IsUnique(string candidate) { if (!globalLookup.ContainsKey(candidate)) return true; if (globalLookup[candidate] == this) return true; if (globalLookup[candidate] == null) { globalLookup.Remove(candidate); return true; } if (globalLookup[candidate].GetUniqueIdentifier() != candidate) { globalLookup.Remove(candidate); return true; } return false; } } }2, PlayerプレハブのUnique Identifierをplayerに変更する。(その他シーンがある場合はそちらのPlayerもplayerに変更する)
Healthポイントの保存
1, 下記コードを追加
Healthusing System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Saving; namespace RPG.Core { public class Health : MonoBehaviour, ISaveable { [SerializeField] float healthPoints = 100f; bool isDead = false; public bool IsDead() { return isDead; } public void TakeDamage(float damage) { healthPoints = Mathf.Max(healthPoints - damage, 0); if (healthPoints == 0) { Die(); } } private void Die() { if (isDead) return; isDead = true; GetComponent<Animator>().SetTrigger("die"); GetComponent<ActionScheduler>().CancelCurrentAction(); } public object CaptureState() { return healthPoints; } public void RestoreState(object state) { healthPoints = (float)state; if (healthPoints <= 0) { Die(); } } } }SaveableEntityusing System; using System.Collections; using System.Collections.Generic; using RPG.Core; using UnityEditor; using UnityEngine; using UnityEngine.AI; namespace RPG.Saving { [ExecuteAlways] public class SaveableEntity : MonoBehaviour { [SerializeField] string uniqueIdentifier = ""; static Dictionary<string, SaveableEntity> globalLookup = new Dictionary<string, SaveableEntity>(); //追加 public string GetUniqueIdentifier() { return uniqueIdentifier; } public object CaptureState() { Dictionary<string, object> state = new Dictionary<string, object>(); foreach (ISaveable saveable in GetComponents<ISaveable>()) { state[saveable.GetType().ToString()] = saveable.CaptureState(); } return state; } public void RestoreState(object state) { Dictionary<string, object> stateDict = (Dictionary<string, object>)state; foreach (ISaveable saveable in GetComponents<ISaveable>()) { string typeString = saveable.GetType().ToString(); if (stateDict.ContainsKey(typeString)) { saveable.RestoreState(stateDict[typeString]); } } } #if UNITY_EDITOR private void Update() { if (Application.IsPlaying(gameObject)) return; if (string.IsNullOrEmpty(gameObject.scene.path)) return; SerializedObject serializedObject = new SerializedObject(this); SerializedProperty property = serializedObject.FindProperty("uniqueIdentifier"); if (string.IsNullOrEmpty(property.stringValue) || !IsUnique(property.stringValue)) //追加 { property.stringValue = System.Guid.NewGuid().ToString(); serializedObject.ApplyModifiedProperties(); } globalLookup[property.stringValue] = this; } #endif private bool IsUnique(string candidate) { if (!globalLookup.ContainsKey(candidate)) return true; if (globalLookup[candidate] == this) return true; if (globalLookup[candidate] == null) { globalLookup.Remove(candidate); return true; } if (globalLookup[candidate].GetUniqueIdentifier() != candidate) { globalLookup.Remove(candidate); return true; } return false; } } }ラストシーンのリロード
1,各スクリプトに下記コードを記述する
SavingWrapperusing System.Collections; //追加 using UnityEngine; namespace RPG.Saving { public class SavingWrapper : MonoBehaviour { const string defaultSaveFile = "save"; //変更 private IEnumerator Start() { yield return GetComponent<SavingSystem>().LoadLastScene(defaultSaveFile); } private void Update() { if (Input.GetKeyDown(KeyCode.S)) { Save(); } if (Input.GetKeyDown(KeyCode.L)) { Load(); } } public void Load() { GetComponent<SavingSystem>().Load(defaultSaveFile); } public void Save() { GetComponent<SavingSystem>().Save(defaultSaveFile); } } }SavingSystemusing System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using UnityEngine; using UnityEngine.SceneManagement; //追加 namespace RPG.Saving { public class SavingSystem : MonoBehaviour { //追加 public IEnumerator LoadLastScene(string saveFile) { Dictionary<string, object> state = LoadFile(saveFile); if (state.ContainsKey("lastSceneBuildIndex")) { int buildIndex = (int)state["lastSceneBuildIndex"]; if (buildIndex != SceneManager.GetActiveScene().buildIndex) { yield return SceneManager.LoadSceneAsync(buildIndex); } } RestoreState(state); } public void Save(string saveFile) { Dictionary<string, object> state = LoadFile(saveFile); CaptureState(state); SaveFile(saveFile, state); } public void Load(string saveFile) { RestoreState(LoadFile(saveFile)); } private Dictionary<string, object> LoadFile(string saveFile) { string path = GetPathFromSaveFile(saveFile); if (!File.Exists(path)) { return new Dictionary<string, object>(); } using (FileStream stream = File.Open(path, FileMode.Open)) { BinaryFormatter formatter = new BinaryFormatter(); return (Dictionary<string, object>)formatter.Deserialize(stream); } } private void SaveFile(string saveFile, object state) { string path = GetPathFromSaveFile(saveFile); print("Saving to " + path); using (FileStream stream = File.Open(path, FileMode.Create)) { BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, state); } } private void CaptureState(Dictionary<string, object> state) { foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { state[saveable.GetUniqueIdentifier()] = saveable.CaptureState(); } state["lastSceneBuildIndex"] = SceneManager.GetActiveScene().buildIndex; //追加 } private void RestoreState(Dictionary<string, object> state) { foreach (SaveableEntity saveable in FindObjectsOfType<SaveableEntity>()) { string id = saveable.GetUniqueIdentifier(); if (state.ContainsKey(id)) { saveable.RestoreState(state[id]); } } } private string GetPathFromSaveFile(string saveFile) { return Path.Combine(Application.persistentDataPath, saveFile + ".sav"); } } }Portalusing System; using System.Collections; using RPG.Saving; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; namespace RPG.SceneManagement { public class Portal : MonoBehaviour { enum DestinationIdentifier { A, B, C, D, E } [SerializeField] int sceneToLoad = -1; [SerializeField] Transform spawnPoint; [SerializeField] DestinationIdentifier destination; [SerializeField] float fadeOutTime = 1f; [SerializeField] float fadeInTime = 2f; [SerializeField] float fadeWaitTime = 0.5f; //private void Start() //{ // transform.parent = null; // DontDestroyOnLoad(gameObject); //} private void OnTriggerEnter(Collider other) { if (other.tag == "Player") { StartCoroutine(Transition()); } } private IEnumerator Transition() { if (sceneToLoad < 0) { Debug.LogError("Scene to load not set."); yield break; } transform.parent = null; DontDestroyOnLoad(gameObject); Fader fader = FindObjectOfType<Fader>(); SavingWrapper savingWrapper = FindObjectOfType<SavingWrapper>(); yield return fader.FadeOut(fadeOutTime); savingWrapper.Save(); yield return SceneManager.LoadSceneAsync(sceneToLoad); savingWrapper.Load(); Portal otherPortal = GetOtherPortal(); UpdatePlayer(otherPortal); savingWrapper.Save(); //追加 yield return new WaitForSeconds(fadeWaitTime); yield return fader.FadeIn(fadeInTime); Destroy(gameObject); } private void UpdatePlayer(Portal otherPortal) { GameObject player = GameObject.FindWithTag("Player"); player.GetComponent<NavMeshAgent>().enabled = false; player.GetComponent<NavMeshAgent>().Warp(otherPortal.spawnPoint.position); player.transform.rotation = otherPortal.spawnPoint.rotation; player.GetComponent<NavMeshAgent>().enabled = true; } private Portal GetOtherPortal() { foreach (Portal portal in FindObjectsOfType<Portal>()) { if (portal == this) continue; if (portal.destination != destination) continue; return portal; } return null; } } }シーンロード前にフェードする
1, 各スクリプトに下記コードを記述する
Faderusing System.Collections; using UnityEngine; namespace RPG.SceneManagement { public class Fader : MonoBehaviour { CanvasGroup canvasGroup; private void Awake() { canvasGroup = GetComponent<CanvasGroup>(); } //追加 public void FadeOutImmediate() { canvasGroup.alpha = 1; } public IEnumerator FadeOut(float time) { while (canvasGroup.alpha < 1) { canvasGroup.alpha += Time.deltaTime / time; yield return null; } } public IEnumerator FadeIn(float time) { while (canvasGroup.alpha > 0) { canvasGroup.alpha -= Time.deltaTime / time; yield return null; } } } }SavingWrapperusing System.Collections; using RPG.Saving; using UnityEngine; namespace RPG.SceneManagement { public class SavingWrapper : MonoBehaviour { const string defaultSaveFile = "save"; [SerializeField] float fadeInTime = 0.2f; private IEnumerator Start() { Fader fader = FindObjectOfType<Fader>(); fader.FadeOutImmediate(); yield return GetComponent<SavingSystem>().LoadLastScene(defaultSaveFile); yield return fader.FadeIn(fadeInTime); } private void Update() { if (Input.GetKeyDown(KeyCode.S)) { Save(); } if (Input.GetKeyDown(KeyCode.L)) { Load(); } } public void Load() { GetComponent<SavingSystem>().Load(defaultSaveFile); } public void Save() { GetComponent<SavingSystem>().Save(defaultSaveFile); } } }
- 投稿日:2021-03-05T04:31:33+09:00
Android App Bundle で戸惑ったこと (Unity)
はじめに
少々時間を無駄にしたので、ここに書き留めます。
環境
- Unity 2018.4.x
得た知見
aabのBuild&Runでは、エラーせずとも実行されない場合がある
- aabをBuild&Runした場合、端末への転送に失敗してもエラーにならず、インストール済みのバージョンが起動されます。
- 転送に失敗する場合は、署名が違うとか、バージョンダウンだとか、色々あります。
- なお、既にアプリが起動中だった場合は再起動されません。
- 従って、ビルド開始時にあらかじめ旧版を起動しておけば、再起動するかどうかで成否を確認可能です。
aabとapkでは挙動が異なる場合がある
- 同じコードをビルドしても、aabとapkでは挙動が異なる場合があります。
- 具体的には、アセットの読み込み時に、apkには存在しない遅延が、aabでは生じることがあります。
- apkは最初に全てロードしますが、起動を早めるために、aabでは必要になってからロードするのでしょうか。
さいごに
- 転送に失敗していることに気付かず、特定のソースだけがコンパイルから漏れているのかと考えて、試行錯誤してしまいました。
- 挙動が異なるのは、ソースが巻き戻っているのではないかと疑ってしまいました。
- 投稿日:2021-03-05T04:16:41+09:00
Unityエディタ拡張 ビルド前処理と後処理の今時の書き方
はじめに
- エディタ拡張で、ビルド前に動的にコードを生成したくて調べたら、インターフェイスを継承してコールバックを使うという記事がいくつか見つかりました。
- 前処理は
IPreprocessBuild.OnPreprocessBuild
で、後処理はIPostprocessBuild.OnPostprocessBuild
が紹介されています。- ところが、書いてみたら
obsolete
で警告されました。- そこから色々調べた結果です。
環境
- Unity 2018.4.x, 2019.4.x
今時の方法
リンク先の「公式スクリプトリファレンス」にもコード例があります。
以降の例は、ほぼ公式のままなので、リンク先をご覧いただいた方が分かり易いかも知れません。ビルド前処理
- ビルド開始前に呼ばれます。
- インターフェイスを継承したクラスを用意して、コールバックを受けます。
IPreprocessBuildWithReport.OnPreprocessBuild
コールバック (公式スクリプトリファレンス)例
- 方針
- 別クラスで定義されている変数を表示するだけのシーンを用意します。
- エディタ拡張で、ビルド直前に、クラスが定義されたファイルを動的に生成します。
- 生成されたコードによって、変数は現在日時で初期化されます。
- 生成されているファイルの初期化子と、実行で表示された日時が一致すれば成功です。
- このやり方で、実行時には取得できない(あるいは取得の面倒な)
PlayerSettings
の要素などをコードに取り込むことが可能です。Assets/Editor/PreBuild.csusing System; using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; public class PreBuild : IPreprocessBuildWithReport { public int callbackOrder { get { return 0; } } // ビルド前処理の中での処理優先順位 (0で最高) public void OnPreprocessBuild (BuildReport report) { Debug.Log ($"IPreprocessBuildWithReport.OnPreprocessBuild for {report.summary.platform} at {report.summary.outputPath}"); var ScriptPathPrefKey = $"{PlayerSettings.companyName}/{PlayerSettings.productName}/ScriptPath"; var AssetPath = EditorPrefs.GetString (ScriptPathPrefKey, "Assets/"); File.WriteAllText (Path.Combine (AssetPath, "Data.cs"), // 要するに、Assets/Data.csに以下を書き出す $@"public class Data {{ public static readonly string BuildDateTime = ""{DateTime.Now}""; // コード生成時の日時 }}"); AssetDatabase.Refresh (); // アセットDBの更新 } }ビルド中処理
- シーンがビルドされる毎に呼ばれるようです。
- インターフェイスを継承したクラスを用意して、コールバックを受けます。
IProcessSceneWithReport.OnProcessScene
コールバック (公式スクリプトリファレンス)例
Assets/Editor/SceneBuilded.csusing UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; public class SceneBuilded : IProcessSceneWithReport { public int callbackOrder { get { return 0; } } public void OnProcessScene (UnityEngine.SceneManagement.Scene scene, BuildReport report) { Debug.Log ($"IProcessSceneWithReport.OnProcessScene {scene.name} as {report.name}"); } }ビルド後処理
- メソッドにアトリビュートを付けることで、ビルド完了後に呼ばれるようです。
PostProcessBuildAttribute
アトリビュート (公式スクリプトリファレンス)例
Assets/Editor/PostBuild.csusing UnityEditor; using UnityEditor.Callbacks; using UnityEngine; public class PostBuild { [PostProcessBuild (0)] public static void OnPostprocessBuild (BuildTarget target, string pathToBuiltProject) { Debug.Log ($"OnPostprocessBuild for {target} at {pathToBuiltProject}"); } }ビルド結果
ConsoleIPreprocessBuildWithReport.OnPreprocessBuild for StandaloneWindows at D:/development/PreBuildTest/PreBuildTest.win/PreBuildTest.exe IProcessSceneWithReport.OnProcessScene SampleScene as New Report OnPostprocessBuild for StandaloneWindows at D:/development/PreBuildTest/PreBuildTest.win/PreBuildTest.exe Build completed with a result of 'Succeeded' in 12 seconds (12068 ms)
- 投稿日:2021-03-05T00:10:44+09:00
Airtest + Poco を iOS で実行するのに引っかかったポイントまとめ
はじめに
Airtest と Pcoo の説明は省きます
去年の CEDEC で発表があり話題になったツールですね
今回は iOS で Airtest と Poco を組み合わせた UIテストを行おうとした際に引っかかったポイントのまとめです
この内容は投稿時点でのポイントであり、数ヶ月後には参考にならない可能性がありますのでまずは公式の Issue を見ることをお勧めします1. iOS-Target の セッションが作成されない
iOS で Airtest を実行するには Airtest 向けに改良した iOS-Target を端末にインストールする必要があります
プロジェクトURL : https://github.com/AirtestProject/iOS-Tagent私の環境では初回のみ特に引っかかることなく iOS 端末でのテストを行えたのですが、それ以降繋がらない問題が発生しました
(繋がらないというのは AirtestIDE の Connect を押しても映像が映らないという意味です、ログをみる限り接続自体は出来ていました)
色々と調査をした結果、恐らくセッションが作成されていないことが問題であると判断し以下のコマンドを叩いてセッションを作成した結果 Connect を押して無事繋がることを確認できました。curl --location --request POST ‘http://192.168.0.103:8100/session’ \ --header ‘Content-Type: application/json’ \ --data-raw ‘{“capabilities”:{}}’iOS-Target を iOS 端末で実行している状態で
http://127.0.0.1:8100/status
へ接続すると以下のような画像の情報が表示されます
sessionId
がnull
の場合はこの対応で解決しました
2. AirtestIDE
1.2.7
でAttributeError: 'IOS' object has no attribute 'instruct_helper'
と表示される恐らく Airtest のバグです
1.2.6
ではこの問題は発生していないのを確認したので、1.2.7
から1.2.6
へバージョンを変えることをお勧めします
Issue は立っていました : https://github.com/AirtestProject/Airtest/issues/8623. Landscape(横画面)でビルドすると Airtest が動作しない
これはかなり引っかかりました・・・
こちらの方の記事を見かけなければ更に沼にハマっていたかもしれません
修正を待ちましょうまとめ
書いてみると大したことなさそうな感じしますね・・・
あまり iOS 向けの記事を書いている方がいなかったのでどなたかの助けになれば幸いです