- 投稿日:2021-12-03T23:51:30+09:00
Unityで自力描画。
この記事は、Unity Advent Calendar 2021 4日目の記事になります。 前日締切だったコンペへの提出が終わったあとのテンションで書きなぐったため、割と文章がおかしかったらごめんなさい。 もしかしたらあとで体裁を整えるかもしれません。 概要 Unityで弾幕STG等のオブジェクトを大量に出せるゲームを作るため、軽量な描画コマンドを記載する資料になります。 ※実際のところBatchRendererGroupだったりJobSystem使えばもうちょっと速度出せるとおもうんですが、踏み込んだ最適化アプローチだったりするので、まずは楽にオブジェクトを出せるアプローチを記載することにしてます。 こんな描画してるよ 割と手加減しているのですが、この時点で400個くらい画面に出てます。 更に言うと エフェクトを表示 敵オブジェクト表示 PostProcessをかけてる 状態です。Unityってすげえ。 とりあえず描画であればやたら出るわけですね。 バッチがどうなってるかは状況に依存しますが、割とまとまった描画になってると考えます。 (アニメごとに描画は違うけど) どういうアプローチで描画するか DrawMesh命令で描画するだけ。 2D想定だと、 スプライト扱いで描画するため2枚のポリゴンを生成 Z方向も指定(ビルボードであれば方向は固定) テクスチャはMaterialに設定し反映 とすると見た目2Dの描画ができてしまいます。 はて、Unityで描画命令・・・? そんなの書いたことあったけ? UnityではGameObjectにRendererをアタッチ、もしくはシーンに登録した時点で 描画はされるはずですが。 GameObjectに各種Rendererをアタッチしている状態だと、言ってしまうと「勝手に描画される」んですね。 そのおかげであまり描画を気にせず取り扱うことができるんですね。 他のコンポーネントとの絡み 上でGameObjectとは関係なく描画コマンドを実行できることは記載しました。 じゃあGameObject・Collider等との関連性はどうなるのか? 答えは「GameObject等と関連しない形で描画ができる」です。 各コンポーネントは本来GameObject上にアタッチすることで効果を発揮するため、 DrawMeshを行った場合、実際の座標に関係させないことが可能になります。 GameObjectを取り扱う場合、各コンポーネントをアタッチすれば即動いてくれる敷居の低さがUnityの便利さではあるのですが、これらを使わないという選択肢も可能になるわけですね。 (勿論GameObjectにアタッチしたコンポーネントも利用可能ですよ) 逆に言うと描画した座標と関連するように各コンポーネントを使うためにはC#で座標を制御する等での処理が必要になるため、Editor上での動作にはむいてない形になります。 描画を別コマンドで実行して何がしたいのか 「GameObjectを1個のオブジェクトジェネレータとしたい」です。 Unityにおいて、GameObjectをInstantiateするのは処理が重い、というのは割と知られていますが、同時にデータ量も多く、処理負荷もかかります。 よって、大量のオブジェクトを動かす場合、GameObjectを敵や弾のマネージャとして、描画は別コマンドで実行、みたいなことを行うアプローチも出てくるわけです。 上記のケースではGameObjectで描画命令を発行するというアプローチは有用かと思います。 わざわざUnity使ってるのにそんなまどろっこしい事する理由が・・・ Unityのレンダリングエンジンを組み込むことができるので、 激しい動きときれいな画面が同居する形になります。 よって、派手なアプローチがいっぱいできるわけです。 まぁその代わり実装は面倒になりますよね、と。 実際、当たり判定やオブジェクト管理についてはUnityの機能を使わずにC#で実装している状態です。 終わりに 割とこういうの書く機会もないので、一度かいてみました。 せっかくだからこういう需要が少ない記事を紛れ込ませるのもひとつということで。 (自分はUnityでも頑張って弾幕STG作りたい派です) 宣伝 現在インディーゲーム「Cry Pic.」開発中です。 上記の知識や描画を使って、大量の弾やエフェクトを描画してます。
- 投稿日:2021-12-03T23:30:16+09:00
Niantic Lightship ARDKの機能調査
まずARDKの概要情報です Lightship ARDKとは、「Pokemon Go」で知られるNianticが2021年11月8日から正式に提供を開始したUnityベースのARアプリ開発キットです。この記事ではその機能や実装してみて気づいたことなどをまとめています。 基本的には下記のドキュメントに乗っている情報を基に書いております。 https://lightship.dev/docs/ ARDKの機能要約 1. Mapping ARアンカー 深度推定 カラー画像入力から、カメラと映ったモノの距離を深層学習で推定。LiDAR深度データは使用されない リアルタイム・メッシュ 画像, カメラの向き, (LiDAR深度データ)からニューラルネットワークにより3Dジオメトリを生成 LiDAR搭載デバイスとそれ以外の場合の違い IARWorldTrackingConfiguration.IsDepthEnabledの真否を変えると、下記表のような違いが生まれる オクリュージョン 深度を基に障害物があったら、3Dモデルを隠す ARDepthManagerのOcclusionModeで調整可能 2. Understanding セマンティック・セグメンテーション 画像内の特定の領域にクラスラベルを割り当てる。ピクセルごとにラベルを取得可能。 index Channel名 メモ 0 sky 雲が含まれています。霧は含まれていません。 1 ground 人工地盤(下)のすべてのものと、土、草、砂、およびその他の自然地盤タイプが含まれます。植生や葉が多い地面は地面として検出されず、代わりに葉として検出されます。 2 artificial_ground 道路、歩道、線路、カーペット、敷物、フローリング、小道、砂利、およびいくつかの競技場が含まれます。 3 water 川、海、池、湖、プール、滝、いくつかの水たまりが含まれています。飲料水、カップ、ボウル、シンク、バスの水は含まれません。反射の強い水は水として検出されない場合があります。 4 building 近代的および伝統的な住宅および商業ビルが含まれます。壁と同義と見なされるべきではありません。 5 foliage 茂み、低木、木の緑豊かな部分、鉢植えの植物、花が含まれています。 6 grass 背の高い草ではなく、芝生などの芝生の地面。 3. Sharing マルチプレイ 1セッションにつき、最大5人で5分が目安 空間データの共有 セッション開始時は、その時点のデバイスの位置を原点とした各自の座標系を作成 Stable以降はホストの座標系に合わせて、デバイスや物の位置を補正する。 4. 開発ツール Virtual Studio アプリのビルド・デプロイに時間を費やさないようにテストするための環境 Mock Mode 仮想で部屋を作り、その部屋で開発したARのテストを行う Remote Mode スマホとPCを接続し、スマホのカメラで得た映像や深度を用いて、ARのテストを行う リリース予定 料金体系 実装例 次は実装してみて重要そうだなと思ったことの羅列です Virtual StudioのRemote Mode構築手順 Remote Feed Appのインストール ARDKExamples/VirtualStudio/ARDK Remote Feed App.Sceneだけをビルド Build SettingsでAutoconnectProfiler, Develop Buildをチェック Remote Feed Appをインストール Remote Feed AppとテストしたいSceneを開いたUnity Editorの接続 PCとスマホを接続 ARSessionを開始しているテストすべきSceneを開く Profilerでデバイスを選択 Virtual StudioでRemote → USBを選択 スマホでRemote Feed Appを開き、USBを選択する 接続に成功すれば[Connected!]と表示される Unity Editorでテストしたい挙動を行う MeshとSemantic Segmentation Mesh 実装例 1. ARSessionManagerのあるシーンでARMeshプレハブを追加する Semantic Segmentation 実装例 1. AR Semantic Segmentation ManagerをARSceneCameraプレハブに追加 2. 下記のようなスクリプトを記述 CheckSemantic.cs // Update is called once per frame void Update() { if (PlatformAgnosticInput.touchCount <= 0) { return; } var touch = PlatformAgnosticInput.GetTouch(0); if (touch.phase == TouchPhase.Began) { int x = (int)touch.position.x; int y = (int)touch.position.y; DebugLogSemanticsAt(x, y); } } //examples of all the functions you can use to interogate the procider/biffers void DebugLogSemanticsAt(int x, int y) { string[] channelsNamesInPixel = semanticManager.SemanticBufferProcessor.GetChannelNamesAt(x, y); foreach (var i in channelsNamesInPixel) { Debug.Log($"{i}"); } } } } HLAPIとLocalization メンバー同士での空間データの重ね合わせ 生成 位置同期 CosmosBehaviour.cs //NetworkBehaviourを継承しているクラスでの関数です protected override void SetupSession(out Action initializer, out int order) { initializer = () => { var auth = Owner.Auth; var descriptor = auth.AuthorityToObserverDescriptor(TransportType.UnreliableUnordered); // UnreliableBroadcastTransformPackerを作成するだけで、 // Transform同期のためのブロードキャスト/受信が設定されます new UnreliableBroadcastTransformPacker ( "netTransform", transform, descriptor, TransformPiece.Position, Owner.Group ); }; order = 0; } TransformType Relieable → 99.95%の信頼性を保証, UnRelieable → 99%で受け取らなかったメッセージはドロップ UnreliableUnorderedとUnreliableOrdered, ReliableUnordered とReliableOrderedがある メッセージの送受信 実装例 タグ付きでメッセージ void PerformActionForAllPeers(IMultipeerNetworking networking, byte[] gameData) { networking.BroadcastData(GameActionMessageTags.PerformSomeGameAction, gameData, TransportType.UnreliableUnordered); } void SubscribeToPeerDataReceived(IMultipeerNetworking networking) { networking.PeerDataReceived += OnPeerDataReceived; } // Since the Hlapi also uses byte messages, this will be fired each time an object is NetworkSpawned as well void OnPeerDataReceived(PeerDataReceivedArgs args) { // Check that it is a message we care about, rather than an Hlapi message if(args.Tag != GameActionMessageTags.PerformSomeGameAction) { return; } // We now know this is the message that this class cares about, so use the data to perform some update } ※タグはイベントデータであるPeerDataRecievedArgsの中で指定できるuint タグ無しでメッセージ private void OnDidConnect(ConnectedArgs connectedArgs) { // ... // Continuing method from second code snippet... _hitStreamReplicator = new MessageStreamReplicator<Vector3> ( "hitMessageStream", _arNetworking.Networking.AnyToAnyDescriptor(TransportType.ReliableOrdered), group ); _hitStreamReplicator.MessageReceived += (args) => { Debug.Log("Ball was hit"); if (_auth.LocalRole != Role.Authority) return; _ballBehaviour.Hit(args.Message); }; } HLAPIの詳細(データの送受信の際に起きていることなど) FAQ NetworkFieldなどの実装例 その他注意点 精度 下記動画のスマホのように位置のずれはあった 通信 サーバーメッセージの遅延は数百ms ~ 数千msになるが、ピアツーピアメッセージの遅延は数十msになる場合がある(デバイスの近さによって異なる)。 下記の間にピアAからピアBにメッセージを送信した場合、ピアBはピアAをまだ有効なピアとしておらず、メッセージをドロップする。そのためPeerAddedイベントの度、数フレーム後に初期化メッセージが発生するようにするべき。 ピアA ← ピアBからPeerAddedイベントを受信 ピアB ← ピアAからPeerAddedイベントをまだ受信していない 現在、同じピアとしてセッションに再参加することはサポートされていない。再参加した場合、デバイスは完全に新しいピアと見なされる。 ホストがセッションを離れる場合、新ホストは選択されない。ただし、ホストが検出したマップはセッションのタイムアウトまで残り、新しいピアは残ったマップに対してローカライズできる。 全ピアが退出した後、30秒後にタイムアウトする。この時セッションに再参加しようとすると、意図しない動作が発生する可能性がある。 おまけ①: 2021年11月30日現在、難しかったこと ※ドキュメントでの記述がないことなので、真偽不明です WindowsのUnityでビルド + XCodeでiPhoneにインストールしようとするとエラー →MacのUnityでビルド + XCodeでインストールすると大丈夫だった Virtual Studioはどちらのモードでもリアルタイム・メッシュ機能が使えなかった →Depth機能とSemantic Segmentation, ネットワーク機能は使えた URPのままVirtual Studio・Mock Modeにすると、仮想環境と認識させるためにARDK_MockWorldレイヤーを設定しているGameObject全てが見えなくなってしまった →その他ピンクシェーダーになる部分が多く、やはりまだ使うべきではなさそう おまけ②: その他 Q. マルチプレイのメッセージ容量100MBで大きい。どう実装しているか。 A. メッセージ byte[] 10MB 永続的なKey,Value Key → string 4KB, Value → byte[] 100MB セッションがアクティブな限り、サーバーに保存されるデータ 更新されると各デバイスでイベントが発火する セッション開始時に全員が得るべきデータを保存するなどの目的で使う 実装例 ※データ制限について
- 投稿日:2021-12-03T21:30:33+09:00
UnityのLightAnchorコンポーネント
この記事はPONOS Advent Calendar2021の6日目の記事です。 昨日は私(@block)のCLIから使えるC#のフォーマッターdotnet-formatの紹介でした。 LightAnchorというものが面白かったので簡単な紹介です。 公式からはUnity 2021.2 のアーティスト・デザイナー向け新機能やNew Lighting Features in Unity 2021.2- Volumetric Clouds, Lens Flare and Light Anchor(動画の24:20あたり)で紹介されています。 マニュアルもありますがパラメーターの説明だけなので直感的に分かりづらいと思います。 Light Anchorコンポーネントとは Unity2021.2から追加されたLight用のコンポーネントです。 今までライトでどこを照らすかの設定はLightをアタッチしたオブジェクトのTransformを編集するしかなかったですがLightAnchorを使用することで、照らしたい場所と方向を基準にライトを設定できるようになりました。 下記のgifはSpotLightをCubeの位置を基準に照らしているものです。 設定方法 LightがアタッチされているオブジェクトにLightAnchorをアタッチしてAnchor Position Overrideに照らしたいオブジェクトのTransformを参照させるだけです。 そのまま角度を設定したり距離を設定することもできます。 ちなみにそのままAnchor Position Overrideに設定しているオブジェクトを動かすとLightのオブジェクトも同時に移動します。 他にもいろんな機能があるのでぜひ試してみてください! Lightの位置を固定したまま対象のオブジェクトの向きに向けたい場合はLook At Constraintというコンポーネントが使えると思います。 明日は@nissy_gpさんです
- 投稿日:2021-12-03T19:37:15+09:00
OculusQuestのハンドトラッキングでライトの色を変えよう
今回作ったもの まず今回作ったものを動画で示します。 Oculus Questのハンドトラッキングで色の頭文字を書いてその色にライトを変えるの出来た!!QuestからUDPでラズパイに色を何にするか送ってラズパイからライトにirMagicianでデータの送信#Oculus #RaspberryPi pic.twitter.com/z05tpje7VB— ryo (@Ah92082778) November 23, 2021 このように、まず色の頭文字を書いてから指パッチンをするとライトの色を変えられるシステムを作りました。 利用したものの概要 Unity 2019.4.18f1 Oculus Quest ラズパイ model2B(記憶があいまいです) Python 2.7(ラズパイでアップデートしてなかったです。今後改善したいと思ってます。) IrMagician ラズパイで用いる赤外線リモコンシステムのためのパーツです。 リモコン操作が出来るライト IrMagicianから赤外線リモコンで色を変えます。 概要図 今回はOculus QuestにアプリをビルドせずOculus Linkを用いて動作させています。ビルドすれば図のデスクトップPC部分に関しては必要がなくなると思っています。(今後改良したいと思っています。) Oculus Questのハンドトラッキングを用いて手の動きと指パッチンを認識しています。 アイデア 今回OCRを用いて文字を認識しようと考えていました。その際に以下の二点が思いつきました。 Unity側でOCRを行ってUDPで文字をラズパイに送る。 Unity側で書いたものを画像として保存しラズパイに送り、画像からOCRを行って文字を認識する。 しかし、1では簡単に利用できそうなものを見つけられませんでした。OpenCVのアセットやTesseractを用いれば出来るかもしれません。2に関しては画像からOCRを行うのがラズパイ2では性能不足に感じ速度が出なかったため断念しました。 そのため、今回は精確な文字認識ではなく複数の点から文字を識別するようにしました。 画像のようにUnityで9つの点を置き、それぞれの当たり判定を用いてR,B,Yのどれかということ認識するようにしました。 コードなどの説明 今回作成したものは大きく分けるとUnity側とラズパイ側二つに分かれます。 Unity(OculusQuest側) ハンドトラッキングをして文字を認識する UDP通信する まずはUnity側ではどのように見えているのかを示します。 このようにハンドトラッキングの指の先に当たり判定があるオブジェクトを子オブジェクトとして設定します。そして、目の前に9つの当たり判定がついているSphereを表示します。 Unityのヒエラルキーは以下の画像のようになっています。 実際に書いたコードについての説明をします。 ハンドトラッキングについてはこの記事がとても参考になりました。https://qiita.com/divideby_zero/items/4949fadb2c60f810b3aa using UnityEngine; using UnityEngine.UI; public class YubiPatchin : MonoBehaviour { //ラズパイのIPアドレスとポート番号を指定します。 public string remoteHost = ""; public int remotePort = 60000; //タッチするSphereを取得します。 [SerializeField] GameObject[] touchSphereObj; //Udp通信を行うためのコードです。 UdpSender udpSender; [SerializeField] OVRSkeleton oVRSkeleton; Vector3 oyayubi; Vector3 nakayubi; Vector3 hitosashi; [SerializeField] Text text; float stepTime = 0f; //これに関しては配列を用いればよかったと後悔しています。今後修正予定です。 public bool touchSphereFlag1 = false; public bool touchSphereFlag2 = false; public bool touchSphereFlag3 = false; public bool touchSphereFlag4 = false; public bool touchSphereFlag5 = false; public bool touchSphereFlag6 = false; public bool touchSphereFlag7 = false; public bool touchSphereFlag8 = false; public bool touchSPhereFlag9 = false; // Start is called before the first frame update void Start() { udpSender = new UdpSender(remoteHost, remotePort); } // Update is called once per frame void Update() { //ここで指パッチンの動作を判定するために親指と中指と人差し指の先のpositionを取得します。 oyayubi = oVRSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_Thumb2].Transform.position; nakayubi = oVRSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_MiddleTip].Transform.position; hitosashi = oVRSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position; //UDPで通信する際に連続して何度も送らないように五秒ごとに送れるようにしました。 stepTime += Time.deltaTime; if (stepTime > 5.0f) { //指パッチンをしているかどうかの判定です。 if ((oyayubi - nakayubi).magnitude < 0.03f && (hitosashi - oyayubi).magnitude > 0.05f) { text.text = "yubiPatchin"; //タッチされていないSphereごとになんの文字化を判定しています。 //udpSender.SendData()でラズパイに文字データとして送信しています。 if (touchSphereFlag8 == false) { udpSender.SendData("RED"); text.text = "RED"; } else if (touchSphereFlag2 == false) { udpSender.SendData("YELLOW"); text.text = "YELLOW"; } else { udpSender.SendData("BLUE"); text.text = "BLUE"; } for (int i = 0; i < 9; i++) { touchSphereObj[i].GetComponent<Renderer>().material.color = Color.white; } touchSphereFlag1 = false; touchSphereFlag2 = false; touchSphereFlag3 = false; touchSphereFlag4 = false; touchSphereFlag5 = false; touchSphereFlag6 = false; touchSphereFlag7 = false; touchSphereFlag8 = false; touchSPhereFlag9 = false; stepTime = 0f; } } } } そしてタッチされるSphereには以下のスクリプトをアタッチします。 using UnityEngine; public class TouchSphere : MonoBehaviour { //このnumberはSphereの場所を示しています。 //123 //456 //789 //で対応しています。 [SerializeField] int number; [SerializeField] GameObject HandTrack; YubiPatchin script; // Start is called before the first frame update void Start() { script = HandTrack.GetComponent<YubiPatchin>(); } // Update is called once per frame void Update() { } private void OnTriggerEnter(Collider other) { gameObject.GetComponent<Renderer>().material.color = Color.yellow; switch (number) { case 0: script.touchSphereFlag1 = true; break; case 1: script.touchSphereFlag2 = true; break; case 2: script.touchSphereFlag3 = true; break; case 3: script.touchSphereFlag4 = true; break; case 4: script.touchSphereFlag5 = true; break; case 5: script.touchSphereFlag6 = true; break; case 6: script.touchSphereFlag7 = true; break; case 7: script.touchSphereFlag8 = true; break; case 8: script.touchSPhereFlag9 = true; break; } } } 次にUdpSenderについてなのですが、今回はこの記事を参考にさせていただいたのでコード自体は記事を参照してください。 https://yubeshicat.hatenablog.com/entry/2019/10/31/163435 そしてそれらをGameObjectのHandTrakingにアタッチします。そしてラズパイのIPアドレスとポート番号を入力してください。 ラズパイ UDPで受け取る IrMagicianを使って赤外線をライトに送る まずUDPでの受け取りに関してもこのサイトを参考にさせていただきました。https://yubeshicat.hatenablog.com/entry/2019/10/31/163435 IrMajicianのコードをsudo で実行するためにSubProcessを用いました。 IrMagicianについてはここで利用方法について書くと長くなってしまうので割愛します。 以下のサイトがとても分かりやすいので、このサイトを見ながら赤外線リモコンを使って設定をしてください。 https://itdecoboconikki.com/2017/05/01/raspberry-pi-irmagician/ そして設定したファイルを用いてライトを変更するための以下のコードを実行します。 #coding:utf-8 import threading import time import signal import sys import serial import subprocess from socket import socket, AF_INET, SOCK_DGRAM HOST = '' PORT = 60000 is_running = True angle_data = bytes(b'') # UDP通信部 sock = socket(AF_INET, SOCK_DGRAM) def udp_communication (): First = False First2 = False global is_running global sock global angle_data sock.bind((HOST, PORT)) try: while is_running: msg, address = sock.recvfrom(64) angle_data = msg if msg == "RED": subprocess.call(["sudo","python","irmcli/irmcli.py", "-p", "-f","irmcli/RED.json"]) if msg == "BLUE": subprocess.call(["sudo","python","irmcli/irmcli.py", "-p", "-f","irmcli/BLUE.json"]) if msg == "YELLOW": subprocess.call(["sudo","python","irmcli/irmcli.py", "-p", "-f","irmcli/YELLOW.json"]) except KeyboardInterrupt: print ("Keyboard Interrupt Exception") if __name__ == "__main__": thread = threading.Thread(target=udp_communication) thread.start() 終わりに 自分ではとても面白いものを作れたと思っています。 今後はこれをもっと改良しながらいろいろと出来るようにしていきたいと思っています。今後はラズパイのGPIOピンを使ってをサーボモータなどと組み合わせたりして何か作りたいと思っています。 参考資料 UnityとラズパイをUDPでつなぐ際に参考にさせていただいたサイト Oculus Questのハンドトラッキングにおいて参考にさせていただいたサイト IrMagicianの使い方についてのとても分かりやすい記事
- 投稿日:2021-12-03T17:29:11+09:00
Unityパフォマンス最適化シリーズーー物理モジュール
4年前、Unityの各主要なモジュールのパフォーマンス最適化知識(初心者向け版)を1つずつ説明しました。近年、エンジン自体、ハードウェアデバイス、製作基準などのアップグレードに伴って、UWAは引き続き規則や方法を更新して、各開発者に提供しつつあります。「アップグレード版」のパフォーマンス最適化マニュアルとして、【Unityパフォーマンス最適化シリーズ】はより多くの開発者が利用できるように、シンプルでわかりやすい表現を心がけています。今回、物理モジュールに関連する知識を共有します。 Unityプリインストールの物理エンジンでは、物理モジュールの時間コストは主に、FixedUpdate.PhysicsFixedUpdateと、ロジックコードの放射線検出および衝突検出に起因します。 FixedUpdate.PhysicsFixedUpdate関数の時間コスト構造は、主に2つの部分で構成されています。1つはPhysics.Processingで、もう1つはPhysics.Simulateです。一般的には、これら2つの関数のスタックに注意を払い、さらにスタック関数のコール回数と時間コストの割合から原因を特定する必要があります。パフォーマンスと最適化の対策に影響を与える一般的な要因は次のとおりです。 一、プロジェクトには本当に物理モジュールが必要か? まず、UnityのAuto Simulationオプションの切り替えメカニズムを理解する必要があります。このオプションはUnity2017.4より前では公開されておらず、プロジェクトに物理コンテンツがあるかどうかに応じてエンジンが自動的にオンまたはオフになります。Unity2017.4以降では、このオプションは公開されており、デフォルトはオンです。 多くのプロジェクトでは、物理モジュールがオンであるかオフであるかに注意を払わないことで、不必要なパフォーマンスの浪費を引き起こします。開発チームは、プロジェクトの状況に応じて物理モジュールをオンにするかどうかを検討することをお勧めします。また、物理モジュールの役割を他のソリューションに置き換えて、物理パーツの時間コストを節約することも検討できます。 二、物理的なコール回数を制御する UWAの実機レポートの物理モジュールでは、物理の更新数が統計されておりました。 物理のコール回数は、次の2つのパラメーターに焦点を当てる必要があります。 1)Fixed Timestep FixedUpdateの更新間隔はこれによって決定されます。この値が大きいほど、フレームあたりの物理更新回数は少なくなります。ただし、物理更新頻度が低すぎる場合は、一部のメカニズムが異常になる可能性があります。開発チームは、許容範囲内で可能な限りFixed Timestepの数値を上げることをお勧めします。 2)Maximum Allowed Timestep ここでは、物理システム自体の特性を知る必要があります。つまり、ゲームは前のフレームがフリーズすると、Unityは現在のフレームの非常に早い段階でFixedUpdate.PhysicsFixedUpdateを連続的にN回ほど呼び出します。Maximum Allowed Timestepはまさに物理更新回数を制限する役割を果たします。 Maximum Allowed TimeStepは、単一フレームの物理最大コール回数を決定します。この値が小さいほど、単一フレームの物理最大コール回数は少なくなります。Fixed Timestepが20 msを前提に、Maximum Allowed Timestepを333msから100msへと下げさせたら、スタック状態に関係なく、このフレームは最大17回ではなく、5回だけ呼び出します。 さらに、次のフレームの物理更新回数を減らすために、まずは他のモジュールを最適化することをお勧めします。 三、Contact数は妥当か? Contact数が異常になるのは、普通次の2つの状況があります。 1)Contact数が多すぎる 盾と地面の衝突など、不必要な物理的衝突検出があるかどうかを考慮する必要があります。 2)Contact数が0で、且つ物理時間が存在している Contact数が0ということは、プロジェクトに衝突またはTriggerがないことを意味しています。クロスシミュレーションなどのような物理特性を使用しない場合は、物理モジュールをオフにする、つまりAutoSimulationをオフにすることを検討できます。 四、Raycastの使用を減らす Raycastとは、放射線検出と呼ばれたものです。その時間は放射線の数に比例します。ということは、 Raycastの時間を制御したいと、最も簡単な方法は、放射線の数を制御することです。 ただし、一部のプロジェクトでは確かに多くのRaycastが必要です。たとえば、弾幕ゲームでのRaycast数が多くあります。この場合、Job SystemでのRaycastCommandを利用して、Raycastの時間をメインスレッドから子スレッドに転送することによってその時間を減らします。 五、Auto Sync Transforms Auto Sync Transformsを選択すると、Physics Queryが発生したら、UnityはRigidbody / ColliderのTranformの変更をPosition、Scaleなどの物理エンジンに同期します。さらに、AutoSimulationが選択されている場合、Unityは物理更新するたびにRigidbodyとColliderを自動的に同期します。したがって、AutoSimulationがオフになっていた後に、プロジェクトで放射線検出またはNGUIが使用されている場合は、通常、Auto SyncTransformsを選択する必要があります。そうしないと、放射線検出の結果が正しくなくなるか、UIイベントが応答しなくなるかのことが発生する可能性があります。 一般に、同期操作のパフォーマンスコストは非常に低いです。非常に大きなシーンで、さらに多数のオブジェクトがシーンにロードされるの状況しか、時間コストが高い問題が発生しません。 六、ロジックコードについて ここでのロジックコードは、MonoBehaviorスクリプトのvoid FixedUpdate関数を指します。通常、Profilerではxxx.FixedUpdateとして表示されます。この関数のコール回数は、物理更新回数に影響されます。 たとえば、Monster.csがシーンにマウントされているmobは10個があり、このスクリプトはvoidFixedUpdateにロジックコードを書き込みました。という場合に、現在のフレームの物理更新回数が3であると、このフレームのMonoster.FixedUpdateが10 * 3 = 30回呼び出されることがProfilerに表示されます。 通常、コードをできるだけUpdate関数に書き込むことをお勧めします。これなら、対応するロジックの実行回数が減り、ロジックコードの時間が短縮されます。 UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。 今なら、UWA GOTローカルツールが15日間に無償試用できます!! よければ、ぜひ! UWA公式サイト:https://jp.uwa4d.com UWA GOT OnlineレポートDemo:https://jp.uwa4d.com/u/got/demo.html UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2021-12-03T17:19:57+09:00
LWRPの下にコードでシャドーの生成距離を動的に変更する方法
今回の話題 1)LWRPの下にどのようにコードでシャドーの生成距離を動的に変更するか 2)SRPはAndroidプラットフォームのBox Projectionをどのように有効にするか 3)リリースされたPCバージョンのウィンドウドラッグの問題 4)国際化されたフォントの問題 5)Unityの実行中に手動でnew Meshを解除する方法 SRP Q:こちらのプロジェクトでは、Unity2018.3.14f1を使用し、 LWRP4.10.0も使用しています。コードでは、シャドーの生成距離を動的に設定したいなら、LightweightRenderPipelineAssetで。 ShadowsDistanceのフィールドも正常に割り振られますが、ゲームのシャドウはまだ設定された距離に従って有効になりません。値を手動で調整すると、すぐに有効になります。パラメータを有効にするために何か必須のアクティベーション操作はありますか?コードとスクリーンショットは次のとおりです。 A:それはCameraDataにあります。LWRPのRenderを書き直して、SingleCameraを個別に実装してから、その中で割り当て操作を行われば済みます。したがって、一般的なプロジェクトはこの値を直接的に使えなく、独自のSRPを実装すべきです。 SRP Q:エディターをAndroidプラットフォームに切り替えると、新しく追加された反射プローブのBox Projectionが無効になっていることに気づいました。原因は何ですか。オンにする方法は? A1:SRPは今だにBox Projectionをサポートしていませんから、エディターに選択されても、Shaderではサポートされておらず、公式は後のアップデートでサポートするようになるそうです。 A2:解決策を見つけました: UnityのシェーダーのBox Projectionコードを直接コピーします。 自分のエディターに追加します: 自分のシェーダーに追加します: オフにする前 オンにする後 効果が出ました。 UI Q:PC版がリリースされたとき、ウィンドウをドラッグしても解像度は同じままですが、Scaleが変わることを願っています。PC版をリリースしたとき、ウィンドウをドラッグすると、シーン全体がウィンドウでいっぱいになりました。これはコードを書くことによって設定する必要がありますか、それともリリースオプションに設定がありますか? A:直接的な設定がないと思います。すべてのカメラのレンダリングターゲットとして固定解像度のRenderTextureを使用することを検討できます。最後に、UGUIのRawImageとCanvasScalerでこのレイアウト要件を満たすことができます。 Font Q:今やっているゲームは、国際的な多言語をサポートしていると考えています。ということは、クライアントのゲームでは、他の言語を同時に見ることができます。たとえば、チャットには暫定的にスペイン語、中国語、日本語、韓国語、東南アジア6の言語に定めます。 今はフォントプランを検討中ですが、独自のフォントを持参すると20MB近くになります。システムプリインストールのフォントを使用するのはどの程度実現可能か知りたいのですが、経験者からの意見を聞いてもよろしいでしょうか。 A1:こちらのプロジェクトは中国語(繁体字・簡体字)、日本語、英語、ベトナム語のバージョンを作りましたが、繁体字のようなフォントファイルには著作権がないため、一応いくつかの数字をフォントファイルに入れるだけで、他の文字は削除しました。フォントは好調に見え、1年間の運営でも異常な状況はありません。フォントライブラリが不備の場合にフォントを使用するなら、たとえば、台湾省の人々は日本語の文字を読むなら、文字が太ったり細ったりすることで、奇妙に表示される可能性があります。 A2:どんなフォントを使用するかにはいくつかの考慮事項があります。 1.フォントサイズ。一部は比較的大きいため、複数の言語をサポートできるフォント。 2.フォントの美しさ、一部のフォントはある言語では美しさを失うのです。 3.著作権、著作権のないオープンソースフォントを選択します。 要約すると、一般的に言えば、2つのスキームを使用できます。 1.上記の要件を満たす直接見つけることができるオープンソースフォントを優先的に選択します。 2.オープンソースフォントが見つからない場合は、フォント処理ソフトウェアを使用して、複数のオープンソースフォントの異なる言語部分を抽出し、それらを1つのフォントファイルにマージします。 フォントファイルが大きい場合は、必要に応じてフォントライブラリを調整できます。これは基本的にこちらのプロジェクトで採用されたスキームです。 Loading Q:プロジェクトで使用されるLive2Dは、フレームワークを作成後に、大量のMeshが動的にnewされます。Resources.UnloadUnusedAssets()インターフェイスを使用する以外に、この部分だけのMeshを解除する方法はありますか? A:new MeshはDestroyImmediateAPIを介して解除されることができます。 UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、パフォーマンス分析と最適化ソリューション及びコンサルティングサービスを提供している会社でございます。 今なら、UWA GOTローカルツールが15日間に無償試用できます!! よければ、ぜひ! UWA公式サイト:https://jp.uwa4d.com UWA GOT OnlineレポートDemo:https://jp.uwa4d.com/u/got/demo.html UWA公式ブログ:https://blog.jp.uwa4d.com
- 投稿日:2021-12-03T15:09:33+09:00
UnityのProfilerの情報を取得する(UnityEditorInternal使用)
はじめに サーバサイドの開発がメインだったのでほとんど触ってなかったUnityを最近触り始めた初心者です。 Unityで特定の機能を担うクラスの処理負荷を見る機会があったのですが、UnityのProfilerはUnity上からしか見れなかったり、フレーム単位での確認になるので特定の関数が呼ばれたフレームなんかを探すのがちょっと面倒です。 柔軟に集計したり別の形式のビューで表示したりしたかったので、Profilerに表示された情報をスクリプトで取り出してみました。 確認環境 Unity 2020.3.23f1 方法 他の記事などを参考にしたところ、 UnityEditorInternal を使う方法で取り出すことができるようです。 例えば、フレーム数の一覧は次のようにして取得できます。 int frame = UnityEditorInternal.ProfilerDriver.firstFrameIndex; while (frame != -1) { frame = UnityEditorInternal.ProfilerDriver.GetNextFrameIndex(frame); } 特定のフレームでのヒエラルキー1行ごとのデータは以下のようにして取得できます。 var property = new UnityEditorInternal.ProfilerProperty(); property.SetRoot(frame, HierarchyFrameDataView.columnDontSort, 0); string output = ""; while (property.Next(true)) { int depth = property.depth; string name = property.GetColumn(HierarchyFrameDataView.columnName); string totalPercent = property.GetColumn(HierarchyFrameDataView.columnTotalPercent); string selfPercent = property.GetColumn(HierarchyFrameDataView.columnSelfPercent); string calls = property.GetColumn(HierarchyFrameDataView.columnCalls); string gcMemory = property.GetColumn(HierarchyFrameDataView.columnGcMemory); string totalTime = property.GetColumn(HierarchyFrameDataView.columnTotalTime); string selfTime = property.GetColumn(HierarchyFrameDataView.columnSelfTime); string warningCount = property.GetColumn(HierarchyFrameDataView.columnWarningCount); // 出力例 output += $"{depth} {name} {totalPercent}\n"; } Debug.Log(output); 画像のようなプロファイラの状態では以下のように出力されます。 (画像と若干ずれがありますが、UIでは設定できないソートなしをスクリプト側で使用しているためです) 1 EditorLoop 99.1% 1 PlayerLoop 0.6% 2 Camera.FindStacks 0.0% 2 Camera.Render 0.2% 3 Camera.ImageEffects 0.0% 4 Graphics.Blit 0.0% 5 RenderTexture.ResolveAA 0.0% 3 CullResults.CreateSharedRendererScene 0.0% 4 ReflectionProbeAnchorManagerUpdate 0.0% 5 ReflectionProbeAnchorPositionUpdate 0.0% 4 UnityEngine.CoreModule.dll!UnityEngine.Rendering::SupportedRenderingFeatures.IsLightmapBakeTypeSupportedByRef() 0.0% 3 Culling 0.1% ... 上記の2例を組み合わせることで、Profilerに記録された情報を全フレームに渡って取得できます。 今回の目的であった「特定のクラスの処理負荷」では、クラス名の文字列が見つかったdepthを利用することでその配下の関数呼び出しだけをうまいこと取り出すことができます。 UnityEditorInternalについて 今回利用している UnityEditorInternal は、ソースは公開されている けどUnity側から利用方法などを提供されていない部分です。 Unityのバージョンアップで仕様変更されて使えなくなる可能性があります。 例として、参考にした2つの記事では動作確認したUnityのバージョンによって利用方法が変わっています。 // 2018ではこれ property.SetRoot(frame, ProfilerColumn.SelfTime, ProfilerViewType.Hierarchy); // 2019以降 property.SetRoot(frame, HierarchyFrameDataView.columnTotalTime, 0); 第2引数はEnumの指定方法が変わっただけですが、第3引数は 2018.4まで利用できたEnum が 2019.1以降はinternalが指定されて参照できなくなっている のがソースからも見て取れます。 別の例として、フレーム一覧取得の際に GetNextFrameIndex を使いましたが、たとえば最終フレームで呼び出した時の挙動は 2020.1での使用箇所 を見る限り-1で判定するとよさそうです。 currentFrame = ProfilerDriver.GetNextFrameIndex(currentFrame); if (currentFrame == -1) break; なお、この数行だけで見ても 2020.2, 2021.1 で変更が入っており、開発の活発さが伺えます。 2020.2 currentFrame = ProfilerDriver.GetNextFrameIndex(currentFrame); if (currentFrame == k_InvalidOrCurrentFrameIndex || !ProfilerDriver.GetFramesBelongToSameSession(currentFrame, prevFrame)) break; 2021.1 currentFrame = ProfilerDriver.GetNextFrameIndex(currentFrame); if (currentFrame == FrameDataView.invalidOrCurrentFrameIndex || !ProfilerDriver.GetFramesBelongToSameSession(currentFrame, prevFrame)) break; まとめ UnityのProfilerに表示された値を取り出し、それにまつわるUnityEditorInternalの中身を少し覗きました。 今回紹介した方法は、今後のUnityバージョンに合わせて変更する必要が出たりそもそも使えなくなる可能性もあるということを考慮した上で利用を検討しましょう。 参考記事 UnityEditorInternal.ProfilerPropertyを使用する場合 Unityが書きだしたログファイルをそのまま読み込む場合
- 投稿日:2021-12-03T13:25:04+09:00
【Unity】STYLY VRでフェードイン・フェードアウトをつくる【STYLY】
はじめに STYLYアドベントカレンダー3日目の記事になります! VR/AR/MRクリエイティブプラットフォーム「STYLY」VRでフェードイン・フェードアウトを作ります。 STYLYではC#が使えないため、ロジックの部分は「PlayMaker」というAssetを使っての実装になります。 こんな感じです↓ この記事で作るもの 今回作るのは数秒ごとに画面がフェードイン・フェードアウトするサンプルです。 まずはサンプルを見てみましょう。 Web版 Web版ではカメラ追従がうまくいかないため、フェード用の板がワールドに配置してあるだけになります。 VR版 VRで見るとフェードイン・フェードアウトしてくれます。 全体の構成として、フェード用の板を作りカメラに追従させることで実現させています。 フェードイン・フェードアウトの作成 フェード用Quadの作成 はじめに画面を隠す用のオブジェクトを作成します。 Hierarchyで右クリック。「3D Object-> Quad」を選択します。 そうすると四角い板のようなGameObjectが作成されるのでPositionを(0, 0, 0.5)。Scaleを(3, 3, 1)に設定してください。 フェード用Materialの作成 このままではQuadのalpha値が使えないのでMaterialというものを作成します。 Projectで右クリック。「Create -> Material」を選択します。名前は「FadeMaterial」にしておきましょう。 選択するとInspectorに「Standard」と書いてあるタブが存在するのでクリック。「UI -> Default」を選択してColorを(0, 0, 0, 255)にしてください。 Materialのアタッチ さきほど作ったFadeMaterialをQuadに適用します。 Projectにある「FadeMaterial」をQuadにドラッグ&ドロップします。 SceneのQuadがグレーからブラックに変わればOKです。 PlayMakerFSMの作成 ここからはPlayMakerを使ったロジックの作成をしていきます。 PlayMakerのアタッチ HierarchyにあるQuadを選択後、InspectorからAddComponentをクリック。 「PlayMakerFSM」で検索をかけて追加します。名前は「CameraFadeFSM」にしましょう。 Initializeステートの作成 追加したPlayMakerFSMのEditをクリックするとこんな画面が現れます。 この画面でロジックの設計をしていくことになります。 では、「State 1」を選択して「Initialize」という名前にリネーム。 右下の「ActionBrowser」から「GetMainCamera・GetMaterial・SetParent」の3つを追加しましょう。 GetMainCameraの「StoreGameObject -> NewVariable...」を選択し「MainCamera」と入力。 GetMaterialの「StoreGameObject -> NewVariable...」を選択し「FadeMaterial」と入力。 SetParentのParentから「MainCamera」を選択。チェックボックスは「ResetLocalRotation」にだけチェックをつけておきましょう。 Idleステートの作成 それでは新規ステートを作成してみましょう。 何もないところで右クリック。「AddState」を選択するとステートが作成されます。 「Idle」にリネーム後、「Wait」アクションを追加し、Timeを「5」にしておきます。 赤いエラーはいったん無視で良いです。 InitializeとIdleをつなぐ Initializeを選択後右クリックし、「AddTransition -> FINISHED」を選択。 FINISHEDを右クリック長押しで矢印が伸びるようになるのでIdleとつないであげます。 こうして繋いでいくことでイベントが遷移していきます! さきほど赤エラーはTransitionが追加されてないことが原因なので、このタイミングでIdleにも「FINISHED」を追加しておきましょう。 StartFadeOutステートの作成 「StartFadeOut」という名前のステートを新規作成後、ActionBrowserから「ColorInterpolate・SetMaterialColor」を追加しましょう。 ColorInterpolateのColorには「2」と入力。Element0には(0,0,0,0)、Element1には(0,0,0,255)を入力。 Timeは「5」、StoreColorは「StoreGameObject -> NewVariable...」を選択し「FadeColor」と入力。 FinisiEventは「FINISHED」を選択。RealTimeにチェックを入れます。 SetMaterialColorのMetarialIndexは「0」。Materialは「FadeMaterial」を選択。 Colorは「FadeColor」を選択しEveryFlameにはチェックを入れます。 これで5秒間かけて透明から黒に変化するようになりました! Waitステートの作成 「Wait」という名前のステートを新規作成後、ActionBrowserから「Wait」を追加しTimeに「1」と入力します。 このステートを挟むことによってフェードアウトの状態を1秒間保持することができます。 StartFadeInステートの作成 StartFadeOutを選択後「Ctrl + D」または「コピー&ペースト」で複製します。 名前は「StartFadeIn」に変更しておきましょう。 カラー情報を反転させたいので、Element0には(0,0,0,255)、Element1には(0,0,0,)を入力。 最後にIdleに矢印をつないでループできるようにしたら完成です! アップロード前に 動作チェック 完成したものを動かしてみましょう。 ゲームシーンを再生するとQuadがMainCameraの子要素に入るはずです。 これはInitializeステートの「SetParent」Actionの効果です。 そしてフェードインアウトしていればロジック部分は問題ありません! Materialの再設定 気づいた方もいるかもしれませんが、このままだとゲームシーン再生直後のQuadが真っ黒な状態です。 序盤に作成した「FadeMaterial」のカラーが(0,0,0,255)となっているためです。(0,0,0,0)に変えておきましょう。 最後に 長くなりましたがお疲れさまでした! 今回作ったロジックを応用して好きなタイミングで実行したり、真っ暗な状態でワールドに変化を起こしたりできると作品作りの幅が広がると思います! 例として僕が作った作品のリンクを張っておきますので参考にしてみてください~~~。 ルーペをつかい世界の断片をさがしだす体験型VR作品「World Fragments」を作成しました!現実にレイヤーを付与する技術である「AR」について考えてきた僕が、「レイヤー」をテーマにした人生初のVR作品です。体験方法はリプ欄に続けます↓#NEWVIEWAWARDS2021 #VR #XR #STYLY #NEWVIEW pic.twitter.com/IdUbIJoC20— ふぉーみ (@Four4_mm) November 1, 2021
- 投稿日:2021-12-03T12:07:04+09:00
同人プロジェクトマネジメント入門:仕様書作成編その3
同人プロジェクトマネジメント入門の目次はこちら はじめに 前回までで、タスクの作成方法について学びました。今回は、タスクを作成するマネージャーに向けて、どのようなタスクを追加すればよいのかのヒントを与えるため、タスクの特徴について書いていきます。 タスクの特徴 ゲーム開発におけるタスクには様々なものが存在します。仕様を決める際には、マネージャーがタスクごとの重さを考えて、出来るだけ軽い作業になるよう工夫したり、重い作業を1個以上に増やさないようにしましょう。同人プロジェクトの開発物についていうと、基本的には、複雑な大作より、良い感じに動くミニゲームの方が周りに楽しんでもらえる確率は高いです。 炎上しやすいタスク 複雑・少数 炎上しやすいタスクは「完成度が100%か0%か2択で、完成度80%は0%にカウントされる」という性質を抱えています。 具体例を挙げると、「戦闘システムが、自分の攻撃だけ完成しました」といった状況では、納品が出来ません。相手の攻撃も要りますし、アイテムを使ったりすることもあるでしょう。全部作らないと作っていない場合と同じという特性を持っています。 そして、このようなタスクは、分業が難しいタスクでもあります。なぜなら、システム内の多数の変数や関数が、相互に複雑に関連しあっているためです。本質的に複雑で巨大なシステムは、個々人で脳内の設計が異なりますし、分業のためのインターフェースの作成も難しいのです。 自作のみ 複雑性が上がれば上がるほど、金の力で何とかならないような仕様が増えていきます。 「独特のシステム」「新感覚○○」などは非常に危険です。同人プロジェクトのゲームの独創性は、システムよりフレーバー面で出すことを勧めます。具体的には、「ハロウィンをテーマにする?」などゲームの雰囲気を重視した独創性を入れた方が、炎上確率は下がります。(ハロウィンに独創性はないですが...) 依存関係が多い 「Aが終わらないとBが出来ない」というようなタスクは、炎上を生みやすいです。なぜなら、待ちが発生しやすいからです。仕様を作成したら仕事を割り当てる計画が必要ですが、依存関係がある仕事を受け持った人のモチベが枯れた場合に、その影響が外部に波及して、モチベのある人間の仕事を阻害するのです。この状況が起こると、他の人間のモチベが死んだり、仕事をしない人を非難したりしてチームの雰囲気が最悪になります。 また「待ち」の解消はマネージャーの仕事でもあるので、マネージャーの過労およびモチベーション消失にもつながります。 変更が多い 当たり前の話ですが、仕様が頻繁に変更されることが予想される、「ふわっとした要件」は炎上します。「なんかいい感じに戦う敵AI」などがいい例で、実装方法が何通りも考えられ、うまくいかなければ様々な実装を試す必要が生まれるのです。 時間が予測しにくい ズルズルと開発が遅れまくる 具体例 以下のような、ルールの複雑さによってゲームの面白さを担保するようなものは、炎上するタスクを生みやすいです。 プロシージャル系統(自動生成ステージなど) 賢いAI全般 リアルタイム通信 格ゲー 3Dアクション 独特なシステムゲー(時間操作とか、磁石アクションとか、流体○○とか) レースゲーム 物理が絡むゲーム 本格シミュレーション ただし、これらのうちそこそこのパターンが、「金でシステムを買う」という方法で対応することが可能です。お金での解決を選んだ場合には、難易度は一気に下がるでしょう。逆に、「自作ゲームで賢い敵AIを作りたい!」とか「当時の社会システムまで模倣したシミュレーションを作りたい」とかの要件の場合は相当厳しいプロジェクトが予想されます。 炎上しにくいタスク 単発・多数 キャラクターのステータスを生める単調作業を無限にする、といったタスクです。これはこれで特有の辛さがあるのは事実です。しかし、キャラの数を30から15にすることは可能ですし、シナリオの場合も無理やり終わらせれば10話構成を5話構成にすることが可能です。 つまり、完成度が100%から0%まで連続的な値をとるというのが「単発・多数」という性質の意味です。 さらに、多数の独立した仕事から構成されているので、複数人で一気に仕上げることもできます。 代替可能 いったん仮の素材で埋めてしまえるタスクです。例えば、カードゲームの場合、大量の2D画像を使う必要がありますが、いったん適当な素材で埋めて余裕があれば自作するという手が取れます。 金の力での誤魔化しがきくタスクは、炎上する前にお金で火を消せます。 依存関係がない 理想的な順番にタスクの依存関係を並べます。上のタスクが多いのがいいんですが、もちろんゲームである以上、下のタスクも存在します。最初に「骨」を作ろう!というのは、「モチベーションという資産をゲーム全体の完成を左右する部分にフル活用しよう」という目的のスローガンです。 「機能Aがなくてもゲームは完成する」:サブクエスト的なもの。ゲーム全体で希釈されるため、機能Aがなくてもプレイヤーは気づかない 「機能Aがないと機能Bがしょぼい」:戦闘における必殺技システムなど。無くなってしまうと、機能Bという範囲内で機能Aが消えるので、機能不足に気づかれてしまうが、プレイ自体に支障はない 「機能Aがないと最終的にゲームが完成しない」:依存関係があるものの、その被害・待ちが発生するのはゲーム開発終盤である 「機能Aがないと機能Bへ進めない」:依存関係があるが、最悪機能Aを別の人に交代させれば機能Bからはまっさらな状況から開発できる 「機能Aが巨大」:依存関係がある上に機能A内部で中途半端に開発が行われていた場合、負の遺産として残り続ける 具体例 以下のような、コンテンツの多さによってゲームの面白さを担保するようなものは、炎上しにくいタスクを生みやすいです。 RPG シナリオゲーム カードゲーム 多数のステージ キャラゲー 多数のユニット おわりに 特定のタスクの難易度が上がってしまうのは仕方がないことです。独創性のあるアイデアというのは、やはり複雑なものです。なぜなら単純なアイデアなら誰かがやっているからです。したがって、複雑性のあるタスクをむやみに避けるのではなく、複雑性の要因のうち一部を我慢する(例:一部のシステムは購入して改造で出来る範囲に仕様変更)ことでメリハリをつけることが重要です。
- 投稿日:2021-12-03T12:00:18+09:00
YouTubeの英語の動画はどう見たほうが間違えないのか
PONOS Advent Calendar 2021の8日目の記事です。 昨日は私のDOTSベースのUnity PhysicsはPhysXよりどの程度軽量なのか?でした。 はじめに 前回の記事は物理エンジンの内容に触れました。この記事を書くにあたって、久しぶりに改めてUnity公式の情報を色々見てみたりしていたのですが、改めて感じたことがあります。 Unityの情報は様々なところにありますが、その一つにYouTubeがあります。YouTubeにはチュートリアルやTipsなどの動画があがっており、中には公式には日本語字幕がつけられていないものもあります。 しかし、今は自動字幕や自動翻訳を使うことができ、大変便利になりました。ですが、残念ながら現在の自動翻訳は少なくとも技術的な動画で確認する限り、かなり厳しい日本語になってしまっているように感じます。 ということで、今回は試しにYouTubeに上がっているUnity公式のPerformance optimization tips: Physics in Unityの内容を確認しつつ、自動生成された英語字幕の精度と日本語自動翻訳の精度を改めて確認して現在の実用性を確認してみました。 内容紹介 このチュートリアル動画は、物理エンジンを使用する際に最初に確認すると良いTipsをたくさん紹介してくれています。 更新頻度や、Physics.Simulateを使用した手動シミュレーション、Physics.autoSyncTransformsのことについてなど、多くのサイトで紹介されているようなポイントも抑えられています。 内容が気になる方はぜひ動画で全容をご覧ください。 Performance optimization tips: Physics in Unity 比較 「Performance Optimization Tips - Physics in Unity.」 この文章はチュートリアルのタイトルでもあります。これに対して自動生成された英語字幕は以下の通りです。 Performance optimization tips physics and unity. inとandを間違えてしまっているようです。似た発音はやはり難しい? 自動翻訳(日本語)は以下のようになりました。 パフォーマンス最適化のヒント物理学と統一 なるほど、わかりませんw 日本語にしなくてよいUnityを日本語にしてしまっているので話がわからなくなってしまっています。もちろんandになっているため、意味も違っています。 「The broad phase attempts to efficiently determine the number of collisions which are needed for the narrow phase.」 日本語では「ブロードフェーズでは、ナローフェーズに必要なコリジョンの数を効率的に決定しようとします。」という感じです。 この文章に自動生成される英語字幕はこちら The broad face attempts to efficiently determine the number of collisions which are needed for the narrow phase. faceは多分phaseのことだと思います。これも似てる発音なので間違っているのではないでしょうか。 では自動翻訳(日本語)はというと、、、 ブロードフェイスは、tに必要な衝突の数を効率的に決定しようとします。 やはり英語がfaceだったのでフェイスになっています。これでは "幅の広い顔" です。しかし、それよりもいきなり問題を提出された気分です。「t」とはなんでしょうか? 文章としてはわかりますが、ナローフェーズの部分がなぜかtになってしまっていて伏せられていて伝わりません。 「When generating runtime meshes to use in the mesh collider it's important that generated meshes have properly created triangles meshes.」 日本語では「ランタイムメッシュを生成してメッシュコライダーで使用する際には、生成されたメッシュに適切に作成された三角形のメッシュが含まれていることが重要です。」という感じ。 この文章に自動生成される英語字幕はこちら When generating runtime meshes to use in the mesh collider it's important that generated meshes have properly created triangles meshes なんの問題もなさそうです。 では自動翻訳(日本語)はというと、、、 メッシュコライダーで使用するランタイムメッシュを生成するときに衝突が実際に計算されるような狭いフェーズでは、生成されたメッシュが適切に三角形を作成していることが重要 なんだか難しいこといってる!って感じになりました。 「メッシュコライダーで使用するランタイムメッシュを生成するときに衝突が実際に計算されるような狭いフェーズでは」・・ こうなると、"どこ"が"どこ"にかかっているのかという、日本語の難しさと専門用語が相まって、すごいこと言ってる感が増しているように思います。 実はこの「衝突が実際に計算されるような狭いフェーズでは」というのはどこからきているかというと、Whenの前にあった会話がのっかってきています。 口頭では「xxxx. When yyyy」と、話が続いているため(少し時間は空いていますが)、それが日本語に訳したときに、こちらの文章に入ってきているのだと思います。 しかし、これはどっちにかかるかで、かなり意味が違ってきてしまう部分です。 「This can lead to a loss of performance if the transform component changes and then you perform physics queries such as ray-casts in quick succession.」 日本語では「このため、Transformコンポーネントが変更された後に、Raycastなどの物理クエリを連続して実行すると、パフォーマンスの低下につながる可能性があります。」という感じ。 この文章に自動生成される英語字幕はこちら this can lead to a loss of performance if the transform component changes and then you perform physics queries such as raycasts and quick succession. raycasts and quick succession.となっており、ここでもinとandが間違っていると思われます。 では自動翻訳(日本語)はというと、、、 なんと表示されませんでした。 まとめ 動画の半分くらいを確認しましたが、英語の自動字幕については概ね理解できる文章になっていました。 ただし、油断しているとinとandが違っていたり、phaseとfaceを間違えていたり、発音が似てて全然違う意味になるところが間違ってる時があるので、英語字幕を鵜呑みにするとハマります。 英語の動画に対して「字幕:自動生成(英語)」は割と高い品質のように見える。(でも時々間違っている事もある) 英語の動画に対して「字幕:自動翻訳(日本語)」はかなり厳しい。文章の翻訳品質以前に、文章中のワードが違う文章にかかってきたりすることもあるようで、英語字幕とずいぶん意味の違うものになってくることがある。 ということで、少なくとも自動翻訳(日本語)はやはりまだ厳しく、英語字幕ならまだ実用的でした。 しかし、おそらくそれも動画による(喋り方による)のでしょう。。。 何か結論がありそうなタイトルにしてしまっていますが、結局のところ耳で聞くのが一番ということになってしまいます。 あるいは英語字幕を元に、耳で聞いて内容を確認するのが正確です。少なくとも日本語字幕で内容を把握すると勘違いしてしまうので危ないです。 聞く練習という意味では、YouTubeの再生速度変更の設定を使い、字幕を表示しつつ0.25倍や0.5倍で再生して確認すると良いかもしれません。 英語を学ぶことは必須とよく言われたりしますが、技術革新があれば一切不要になる未来もあるかもしれません。 最近の翻訳アプリなどをみていると進化したなーとめっちゃ思うんですが、やはりこういう内容の動画になってくるの難しいのでしょうか? ということで、明日は@e73ryoさんです!ご期待ください。
- 投稿日:2021-12-03T11:33:30+09:00
新卒2年目までに学んだ、コーディングで意識すること
本記事はCraft Egg Advent Calendar 2021の12/3の記事です。 12/2の記事は@ishiguro_takuyaさんの[Unity] UniversalRenderPipelineについて調べた覚え書き でした。 はじめに 株式会社Craft EggでUnityクライアントエンジニアをしている豊田です。 今回は私が新卒入社してから現在の2年目までに、プルリクエストでチームからもらったコメントや技術書をなどの学びを元に、コーディングで気をつけていることをまとめてみました。 before/afterのコードの例を出すことで、新卒エンジニアが遭遇しやすいコーディングアンチパターンとその改善例の参考になれば幸いです。 コーディング規約やしきたりはチームによって異なりはしますが、できるだけ一般的な内容を取り上げました。 ※本記事で取り上げるコードはサンプルです。重要でない部分の命名は「hoge」としたり、簡略化しています。 表面上の改善 リーダブルコードには、第1部に「表面上の改善」が紹介されています。 命名規則やコードの体裁など、第三者(未来の自分自身も)がコードを理解するために最初に意識できる観点だと思います。 適切な変数名にする 意味が分かる命名にする boolは「is~」「should~」「can~」「exists~」「has~」など コールバックは「onCloseDialog」など「on~」 略しすぎない。一般的な略称ならOK(× s_pos → ◯ startPosや、startPosition) 英文法に誤りが無いか気をつける(× onClosedDialog → ◯ onCloseDialog) など 適切な関数名にする 基本的に動詞始まりにする 初期化は「Initialize」「Setup」 生成は「Create~」 登録は「Register~」 変換は「ConvertHogeToFoo」 「Add◯◯」なら「Remove◯◯」というように、名前と処理を対にして理解しやすくする 「~IfNeed」「Check~」のような命名の関数を避ける(処理を分けられないか検討する) 英文法に誤りが無いか気をつける など 一次変数でも分かりやすい命名にする 一次変数などスコープが小さくてもできるだけ意味の分かる命名にします。 before UserData u = GetUserData(userId); after UserData userData = GetUserData(userId); ※ラムダ式の中でも同様 before filterUsers = userList .Where(x => x.Id % 2 == 0) .ToList(); after filterUsers = userList .Where(user => user.Id % 2 == 0) .ToList(); ネストを浅くする 例外は先に早期returnすることで思考をクリアにできます。 before private void Hoge() { if (hoge != null) { Foo(); if(foo != null) { // メインの処理が続く } } } after private void Hoge() { if (hoge == null) { return; } Foo(); if (foo == null) { return; } // メインの処理が続く } なお、早期returnは関数の出口が複数でき、利用側の期待する処理が行われず罠にハマることがあるため、何でも早期returnするのではなく、処理を分離できないか検討するなどしてケースバイケースで用いるのが良いと思います。 三項演算子を使う before if (IsHoge()) { foo = bar; } else { foo = null; } after foo = IsHoge() ? bar : null; foreachをLINQに置き換える before private List<ItemData> CreateItemDataList(List<uint> itemIds) { List<ItemData> itemDataList = new List<ItemData>(); foreach (uint id in itemIds) { itemDataList.Add(new ItemData(id)); } return itemDataList; } after private List<ItemData> CreateItemDataList(List<uint> itemIds) { return itemIds .Select(id => new ItemData(id)) .ToList(); } 配列やListに対してforeachで処理する場合の多くはLINQで書けます。 文字列補間を使う C#の6.0以上で使える文字列補間は、書きやすく見やすいです。 https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated before Debug.LogError("userId : " + userId + ", userName : " + userName + "のユーザでエラー"); after Debug.LogError($"userId : {userId}, userName : {userName}のユーザでエラー"); コメントアウト 自明なコメントは書かない Initialize(); // 初期化 と書かれていてもコメントの有無で情報量が変わりません(むしろノイズになりうる)。 できるだけコードで説明することを心がけます。 処理の内容ではなく理由を書く 特殊な実装をしていたら、なぜそうしているかのコメントがあると、読んだ人をびっくりさせません。 なお、低レベルのコードを要約したり、処理のまとまりを説明する場合など、ケースによって処理の内容は書くことはあると思います。 問題のあるコードにコメントを書く こちらも読んだ人をびっくりさせることを防ぎます。 やむを得ず綺麗じゃない実装をしているのなら、しれっとコードに紛れ込ませるのではなくTODOやHACKなどを添えて問題点を説明します。 安全性、Nullチェック Get関数は失敗を考慮する before UserData userData = GetUserData(userId); userData.HogeMethod(); この例だと、userDataの取得に失敗した時にNullエラーになりかねないので、Nullチェックを挟みます。 after UserData userData = GetUserData(userId); if (userData == null) { Debug.LogError( "UserData is null. userId : " + userId ); // 場合に合わせて例外処理を行う return; } userData.HogeMethod(); Null演算子を使って簡潔にする before if (userData != null && userData.Card != null) { // userData.Cardにアクセスする処理 } after if (userData?.Card != null) { // userData.Cardにアクセスする処理 } ※ リストのNull、空チェックも同様に簡潔にできます before if (userList != null && userList.Count != 0) { // userListにアクセスする処理 } after if ((userList?.Count ?? 0 ) != 0) { // userListにアクセスする処理 } なお、MonoBehaviourなどUnityEngine.Objectを継承したものに対してのnullチェックは以下のような問題もあるので、Null演算子は使わずに== nullを使います。 エラー通知は必要な時にはっきり行う ログは、エラーが起きた時にエラー出力をさせるなど、必要な時に行います。 正常挙動である時はログは出力させません。 なお、追いにくい低レイヤの処理には正常ログを入れる場合もあるかと思います(問題が起きた時に追いやすくする)。 制御の改善 処理の対象を絞ってLoop処理を回す before foreach (var hoge in hogeList) { if(!hoge.IsFoo) { continue; } hoge.HogeMethod(); } 先にIsFooで絞ってからforを回したほうが、どういうデータを処理したいループなのかが分かりやすいです。 after foreach (var hoge in hogeList.Where(hoge => hoge.IsFoo)) { hoge.HogeMethod(); } 状態を保持する変数はenumで定義する フラグを乱立すると状態が分かり辛く、ソースを読み解くのが大変になります。 before private bool isProcessing; private bool isInterrupted; private bool isInitialized; after private enum StateType { None, Processing, Interrupted, Initialized } private StateType currentState; 設計の改善 モジュールを「純粋」にして、モジュール間を「疎遠」にすることを意識します。 凝集度と結合度の話ですが、以下の要素は基本的に凝集度を高く結合度を低くするための実践例だと思います。 長すぎるメソッド、クラスを書かない 一つの関数が長くなっているのであれば、まず分離できないかを検討します。 before public void Setup() { // UI、コンポーネントの初期化が数行に渡って書かれる ... // ロードの準備が数行に渡って書かれる ... // ロード処理が数行に渡って書かれる ... } after public void Setup() { SetupComponents(); PrepareLoad(); Load(); } 必要ないものはpublicにしない クラスが、利用側の知る必要のない内部の詳細部分を隠蔽すれば、やりとりがシンプルになりコード全体の複雑性を下げることができます。利用側からみても、使い方がシンプルになり使い勝手が良くなります。 まずprivateで書いてみて、公開する必要があればpublicにする、がいいかもしれません。 ロジックとデータは近くに置く before(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } // コンストラクタは省略 } before(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.Price * purchaseData.Count}円です") このbeforeの例では、データとなるPurchaseDataクラスと、利用側の2つのコードがあります。 データのクラスは、DB由来のモデルクラスと捉えてもいいです。 改善したいのは、Debug.Log内の金額の計算を利用側で行っている点で、ロジックがデータの外側に実装されてしまっています。 after(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } public uint GetAmount() { return Price * Count; } // コンストラクタは省略 } after(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.GetAmount()}円です") afterでは、合計金額を計算するロジックをデータのクラス側に移動しました。 例えばこれに、「軽減税率を適用するか」という「データ」を増やし、消費税計算の「ロジック」の実装が必要になった時に、改修の対象はPurchaseData内のみですので、利用側はロジックの変更を知らなくて済みます。 引数で処理の内容が変わる関数を避ける 以下のように引数を元に処理が変わる関数だと、利用側が関数のロジックを把握する必要があり、ブラックボックス化できません(制御結合になっている)。 制御結合になっている例 public void hoge(bool flag) { if(flag) { // 処理Aが続く } else { // 処理Bが続く } } ただし、システムによっては、制御結合を避けられない場合もあるので、集まっている機能を精査しながらより結合度を下げられないか検討します。 無駄な引数を送らない before例ではスタンプ結合になっています。関数にはできるだけ使うものだけ送るようにします。 before public void SetUserNameLabel(UserData userData) { userNameLabel.text = userData.Name; } after public void SetUserNameLabel(string userName) { userNameLabel.text = userName; } Unity関連 GetComponent、Find系の関数を使わない パフォーマンスに悪いので、事前にSerializeFieldで参照を持たせます。 Update()は使わない コールバックやコルーチンで実現できないか検討をします Animatiorの引数はhash値を使う Tips的な項目ですが、animatorの引数はhash値を使った方がパフォーマンス的に良いです。 Animator.StringToHash で取得した値を保持しておくのがミソ(先輩の言葉を引用) before private void PlayRunAnimation() { animator.Play("run"); } after private readonly int AnimationHashRun = Animator.StringToHash("run"); private void PlayRunAnimation() { animator.Play(AnimationHashRun); } さいごに コードの可読性、保守性、安全性や柔軟性を上げるための手法、設計の原則は今回記事に取り上げたことの他にも様々あると思います。 業務でのプルリクエストでのコメントで得た学びの他、「リーダブルコード」と「プリンシプルオブプログラミング」からも執筆にあたって参考にしました。これらの書籍は1年目で読んだのですが、とても勉強になりました。 今回は局所的なコードの改善事例を取り上げましたが、今後インタフェースを使った実装の分離や、デザインパターンを用いた実践なども体系的にまとめてアウトプットしてみたいです。 アドベントカレンダーの明日の記事は @kai_yamamoto です。お楽しみに! 参考文献
- 投稿日:2021-12-03T10:49:33+09:00
【トラブル回避】端末依存のC#処理に注意!
本記事は、サムザップ Advent Calendar 2021の12/7の記事です。 はじめに C#の一部APIには、端末の設定によって結果が変わる挙動をするものがあります。 今回はそんな危うい挙動をする処理と、それを端末の設定に依存させないで行う方法を紹介したいと思います。 端末の言語設定に依存した処理 float n = float.Parse("1.5"); これは1.5という文字列をパースしてfloatへ変換する何の変哲もないコードですが、 端末が特定の言語に設定されている場合、以下の例外が投げられて処理は失敗してしまいます。 FormatException: Input string was not in a correct format. これは小数点の表現がカルチャによって異なることと、デフォルトではカルチャは端末の言語設定を参照していることに依るものです。 [ws][sign] [integral-digits[,]]integral-digits[.[fractional-digits]][e[sign]exponential-digits][ws] . A culture-specific decimal point symbol. Single.Parse Method (System) | Microsoft Docs 例えば端末の言語がフランスに設定されている場合、小数点はカンマ(,)で表現されるため、 1.5を浮動小数点数として正しくパースすることが出来ません。 この言語設定への依存は、明示的にカルチャを指定することで解消出来ます。 以下のコードでは、float.Parseメソッドの第2引数へ特定の国/地域に依存しないカルチャ1を指定しています。 float n = float.Parse("1.5", CultureInfo.InvariantCulture); なお今回はfloatを例に挙げましたが、同じ浮動小数点数のdoubleはもちろん、 DateTime, DateTimeOffsetなどのパースも端末の言語設定の影響を受けます。 Unity特有の事情 ちなみにUnityではAndroid向けにビルドしたアプリで、デフォルトのカルチャがInvariantCultureとなるバグ2がありました。 このバグは修正されており、Unity 2020.1.5f1以降ではちゃんと端末に設定された言語のCultureInfoオブジェクトが返されます。 従って同じAndroid端末・同じ言語設定(例では日本語)で以下のコードを実行した場合、 Debug.Log(CultureInfo.CurrentCulture.DisplayName); Unity 2019.4.15f1では、 Invariant Language (Invariant Country) Unity 2020.3.14f1では、 Japanese (Japan) と異なる結果が出力されます。 もしパース処理などでデフォルトのカルチャがInvariantCultureであることに依存していたコードがある場合、 古いUnityからバージョンアップをする際には注意が必要でしょう。 端末のタイムゾーンに依存した処理 少々わざとらしいコードですが、以下の処理も端末のタイムゾーンによって結果が異なります。 // toの指す日付とfromの差を出力する var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var delta = to.Date - from; // to.Dateでtoの日付部分を取得する Debug.Log(delta.ToString()); 例えば端末のタイムゾーンが日本標準時(+09:00)の場合は、想定した通りに2日と出力されますが、 2.00:00:00 中央ヨーロッパ時間(+01:00)に設定していた場合は、2日と8時間と出力されます。 2.08:00:00 何故この差異が生じるかというと、 DateTimeOffsetのDateプロパティは元々のタイムゾーンが反映されないのと、 The value of the DateTime.Kind property of the returned DateTime object is always DateTimeKind.Unspecified. It is not affected by the value of the Offset property. DateTimeOffset.Date Property (System) | Microsoft Docs DateTimeとDateTimeOffsetの減算は、DateTimeがDateTimeOffsetに変換されてから行われますが、 その変換の際に端末に設定されたタイムゾーンが採用されてしまうことによります。 If the value of the DateTime.Kind property is DateTimeKind.Local or DateTimeKind.Unspecified, the date and time of the DateTimeOffset object is set equal to dateTime, and its Offset property is set equal to the offset of the local system's current time zone. DateTimeOffset.Implicit(DateTime to DateTimeOffset) Operator (System) | Microsoft Docs 端末のタイムゾーンが中央ヨーロッパ時間(+01:00)のときの処理を一つ一つ追っていくと、 var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); DateTime a = to.Date; // 1. 2021-12-03 00:00:00, Kind=Unspecified DateTimeOffset b = a; // 2. 2021-12-03 00:00:00+01:00 TimeSpan delta = b - from; // 3. 2021-12-03 00:00:00+01:00 - 2021-12-01 00:00:00+09:00 Dateプロパティで日付部分を表すDateTimeインスタンスを取得(DateTimeKindはUnspecified) DateTimeインスタンスのDateTimeOffsetへの暗黙の変換、DateTimeKindはUnspecifiedなので、システムのタイムゾーン(+01:00)のオフセットが設定される 差の計算の日時をUTCに換算すると、2021-12-02 23:00:00 - 2021-11-30 15:00:00 となる 結果、差が2日と8時間になってしまっていることが分かります。 端末のタイムゾーン設定に依らず正しい結果を得たい場合は、 日付を取り出す際に元々のオフセットを指定して、明示的にDateTimeOffsetをインスタンス化する必要があります。 var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var delta = new DateTimeOffset(to.Date, to.Offset) - from; Debug.Log(delta.ToString()); これで意図した通りに2日と出力されます。 2.00:00:00 最後に 同じ環境で開発と動作確認を行っている場合、見逃されがちな端末の設定に依存した処理を紹介いたしました。 この記事が堅牢なシステムを構築するための助けになれば幸いです。 参考 .NET のカルチャー依存 API 問題 | ++C++; // 未確認飛行 C ブログ .NET での数値文字列の解析 | Microsoft Docs DateTime と DateTimeOffset 間の変換 | Microsoft Docs CultureInfo.CurrentCulture always returning Invariant Language (Invariant Country) - Unity Forum CultureInfo.InvariantCulture プロパティ (System.Globalization) | Microsoft Docs ↩ Unity Issue Tracker - [Android] System.Globalization.CultureInfo.CurrentCulture returns Culture Invariant when is being built on Android device ↩
- 投稿日:2021-12-03T10:42:50+09:00
【Unity】後回しにすると大変なことになった事例集
はじめに 本記事は、サムザップ Advent Calendar 2021 の12/8の記事です。 経験上 私は、Unityを使ったゲーム開発をしております。 ゲーム開発では、モック開発中にゲームをプレーできるように優先して開発してしまいますが 開発しているときから常に意識してほしいことを説明したいと思います 後回しにすると大変になった事例をいくつか紹介します 意識してほしいこと 画面遷移 モック中に複数の画面の機能を量産して動く形に仕上げ、 デザインも適応させ見た目の確認をすることが多いと思います。 量産ばかりしていていざ本開発にむけて仕上げる状態になったときに やりたい挙動ができなくなることがしばし見受けられます。 画面遷移中にインタラクションを入れたいけど入れにくい ダイアログを出す仕組みが各々違うからインタラクションが入れにくい Androidのバックキーを入れたいけど戻るボタンの処理が各々違うからいれにくいなどなど 上記のようなことをおろそかにすると、本開発中にチームメンバーが増えて さらにページ数が増えてくると修正が困難になりがちです。 最初にページの遷移やダイアログの遷移のベースを先に作ってしまうことをおすすめします 設計のポイント ページの遷移やダイアログを(Prefab単位で実装するか複数のシーンを使って実装するか) Unityだとどちらかの方法を使って実装することが多く見受けられます。 私の経験上だと、Prefabを使って構成することが多い印象ですが どちらの方式でつくるか決めます 次に挙動を決める。 設計によって、細かく挙動がかわると思いますが。ここでは例として列挙します ページに移動する 1. 初期化する 2. アニメーションを入れる 3. 画面を表示する ページから離れる 1. アニメーションを入れる 2. 画面から離れる 3. 後始末(メモリー等やアセットの解放など) 上記のような処理で設計した場合にそれぞれをベースコードとして実装しておくことで すべてのページが同じ挙動になります。 アセット管理 モック開発中に乱雑に配置してしまったアセットですが、 これらは大体が使われない素材になりがちです。 ここも適当に配置して放置しまっていると本開発するというフェーズの時に アプリサイズが肥大化してしまうという問題をよく拝見します。 すべての素材をResources上に配置してしまっている Atlasデータと素材がResources上に一緒に置かれてしまっている 圧縮形式をきめていない モバイル以外のプラットフォームにも考慮しておく(Windows)など ポイント 素材類は、Resources上に配置せずに別フォルダーに入れておく。 Atlasデータは、Resources上に配置しておく。 Assets/ ├ Sandbox/ │ └ Mock/ │ └ UI/ << 開発中はこちらに配置 │ └ Release/ │ └ UI/ << 本開発になったらこちらに配置 ├ Resources/ │ └ Atlas/ << こちらにAtlasデータを配置して、AtlasのPackingフォルダーを適宜変更 今回の例は、Atlas例にしていますが、他の素材も同様にわけておくことをおすすめします 圧縮形式について 圧縮形式をきめておかないと、見た目の印象から変わってしまいますし アプリサイズにも影響してしまうのでここも抑えておきます 圧縮形式(モバイル) 用途 注意点 ASTC 4x4 UIパーツ(Atlas) 2048 x 2048に収める ASTC 4x4 他のテクスチャ PCでも考慮してDXT5にする予定なので4の倍数にする 上記は一例ですが、あらゆる素材で構成されますので チームで相談しつつ適切に設定することをおすすめします アセットバンドル管理 ゲーム開発において、アセットバンドルにして外部で管理することが多くなると思います。 乱雑に配置してしまっていると肥大化してしまう傾向になりがちです。 肥大化するとダウンロード時間の配慮も必要になってきてしまいます ポイント アセットは、フォルダパック管理で管理する アセットサイズが大きい素材は、ファイルパックにして分割しておく 並列ダウンロードの実装もしておく この3点を抑えておくだけ、効果が違います。 Assets/ ├ AssetBundles/ │ └ character/ │ └ 1/ │ │ head │ │ body │ │ hair │ │ background │ │ icon │ └ movie/ │ └ 1/ │ │ sample1 │ │ sample2 │ │ sample3 │ │ sample4 │ │ sample5 上記のような構成でアセットバンドルに管理されているという例で説明します キャラクターの素材を下記のように管理した場合 パック形式 AssetBundle Label 補足 フォルダパック character/1 一つで5つの素材を管理(一つの素材が数K Byte) ファイルパック character/1/headcharacter/1/bodycharacter/1/haircharacter/1/backgroundcharacter/1/icon 5つをそれぞれ管理 ムービーの素材を下記のように管理した場合 パック形式 AssetBundle Label 補足 フォルダパック movie/1 一つで5つの素材を管理(一つの素材が数M Byte) ファイルパック movie/1/sample1movie/1/sample2movie/1/sample3movie/1/sample4movie/1/sample5 5つをそれぞれ管理 例として、それぞれを10万台の端末が一斉にダウンロードを開始した場合 パック形式 素材 補足 フォルダパック キャラクター 10万台 × 1コネクション ファイルパック キャラクター 10万台 × 5コネクション フォルダパック ムービー 10万台 × 1コネクション ファイルパック ムービー 10万台 × 5コネクション フォルダパックのほうがコネクション数が少なく済むので ダウンロードが高速になりそうですが ムービーは数M Byte × 5のファイルサイズになるので ファイルサイズが数M Byte以上あると一つのダウンロード時間が 遅くなる傾向が経験上体験しましたので 並列ダウンロードできる仕組みを用意しておくをおすすめします ポイントまとめ アセット管理は小さいサイズでまとめて管理できるならフォルダパック 大きいサイズになるなら、ファイルパックして分割ダウンロード アセットバンドルでは一つの素材でもフォルダパックで作成しておき、 大きいサイズになるものはファイルパックで管理することをおすすめします フォルダパックにしておくことで、後で追加しやすくなります。