YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

URPのライティングについて

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
前に書いてあったライトの知見を共有します。

目次


ライト周りで困ることは多い


Unityで負荷の少ない環境を作るとき影の解像度をいじったりしたくなる。
しかしこのあたりがしっかりとまとまっているサイトは意外と多くない。

今回はまとめていきたい。

前提条件


URP

Unity 2021.3以降

何によってライトが決まるのか


実際にライトが対象に当たるとき光源となるのは何なのか、
何が影響して光っているように見えるのかいくつか要因がある。

レンダリング順に並べると下の画像の様になります。
設定でいうと
レンダリングパイプラインの種類
・Lightingウィンドウの設定
・メッシュのEmission設定(静的・動的)
・メッシュの設定(静的・動的)
・ライトソースの設定(静的・動的)
・プロジェクト設定のクオリティ
・プロジェクト設定のグラフィックス
・ポストエフェクトの設定
あたりを見れば良い
様々な設定があるが、
まずは簡単に説明できる動的ライティングの説明から。

動的ライティング


まずライトソースがリアルタイム or Mixになっていると動的(リアルタイム)な影を生成するようになる。
また影を作るオブジェクトが静的(static)とマークされていないことでリアルタイムに影を生成する。
(※Lighting設定でmixになっていると挙動が変わってくるので注意)

ライトの質をコント―ロールするにはURPアセットの編集が必要。
(プロジェクト設定のグラフィックスのAssetに設定されている)

MainLight→メインライトのリアルタイム描画を有効にするかどうか
Cast Shadow:影のON/OFF
Shadow Reslution:影の解像度
Additinonal Lights→追加ライトのリアルタイム描画を有効にするかどうか
影の解像度(Shadow Reslution)はShadowのDistanceベースで貼り付けが行われる。
なので影の解像度を高くしたければ、Shadow Reslutionを上げるか、Distanceを縮めると良い。
Cascade:距離に応じて影の解像度を変える設定。
Depth BiasとNormal Bias→シャドウアクネと呼ばれるアーティファクトが発生しないように補正処理を行うものらしい。
処理負荷が高いので要調整。

静的ライティング


ライトは動的に生成すると重い。
そのため、一度影の計算をしておいてシャドウマップ・ライトマップとして保存しておく事ができる。
静的ライトを利用するには、影を落とすオブジェクト(メッシュ)を静的(Static)にする必要がある
また、ライトソースの設定をミックスorベイク(baked)にする必要がある

LightingウィンドウのSceneタブのMixedLighing→BakedGlobalIlluminationにCheck☑をいれ、Generate Lighingのクリックでベイクが実行される
オブジェクトに対するライトマップの解像度は、オブジェクトごとに決められる。
ライトマップScaleというパラメーターがメッシュレンダラーに存在する。
(小さくすると荒くなる)

ライトマップ全体の解像度はMaxLitghMapSizeにて決められているので遠くのオブジェクトは解像度を低くして近距離は高くすると良い。
また、小さな静的オブジェクトはディティールまで見ないため、ソレで大きなライトマップの範囲を使用するのはもったいないため、メッシュレンダラーでライトプローブへのベイクをOFFにすることを推奨。

(Mixライティングでライトプローブの影響を受けるようにすれば小さいオブジェクトまでベイクする必要がなくなる。ライトプローブについては後述)
また、LightingウィンドウのSceneタブの
Direct Smples(直接光サンプル数),Indirect Samples(間接光サンプル数),Environment Samples(環境光サンプル数)にてサンプル数を設定できる。
使用するユニットのテクセルに依存する。
Bounces→光の跳ね返りの回数
Filtering→光の到達工合が変わってくるため、フィルタで自然な影に加工する方法
(Autoが自動でAdvanedが上級向け設定)
フィルタリングはuvチャートという単位で適用されるらしい。
(今回は上級向け設定は割愛)

混合ライティング


混合ライティングは動的ライトと静的ライトをうまく組み合わせてライティングするもの。
MixするにはパイプラインAssetにて混合ライティング(Mixed Lighting)をONにする必要性があり、ライトソースの設定をミックスにする必要がある。
種類は主に3つ。

  • Baked indirect(間接光のベイク)
  • Subtractive(減算モード)
  • Shadow musk(シャドウマスク)/Distance Shadow musk(ディスタンス(距離)シャドウマスク)

Shadow muskだけShadow muskとDistance Shadow muskの設定に分かれる。
種類はLightingウィンドウのシーンのタブ、LightingModeで指定が必要。
また混合ライティングを実現するためにベイクライトを動的オブジェクトに反映させる方法がある。
こちらはライトプローブの説明にて解説。

Baked indirect(間接光のベイク)


間接光のみをベイクし、影や直接光は通常通りリアルタイムに処理を行う。
リアルタイムのため遠距離の影は生成されない。
同じシーンでディレクショナルライトをOFFすることによって夜を表現出来たりする。
遠距離を見る必要がない、上から視点や霧の濃いゲームに適している。

Subtractive(減算モード)


静的なオブジェクトほぼ全てをベイク。ライト、影、間接光。
リアルタイムなオブジェクトにも影を落とすことが出来る。
減算でベイクするため要影色調整。(リアルタイムな影の色と差異が出てしまう)
また光沢情報はライトマップの解像度に依存するため、静的オブジェクトに対する光沢(スペキュラ)情報が大きく失われることになる。
(動的なオブジェクトはベイクしないので、ライトプローブ設定を細かにすることで明るさを反映させる必要がある)
(シャドウオンリィというオプションもあるがココでは説明割愛)
とにかく軽くしたい場合に有効。

Shadow musk(シャドウマスク)/Distance Shadow musk(ディスタンス(距離)シャドウマスク)


Shadow muskは大きく2種類
静的なオブジェクトの影の情報と間接光の情報のみベイク。
静的なオブジェクトのシャドウマスクを事前に生成。
(動的なオブジェクト以外は)質感表現はリアルタイムのため光沢(スペキュラ)もきれいに出る。
(ShadowRadiusによりソフトな影も可能)
影と事前ベイクの影が同じ質感で影を落とす。
静的なオブジェクトの影はダイナミックなオブジェクトに影をおとさない。
(ライトプローブ設定を細かにすることで明るさを反映させる必要がある)
(シャドウオンリィというオプションもあるがココでは説明割愛)
1オブジェクトに対して4つしかライトが割り当てられない。
(マスクにRGBAのテクスチャを使っている関係上)
(ダイナミックな影とは相性が悪いが)Shadow muskはかなり綺麗な影を落とすので画にこだわりたい場合に有効。
Distance Shadow muskは広いステージで違和感なく表示できるので広いステージを作る場合に有効。
Distance Shadow musk(ディスタンス(距離)シャドウマスク)
近距離はリアルタイムシャドウで、遠距離はシャドウマスクのものを使うオプション
プロジェクト設定のクオリティにて設定可能

明かりの種類


(なお、やまだたいしによる分類です)

1.ライトソース


直接光(Direct lighting)

  • ポイントライト
  • スポットライト
  • リアライト

距離減衰が存在する光源。
電球やスポットライトなどで使われる。
それぞれエリアライトはリアルタイムで使えないなど様々な制約がある
光の跳ね返りなどは含まない。
距離減衰をし、距離に応じて影の濃さが変わる。

ディレクショナルライト(指向性ライト,directional lighting)

  • 太陽光など大きな光

距離減衰をしない平行な光。
何処から光が指しているかにかかわらず影を作る。
本物の太陽は距離減衰をするが、Unityでは距離減衰をしないライトを使用する。

2.エミッション(Emission)


エミッシブマテリアル。光るオブジェクト。
Materialが直接自己発光してるかのように見えるオブジェクト。
設定により、単純にBloom処理されるだけか、ライトとして処理されるか変わる。
光源として利用したい場合、
エミッションプロパティを有効にし
Lightmap Staticに設定する必要がある。
RealtimeGIで利用するか、BakedGIで利用するか選択する。
(選択したGIが有効でないと動作はしない)

Bloomの調整

HDRPならパラメーターの
Use Emission Intensity をチェックして
Emissive Color に色を指定、
Emission Intensity の指定
Exposure weightの指定を行い露出の強さを変更をすることで調整が出来る
URPではポストエフェクトで調整が必要
URPAssetにてHDRを有効化し
MaterialにもHDR(ハイダイナミックレンジ)の色指定が必要
(RGBに1以上の値を格納するためで1を超えると発光する)
またポストエフェクトにてBloomのポストエフェクトをかける

3.アンビエントライト(Ambient light,環境光)


拡散環境光(アンビエントライト)として知られているライトは 、シーン周囲すべてに存在するライト。
URPではLighingウィンドウのEnvironmentタブのEnvironmentにて設定が出来る。
シーン全体を暗くしたい場合、この設定のライトを暗くする必要がある。

4.間接光(Indirect lighting)


光が反射した結果出る光。
ライトから出た光が1回以上反射してから届く光。

ライトの種類についてまとめ


4は1~3までの結果を元に下記GIにて生成される。
Lightingウィンドウの設定によって挙動が変わってくる。
最初に大雑把にリアルタイムで
ライトを配置してどのようなイメージになるかシュミレーションしてから
各ライトや設定をベイクor Mixに変更し調整するのをオススメしたい。

グローバルイルミネーション(大域照明,GI,Global illumination)


空間表現をシミュレーションする。
まっとうな間接光では計算が長いためソレを軽量化するための仕組み。
簡単にいうとUnityでは間接光(照り返し)に関する全体設定。
GIにはいくつかの処理がある。
Unityではベイクした/動的なライトマップ、放射照度のボリューム、光伝播ボリューム、ベイクした/動的なライトプローブ、ボクセルベースのGI、距離フィールドベースのGIなどがある。
Unity内部ではEnlighten というソフトを利用しているらしい
(リアルタイム以外の利用は非推奨。リアルタイムGIではコレが利用されるが後に廃止になるかも?)
Progressive CPUというのが現在は主流。
(Progressive GPUに変わってくるかも)
GIが無効の場合、静的ライトは動作しない。
リアルタイムGIはGI情報が随時更新される。
昼→夜など変わる場合に有効。

ライトマッパー(lightmapper)


光線を射影し、ライトバウンスを計算し、結果のライティングをテクスチャに適用する。
ライトマップとライトプローブのデータを生成。

ライトマップ


3Dメッシュ上の光の強度や色をあらかじめ「ライトマップ」というテクスチャに焼き込んでおくもの。
プログレッシブライトマッパーというツールを使って生成。
(プログレッシブライトマッパーはパストレーシングに基づく高速のライトマッパーシステム )

ライトプローブ


ライトマップ同様、ライトプローブはシーンのライティングに関する “ベイクした” 情報を格納します。
両者の違いは、ライトマップはシーンの サーフェス に当たるライティングの情報を格納するのに対し、ライトプローブはシーンの なにもない空間 を通り抜けるライトの情報を格納する。
つまり、ベイクライトを動的オブジェクトに反映することが出来る。
ライトプローブはライトプローブグループをシーンに配置することでベイクすることが出来る。
動的オブジェクトは最も近くにあるライトプローブからサンプリングを行い、ライトの結果に合成する。
ライトプローブは影は提供されない、ライティング情報と明かりの程度、色を提供する。
利用するライトプローブは1つではなく4つほど。

リフレクションプローブ


金属の反射に関する部分です。
割愛。
要望があるなら書きます。

参考記事


  

まとめ


様々な要素が絡み合うが、基本的な要素は単純。
一度使い方を理解してしまえばスマホ、コンシューマー様々なケースに対応した適切なライティングを設定できると思った。

がんばえー

【前編】UnityのAddressableを個人制作で使いこなす

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
UnityのAddressablesを個人でも導入したいと思い今回はそれの備忘録です。

目次


Addressablesを個人でも導入したいわけ


Unityでアセットを読み込む場合いくつかの方法がある。

・Unityのシーンファイルに紐づけてしまう方法
・Resourcesに格納してしまう方法
・Streaming Assetsとして格納してしまう方法
・AssetBundlesとして管理する方法
・Addressablesを使う方法
・ContentLoadModuleを使う方法

後、例外的にEntitiesはContentLoadModule上に独自システムが構築されており、Content managementという名前の機能がある。

現在企業でも使われている方法がAddressablesだ。
AssetBundlesを使っている企業もいるがAssetBundlesは使いづらくAddressablesへ移行が進んでいる。

特段気にせず、Unityシーンに全て紐づけてもいいが、アプリのサイズが大きくなってしまう問題点や、
Unityのシーンファイルを開いたときのメモリ使用量が格段に上がってしまう。

次に考えるのはResourcesだが、Unityとしては非推奨になっている。

learn.unity.com

個人的には個人開発者であればResourcesでも構わないと思うが、
ストアのアプリを更新せずに中のリソースを更新したりできる利点などもあるので是非に一歩先に行きたい個人開発者は導入を考えたい。

また、多言語対応する場合はこの機能を使うのが主流となっている。

今回は私の勉強がてらAddressableについて深堀りしていこうと考えている。

正直使うだけなら他の方々のブログを参考にしていただいたほうが良いだろう。
www.hanachiru-blog.com

そもそもAddressablesとはなにか?


アセット管理システム。
非同期ロードを使用して、あらゆる依存関係のコレクションが存在する、任意の場所からロードを行うことができる。

unity.com

どうやって管理していくか


Addressableはかなり複雑だ。
しっかり管理するならCDNも必要だし、何と言ってもメモリ管理が複雑になる。
まずは手始めにローカルのリソースを使う方法を調べていく。

ダウンロード


まずは PackageManager によりインストールを行います。
現在(2023/12/23)インストールできるもので最新は2.0.6となっている。
安定版は1.21.19だ。

docs.unity3d.com

インストールすると、以下のようなメニューが追加される。

Create Addressables Settingsをクリック。

するとAssets配下にAddressableAssetsDataという名前でAddressablesを利用するためのデフォルト設定で初期設定ファイル郡が生成される。

初期設定


Addressablesの初期設定はいくつかある。
各種ScriptableObjectで構成され保存されている。

AddressableAssetSettings


基本設定だ。

docs.unity3d.com

Profile(プロファイル)

どうやってAddressablesを使うかの設定。

  • Local
    ローカルでの設定。
  • Remote
    リモートでの設定。
  • BuildTarget
    ビルドターゲットの名前
  • New Entry
    ビルドしたアセットの保存先

LocalとRemoteで設定できる内容はバンドルの場所だ。
ちなみにバンドルとはAddressablesの対象としたアセットのことだが、圧縮済みのビルド済みデータのこと。
Local設定でありながらリモートの場所の設定ができたり、
その逆、Remote設定でありながらLocalの指定ができたりする。ややこしい。

  • Built-In
    ローカルコンテンツ用の場所プレイヤービルドに自動的に含まれる
  • Editor Hosted
    エディターの [ホスティングサービス] で使用するパス定義
  • Cloud Content Delivery
    Cloud Content Deliveryのパス。ちなみにCloud Content DeliveryとはUnity公式のCDNみたいなもの。
  • Custom
    自由に設定ができる。

ホスティングサービスってどこのことを言っているかというとココ

今回はLocalで使うのでどちらとも一旦ビルドインにしておく。

ちなみにLocalとRemoteどちらを使うのか指定は別途する箇所があるのだがコチラは後述する。

Diagnostics (診断)

ログ出力のためのオプション。

  • Send Profiler Events
    プロファイラーイベントを有効化
  • Log Runtime Exceptions
    アセットのロード操作で発生したランタイム例外をログに記録

Catalog (カタログ)

どこからなんのアセットをとってくるか情報が書かれているCatalog,そのカタログの設定。
ちなみにLocalのみでアセットを管理している場合はCatalogの存在はほとんど意識する必要はない

  • Player Version Override
    カタログのPlayerVersionの上書き
  • Compress Local Catalog
    Localのカタログの圧縮設定をするか
  • Build Remote Catalog
    リモート カタログを構築設定.
    読み込む場所を指定する。
  • Only update catalogs manually
    リモート カタログの自動チェックが無効化

Update a Previous Build(ビルド更新設定)

  • Check for Update Issues
    Check for Content Update Restrictions ツール(アセットに問題があったときに対処するツール)で
    どのように対処するかの設定?もしくはどのような場合に更新させるかの設定。読んでもよくわからんかった。
    List Restricted Assets (recommended) と書いてる通りList Restricted Assetsが推奨そうなのでList Restricted Assetsで良さそうだ。
    多分リストに差分があった場合に更新してくれる。
  • Content State Build Path
    コンテンツ状態ビルド先の設定

Downloads (ダウンロード)

  • Custom certificate handler
    Httpsの証明書確認用。
    Unityengine.Network.CertificateHandlerクラスを拡張して実装するらしい。
  • Max Concurrent Web Requests
    最大WebrRequest数,2~4 が最適な同時リクエスト数らしい。
  • Catalog Download Timeout
    カタログダウンロードのタイムアウト時間の設定。
    0はタイムアウトなし.

Build (ビルド)

  • Build Addressables on Player Build
    Addressableがプレイタービルドの一部としてビルドされるかどうか?
    Build Addressables content on Player Build→プレイヤーのビルド時に常に Addressables コンテンツをビルドする
    Do not Build Addressables content on Player Build→Addressables コンテンツをビルドしない。Addressables コンテンツを変更した場合は手動でのビルドが必要。
    Use global Settings (stored in preferences)→ エディターの環境設定に従う。
  • Ignore Invalid/Unsupported Files in Build
    無効なファイルがあったときの挙動の指定。
    チェックがある場合はビルドを中止するのではなく、それらのファイルを除外
  • Unique Bundle IDs
    ビルドで一意のバンドル名を作成するかどうか
  • Contiguous Bundles
    効率的なバンドルレイアウトを作成するかどうか。
  • Non-Recursive Dependency Calculation
    有効にすると、アセットに循環依存関係があるときにビルド時間が短縮
    注意点:循環依存関係によっては、このオプションを有効にするとロードできない場合があるらしい.

Build and Play Mode Scripts (ビルドスクリプトと再生モードスクリプト)

Play Mode Scriptでの読み込み設定
どのようにビルドプロセスを処理をさせるかそれぞれ処理が入っている。
特に弄ることはなさそう。

Asset Group Templates(アセットグループテンプレート)

作られるアセットグループのテンプレ設定を置いておく。

Initialization Objects(初期化オブジェクト)

Addressables.InitializeAsync呼んだときの初期設定をここに詰め込むっぽい。
Unity公式として設定があるのはキャッシュ初期設定だけだけど、
ユーザーでカスタマイズしてScriptableObject形式で処理を追加できるらしい?
特段触る必要はなさそう。

Group settings(グループ設定)

Addressablesにはそれぞれグループごとに設定ができる。

Content Update Restriction

  • Prevent Updates(アップデート禁止)
    チェックが入っている場合、バンドル内のアセットが変更されている場合のみ、バンドル全体が再構築
    チェックがない場合は毎回上書きされる

Content Packing & Loading

  • Build & LoadPaths
    biuld path設定.
    Profile(プロファイル)で設定した内容

  • Advanced Options

    • Asset Bundle Compression
      圧縮形式の指定
    • Include In Build
      このグループのアセットをコンテンツビルドに入れるかどうか。
    • Force Unique Provider

    • Use Asset Bundle Cache
      リモート配布するバンドルをキャッシュするかどうか。

    • Asset Bundle CRC
      ロード前に、バンドルの整合性を確認するかどうか
      Disabled(無効)、Enabled, Including Cached (有効、キャッシュされたものを含む)、Enabled, Excluding Cached (有効、キャッシュされたものを除く)
    • Use UnityWebRequest for Local Asset Bundles
      ローカル AssetBundle アーカイブをロードするときに、AssetBundle.LoadFromFileAsync ではなく
      UnityWebRequestAssetBundle.GetAssetBundle を使用
    • Request Timeout
      リモートバンドルのダウンロードのタイムアウト間隔。
    • Use Http Chunked Transfer
      バンドルのダウンロード時に HTTP/1.1 チャンク転送エンコーディング方式を使用するか
      非推奨らしいので使わなくていい。
    • Http Redirect Limit
      バンドルのダウンロード時のHttpリダイレクト許容数。-1だと無制限.
    • Retry Count
      ダウンロードが失敗したときに再試行する回数。
    • Include Addresses in Catalog
      カタログにアドレス文字列を入れるかどうか
      入れない場合はカタログサイズが小さくなるらしい.
    • Include GUIDs in Catalog
      カタログに GUID 文字列を入れるかどうか。
      AssetReference を使用してアセットにアクセスするなら必須らしい。
      入れない方が軽量化できるらしい
    • Include Labels in Catalog
      カタログにラベル文字列を入れるかどうか
      ラベルを使用しない場合にOffにできる、まあない方が軽量化できるらしい
    • Internal Asset Naming Mode
      AssetBundle 内のアセットの ID を決定の指定
      Full Path: プロジェクト内のアセットのパス
      Filename: アセットのファイル名(もちろん同一ファイル名禁止)
      GUID:GUID
      Dynamic:(文字情報がすくなくなるから)推奨らしい グループ内のアセットに基づいて作成できる最も短い ID
    • Internal Bundle Id Mode
      AssetBundle が内部的にどのように識別されるかを決定の指定
      Group Guid:推奨グループの一意の ID
      Group Guid Project Id Hash:グループ GUID とクラウドプロジェクト ID
      Group Guid Project Id Entries Hash: グループ GUID、クラウドプロジェクト ID
    • Cache Clear Behavior
      キャッシュから AssetBundle を消去するタイミングを決定
      ClearWhenSpaceIsNeededInCache :キャッシュにスペースが必要な場合はクリア
      ClearWhenWhenNewVersionLoaded:新しいバージョンが正常にロードされると、バンドルはキャッシュから削除
      古いの残ってても仕方ないしClearWhenWhenNewVersionLoadedでよくね?
    • Bundle Mode
      バンドルにパックする方法の指定
      Pack Together: すべてのアセットを含む 1 つのバンドルを作成
      Pack Separately: グループ内のプライマリアセットごとにバンドルを作成
      Pack Together by Label: 同じ組み合わせのラベルを共有するアセットのバンドルを作成
      うーん、特にこだわりが無ければPack Togetherか?
    • Bundle Naming Mode
      AssetBundle のファイル名を作成する方法の指定
      Filename: グループ名から派生した文字列
      Append Hash to Filename: グループ名から派生した文字列+バンドルハッシュを付加した文字列
      Use Hash of AssetBundle: バンドルハッシュ
      Use Hash of Filename: Filenameから計算されたハッシュ
    • Asset Load Mode
      アセットを個別にロードする (デフォルト) か、グループ内のすべてのアセットを常にまとめてロードするか指定
      Requested Asset and Dependencies(Requestアセットとその依存)が推奨らしい

    • Asset Provider
      「AssetBundle内のアセット」をロードするためのプロバイダークラスを定義
      カスタムで作ってなければAssets from Bundles Provider

    • Asset Bundle Provider
      「AssetBundle」をロードするためのプロバイダークラスを定義
      カスタムで作ってなければ AssetBundle Provider

だいぶ理解が進んだ


だいぶ理解が進んだが、行数も多くなってきたので今回の記事はココまでにしておく

今回ばかりは長くなりそうな予感。

【追記版】いいかげんエクセルでマスターデータ管理辞めません?UnityでのJson,SQLiteを使ったマスター管理

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
前回で終了しようと思ったのですが、
最終的な使い方が詳細にわかりづらい、
実際に使うには想像しづらい
と思い実務に耐えゆる設計をしたリポジトリを公開することにしました。

前回の記事はこちら
orotiyamatano.hatenablog.com

目次


公開リポジトリ


github.com

リポジトリの解説


基本的には前回、前々回で書いた解説通りの実装をしています。
その差異を今回は解説します。

  • VContainerの利用

今回はVContainerを利用しDIすることでシングルトンを避けています。

  • ProjectSettingの追加

様々なPath情報をSQLiteDataBaseSettingsというScriptable Objectに逃して、
それを参照するように改変しました

  • プリプロセッサディレクティブの追加(ついでに設定を変更するエディタ機能も)

プリプロセッサディレクティブを参照することで、SQLiteを使うかMaster Memoryを使うか出し分けができるようになりました。
Connection_MasterMemoryというシンボルを追加するとエディタでもMasterMemoryを参照するようになります。
(ビルド時はMasterMemory参照)

また、それに伴いシンボルを追加できるエディタ拡張も追加。

  • Addressablesの追加

MasterMemoryを追加しMasterMemoryの直読みをやめました。

以上です。

まとめ


これで大分実務に耐えゆる設計になったかと思います。
使ってみてください。

【後編】いいかげんエクセルでマスターデータ管理辞めません?UnityでのJson,SQLiteを使ったマスター管理

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
後編です。 前編はこちら orotiyamatano.hatenablog.com

目次


1. DBeaverのダウンロード
2. SQLiteに必要なセットをインストールする
3. DBeaverでデータをつくる
4. SQLiteとUnityの接続設定
5. スクリプトからの一時データの書き出しとSQLiteデータの更新
6. MasterMemoryの設定
7. MasterMemory用に書き出し
8. MasterMemoryで読み込む

前編では5. スクリプトからの一時データの書き出しとSQLiteデータの更新までやりました。
後編は最後までやっていきましょう。

導入手順

6. MasterMemoryの設定


MasterMemoryの導入に移ります。

github.com

UnityのMasterMemoryはMessagePack依存ですので
MessagePackのunitypackageをダウンロードインポート(Getting Started(Unity)を読んでね)し、
次にMasterMemoryのパッケージをインポートします。

Unity上で動かすのはmpc.zipとMasterMemory.Generator.zipが必要になるのですが、
導入も面倒なので今回は.NET Coreの方で利用しようと思います。

(早くソースジェネレーター対応して欲しいですね。してくれたらこの辺考えなくて良いのですが……)

本記事で解説しても良いのですが、
もうインストール方法を書いている方がいるので参照させていただきます。
light11.hatenadiary.com

↑に倣ってインストール完了したらマスターメモリ用のクラスを作っていく。
とその前にSQLiteとMaster Dataとで同じような構成のクラスができるようになる。
同じように扱うためにInterfaceを用意する。

public interface ITestTable
{
    public int Id { get; set; }
    public string TextVal { get; set; }
}

前編で作ったTestTable.csにも適用しておく。
そして、Assets/Scripts/MasterフォルダにMasterMemory用のクラスを用意した。

using MasterMemory;
using MessagePack;

namespace Master  // 本記事では必須
{
    [MemoryTable("TestTable"), MessagePackObject( true )]
    public class MasterMemoryTestTable : ITestTable
    {
        [PrimaryKey] public int Id { get; set; }
        public string TextVal { get; set; }
    }
}

(Table名はAttributeから取得するのでクラス名は完全一致の必要はない)

そしてデータを格納するためのフォルダとしてAssets/Scripts/MasterにGeneratedというフォルダを作成。

ProjectRootで以下コマンドを打つ.
するとMasterMemory関連のスクリプトが生成される。
dotnet-mmgen -inputDirectory ./Assets/Scripts/Master -outputDirectory ./Assets/Scripts/Master/Generated -usingNamespace "MasterMemory"

-inputDirectory には↑で作ったクラスが入ったディレクトリを指定します。
-outputDirectory-usingNamespace で指定した名前空間に所属するクラスが生成されます。

次はMessagePackの関連のデータを生成する。
mpc -input ./Assets/Scripts/Master -output ./Assets/Scripts/Master/Generated

これでようやく関連クラスが生成された。

7. MasterMemory用に書き出し


さて、最低限の準備が出来たところで、
最終的にはbyte形式にした方が良いだろうと思うのでTabeDataEditor.csを以下のように改変した。

ClassLoaderに↓も追加

   public static void DynamicCopyPropertiesWithCommonInterface(object source, object target, Type interfaceType)
    {
        foreach (var prop in interfaceType.GetProperties())
        {
            var sourceProp = source.GetType().GetProperty(prop.Name);
            var targetProp = target.GetType().GetProperty(prop.Name);

            if (sourceProp == null || targetProp == null || !targetProp.CanWrite) continue;
            var valueToCopy = sourceProp.GetValue(source);
            targetProp.SetValue(target, valueToCopy);
        }
    }

SQLiteの中に入っているデータをすべてMasterMemory用に変換し書き出す処理として
SQLiteToMasterMemoryボタンを追加した。

SQLiteの値を取得してくる処理をメソッド化して共通化.
GetSQLiteValueメソッドに。

すべてのMasterのNameSpaceとTablesのNameSpaceのクラスを取得し、
同じInterfaceを所持していた場合にGetSQLiteValue で取得したデータをリフレクションを使ってIntaface経由で値を変換している。

最終的にはAssets/Master/Binary/Master.bytes へ保存という流れだ。
多分変換処理は大分重い処理だと予測されるため、やるのであれば頻繁にはおすすめしない。
開発環境ではSQLiteのデータを使いビルドデータではSQLiteをMasterMemoryへ変換されたデータを使うなどと利用分けすると良さそうだと思う。

このスクリプトにはMaster Memoryに新規追加されたクラスの処理は書かれていないので毎回生成コマンドを叩く必要があるが、まぁソースジェネレーター対応がされればその辺考えなくても済むんだろうなぁと思ってます。

(早くきて……)

(どうしても気になる方はmpc.zipとMasterMemory.Generator.zipをUnityに入れてエディタ拡張からそれぞれ生成処理を叩くのを作っても良いかもしれない,もしくはbatファイルを用意して叩くとか?)

6. MasterMemoryで読み込む


さて、書き込み処理が出来たところで、最後、MasterMoemoryのbyteデータの読み込みです。

DBLoad.csを書き換えてそこで読み込むようにしてみます。

using UnityEngine;

public class DBLoad : MonoBehaviour
{
    private async void Start()
    {
        var data = await TestTableRepository.GetDataAsync(1);
        Debug.Log(data.TextVal);
    }
}
using Cysharp.Threading.Tasks;
using Tables;

public static class TestTableRepository
{
    public static async UniTask<ITestTable> GetDataAsync(int index)
    {
        ITestTable data;
#if true // プリプロセッサディレクティブで使いわけしたりデータキャッシュできる仕組みにできるとさらに良さそう
        data = MasterMemoryData.DB.MasterMemoryTestTableTable.FindById(index);
#else
        var path = Application.dataPath + "/db/testdb";
        var db = new SQLiteAsyncConnection(path);
        data = await db.GetAsync<TestTable> (1);
#endif
        return data;
    }
}
using System.IO;
using MasterMemory;
using UnityEngine;

public static class MasterMemoryData
{
    private static MemoryDatabase _db;

    // とりあえず、シングルトンで処理(DIにするともっと良いよね)
    public static MemoryDatabase DB
    {
        get
        {
            if (_db == null)
            {
                DownloadMasterData();
            }
            return _db;
        }
    }

    private static void DownloadMasterData()
    {
        const string binaryPath = "Assets/Master/Binary/Master.bytes";

        var data = LoadBinaryData(binaryPath);
        if (data != null)
        {
            _db = new MemoryDatabase(data);
        }
    }
    
    // Note:実際はAddressablesとかで読んでくると良さそう.
    private static byte[] LoadBinaryData(string binaryPath)
    {
        if (File.Exists(binaryPath))
        {
            var binary = File.ReadAllBytes(binaryPath);
            return binary;
        }

        Debug.LogError("ファイルが見つかりません: " + binaryPath);

        return null;
    }
}

とりあえず、今回はシングルトンで処理してみました。
こちらもSQLite同様問題なく処理できました。

またInterfaceが共通化されているので場合に応じてSQLiteを読むか、
MasterMemortを使うかプリプロセッサディレクティブなどで差別化可能です。
工夫してみてください。

まとめ


とりあえず、今回はNameSpaceは決めで行い、ファイル格納箇所も直値を指定しましたが、ファイル格納箇所についてはProject Settingsなどに逃せばもう少し柔軟な設定が可能だと思います。

またSQLiteは許容する型が少ないですがMaster Memoryに格納する際にプロパティにて文字列から該当の型へ変換するロジックを書くことで、本来の型で処理することも可能です。

エクセル撲滅のため皆さん色々工夫してゆきましょう!

以上、やまだたいしでした。

【前編】いいかげんエクセルでマスターデータ管理辞めません?UnityでのJson,SQLiteを使ったマスター管理

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
エクセルが嫌いすぎるので本記事を書くことにしました。

目次


なぜエクセルを辞めたいのか?


現在ゲーム業界ではエクセルにてデータマスターを更新するのが普通になっています。
しかし、正直エクセルデータでは後からのカラム変更がやりづらいなどがあります。
競合が発生したりデータを入力するのも一苦労です。
いい加減15年前以上前から続いているエクセルをやめたいです。
今回は代案の提案です。
SQLiteを使ってみましょう。

前提


今回はUnity前提です。
なおかつWinodws環境で64bit前提。
Unityバージョンは(UnitySQLiteAsyncが2018.3以降対応なので)2018.3以降を想定。(今回は2022.3.10f1を使用)
SQLiteでデータの扱いをします。

インストールが必要なソフトウェア


DBeaver
これだけ。
厳密に言うと他にもあるのだけど、DBeaverを通じて勝手にインストールされ意識する必要がない。
DB構築も設定もコレでできるため、今回はコレを選定。

プログラマーがUnity環境に入れるライブラリ


これは必須ではないが、ゲームとしてSQLiteをそのまま使うのはセキュリティ上、不安が残るため
最終的にMasterMemoryに変換する(またSQLiteは環境依存もあったりするので)
github.com

SQLiteを読み込むためのライブラリ
github.com

導入手順


手順は以下の通り

1. DBeaverのダウンロード
2. SQLiteに必要なセットをインストールする
3. DBeaverでデータをつくる
4. SQLiteとUnityの接続設定
5. スクリプトからの一時データの書き出しとSQLiteデータの更新
6. MasterMemoryの設定
7. MasterMemory用に書き出し
8. MasterMemoryで読み込む

前編では5. スクリプトからの一時データの書き出しとSQLiteデータの更新までやりましょう。

1. DBeaverのダウンロード


コチラよりインストール
dbeaver.io

今回はWindows前提で解説する。

インストーラーは特に気にせずデフォルトで設定.

2. SQLiteに必要なセットをインストールする


新規で開いて、右クリック

SQLiteを選択

するとライブラリ等ダウンロードしていいか聞かれるのでそのままダウンロードする。

適当なフォルダに名前をつけてパスを指定する。

(このようにDBファイルができる)

4. SQLiteとUnityの接続設定


今回使うのはこちら

github.com

UnitySQLiteAsyncです。
UniTaskベースでsqlite-netを使ったライブラリです。
割りと更新が近いのと非同期対応していたので選定しました。
最悪、sqlite-netを使うという手もあります。
まずはこちらのREADMEにあるパッケージをインポートします。

実際に接続する前に
DB読み込みをするための仮データを作ります。

後は適当にぽちぽち作って行きます。

SQLiteの解説は割愛。
とりあえず、今回はテストでIdとTextValというカラムを追加し、
IdにIndex設定をしました。(Index設定したら勝手にPKになるっぽい)

各種カラムや設定を追加しCtrl+Sを押すと↓のようなウィンドウが出てくるので接続をしてDBを更新する

適当にカラム追加。

後は、テーブル構造を定義したクラスと……。

using SQLite;

namespace Tables  // 本記事では参照に必須なので追加しておく
{
    [System.Serializable]  // SQLiteでは必須ではないが本記事では必要なので追加しておく
    public class TestTable
    {
        [AutoIncrement, PrimaryKey, Indexed] public int Id { get; set; }
        [MaxLength(64)] public string TextVal { get; set; }
    }
}

適当なオブジェクトにアクセスするスクリプトをアタッチすると……。

using SQLite;
using UnityEngine;

public class DBLoad : MonoBehaviour
{
    async void Start()
    {
        var path = "C://Test/Testdb";
        var db = new SQLiteAsyncConnection(path);
        TestTable test = await db.GetAsync<TestTable> (1);
        Debug.Log(test.TextVal);
    }
}

ログが表示される。

ここまでくれば文字列で読んだデータを各種必要な情報に変換すれば良いので、
マスターデータとしては最低限の役割は果たしたといえる。

5. スクリプトからの一時データの書き出しとSQLiteデータの更新


UnitySQLiteAsyncを使ったSQLiteの読み込みを行った。
しかし、現状では競合する問題点は解決されていない。
そこで一時データとして書き出しと読み込み更新機能を作っていく。

今回はJsonの書き出しと読み込みのために以下のpackageをPackage Managerから取り込んでおきます。

docs.unity3d.com

com.unity.nuget.newtonsoft-jsonをAdd Package する

JsonUtilityを使いたかったのですが、そちらだとSQLiteのライブラリのアトリビュートが邪魔をしてしまったり、プロパティを利用するとJsonとしての書き出しが出来なかったので入れることにしました。

読み取り書き込みにあたり、
以下のようなエディタ拡張のコードを書きました。

using System;
using System.Collections.Generic;
using System.IO;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using SQLite;
using UnityEditor;
using UnityEngine;

public class TableDataEditor : EditorWindow
{
    [MenuItem("Tools/Table Data Editor")]
    public static void ShowWindow()
    {
        GetWindow<TableDataEditor>("Table Data Editor");
    }

    void OnGUI()
    {
        if (GUILayout.Button("Save Data"))
        {
            SaveData();
        }

        if (GUILayout.Button("Load Data"))
        {
            LoadData();
        }
    }

    private static void SaveData()
    {
        var classes = ClassLoader.LoadAllClasses("Tables"); 

        var path = Application.dataPath+"/db/testdb";  // テーブルの格納先変えました
        var db = new SQLiteAsyncConnection(path);
        foreach (var classType in classes)
        {
            SaveClass(classType, db).Forget();
        }
    }

    private static void LoadData()
    {
        var classes = ClassLoader.LoadAllClasses("Tables"); 

        var path = Application.dataPath+"/db/testdb";  // テーブルの格納先変えました
        var db = new SQLiteAsyncConnection(path);
        foreach (var classType in classes)
        {
            LoadClass(classType, db).Forget();
        }
    }

    private static async UniTask SaveClass(Type classType, SQLiteAsyncConnection db)
    {
        // AsyncTableQuery<T> オブジェクトを取得
        var tableMethod = db.GetType().GetMethod("Table")?.MakeGenericMethod(classType);
        if (tableMethod == null)
        {
            return;
        }

        var tableInstance = tableMethod.Invoke(db, null);
        var toListAsyncMethod = tableInstance.GetType().GetMethod("ToListAsync");
        if (toListAsyncMethod == null) return;

        // UniTask から List<T> を非同期的に取得
        dynamic task = toListAsyncMethod.Invoke(tableInstance, null);
        var listInstance = await task;

        // JSONにシリアライズしてファイルに保存
        var json = JsonConvert.SerializeObject(listInstance, Formatting.Indented);
        var filePath = Path.Combine(Application.dataPath, classType.Name + ".json");
        await File.WriteAllTextAsync(filePath, json);
        Debug.Log("Saved: " + filePath);
    }
    
    private static async UniTask LoadClass(Type classType, SQLiteAsyncConnection db)
    {
        var filePath = Path.Combine(Application.dataPath, classType.Name + ".json");
        if (!File.Exists(filePath))
        {
            Debug.Log("File not found: " + filePath);
            return;
        }

        try
        {
            // テーブルが存在しない場合は作成
            await db.CreateTableAsync(classType);

            var json = await File.ReadAllTextAsync(filePath);
            var listType = typeof(List<>).MakeGenericType(classType);
            var listInstance = JsonConvert.DeserializeObject(json, listType) as System.Collections.IList;

            if (listInstance != null)
            {
                foreach (var item in listInstance)
                {
                    await db.InsertAsync(item);
                }
                Debug.Log("Data loaded and inserted into database.");
            }
        }
        catch (Exception e)
        {
            Debug.LogError("Error loading JSON from file or inserting into database: " + e.Message);
        }
    }
}

このスクリプトはTablesというName Space配下のスクリプトを全て取得し、
Jsonに書き出したり読み込んだりするスクリプトです。

これでJsonをGitベースで管理することでマスターがダイレクトに競合することなく、
実装ができます。
(今考えるとオートインクリメントでの処理は結合時に死ぬことがあるのでやめた方がいいですね)

↓貼り忘れていたClassLoaderクラス

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

public static class ClassLoader
{
    public static IEnumerable<Type> LoadAllClasses(string namespaceName)
    {
        var classList = new List<Type>();
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        foreach (var assembly in assemblies)
        {
            classList.AddRange(
                assembly.GetTypes().Where(
                    t => t.IsClass && t.Namespace == namespaceName && !t.IsSubclassOf(typeof(MonoBehaviour))
                )
            );
        }

        return classList;
    }
}

まとめ


前編では、一時データの読み込みと、
Jsonへの書き出し、Jsonからの読み込みを行いました。
後編では実際にマスターメモリーのセットアップと、
実際にマスターメモリーでのデータ取扱を行っていこうと思います。

【アドカレ】UnityでGizmos(ギズモ)を使ってみよう

こんにちは、やまだたいし( https://twitter.com/OrotiYamatano )です。
本記事はUnityゲーム開発者ギルド Advent Calendar 2023の14日目の記事です。
前日はまんたあにどさんによる「2023年を振り返る」でした。
いくつもイラストを投稿されていて、見栄えのする記事でしたね。
今日30歳になってしまったおっさんには新社会人が眩しい……。

adventar.org

目次


UnityのGizmosについて


UnityのGizmosについての記事が少なかったので今回は本記事を作成することにしました。

そもそもGizmosとは


Unity上に表示されるこういったアイコンや、線のことをGizmos(ギズモ)とよびます。

ギズモメニュー - Unity マニュアル

なぜGizmosを使うのか


デバッグ機能であればプリプロセッサディレクティブ(#if UNITY_EDITOR )を使うことでデバック機能として使うことも出来ます。

しかし、あえてGizmosを使うメリットもあります。

・メモリを使わない
まずスクリプトとしてアタッチされる場合少なくとも少しメモリを使います。 メモリを使わずに済みます。

・表示非表示が楽
Unity標準機能でGizmosの表示非表示オプションがあるため明確に表示非表示を切り替えることが可能です。

・明確に本番には乗らない
デバッグ機能をつくってうっかり乗ってしまうこともあると思うがGizmosはそもそもdrowgizmosが本番では走らないため乗ることがない。

どのような機能があるのか


まずどのような表示があるかの前にGizmosには表示するためのメソッドが2つある
OnDrawGizmosOnDrawGizmosSelected だ。

UnityのオプションでGizmosを表示設定にしているなら常に描画するのがOnDrawGizmos

選択したオブジェクトだけ表示するのが OnDrawGizmosSelected となっている。

Gizmos.DrawCube


public class DrawCube : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.color = new Color(1, 0, 0, 0.5f);
        Gizmos.DrawCube(transform.position, new Vector3(1, 1, 1));
    }
}

キューブを描画する。
様々な色はもちろんGizmosとして描画出来る。

Gizmos.DrawFrustum


public class DrawFrustum : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.DrawFrustum(transform.position, 60f, 100, 1, 0.6f);
    }
}

視錐台の表示。
私は使ったことがないがcinemasceneあたりの機能を作るときに便利そう。

Gizmos.DrawGUITexture


public class DrawGUITexture : MonoBehaviour
{
    public Texture myTexture;
    void OnDrawGizmos()
    {
        Gizmos.DrawGUITexture(new Rect(10, 10, 200, -200), myTexture);
    }
}

画像の表示

Gizmos.DrawIcon


public class DrawIcon : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.DrawIcon(transform.position, "AvatarInspector/RightHandZoomSilhouette", true);
    }
}

ギズモアイコンが表示できる。
Assets/Gizmosフォルダーに画像を格納することでオリジナルのアイコンも設定可能。

ちなみにUnityのIconはプログラム上から指定が可能。 github.com

allowScalingがTureになっていると距離に応じて拡縮するようになる。
消える距離が出てくるので注意が必要そうだ。

Gizmos.DrawLine


public class DrawLine : MonoBehaviour
{
    public Transform target;

    void OnDrawGizmos()
    {
        if (target != null)
        {
            Gizmos.color = Color.blue;
            Gizmos.DrawLine(transform.position, target.position);
        }
    }
}

線の描画

Gizmos.DrawLineList


public class DrawLineList : MonoBehaviour
{ 
    public Vector3[] points;

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawLineList(points);
    }
}

複数の線の描画

Gizmos.DrawLineStrip


public class DrawLineStrip : MonoBehaviour
{
    public Vector3[] points;
    void OnDrawGizmos()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawLineStrip(points, true);
    }
}

2023.1から追加されたらしい
これも線の描画で、全ての点を結ぶような線が作れる

Gizmos.DrawMesh


public class DrawMesh : MonoBehaviour
{
    public SkinnedMeshRenderer model;
    void OnDrawGizmos()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawMesh(model.sharedMesh);
    }
}

Meshの描画。
指定するMeshならなんでも。
扇形のポリゴンを生成してエネミーの視覚範囲を描画したり、目印として役立ちそう。

Gizmos.DrawRay


public class DrawRay : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Vector3 direction = transform.TransformDirection(Vector3.forward) * 5;
        Gizmos.DrawRay(transform.position, direction);
    }
}

レイの表示。
ゲームでRayを飛ばすことは少なくないと思うがどこで詰まっているのかわからなくなることもある。
可視化に便利。

Gizmos.DrawSphere


public class DrawSphere : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawSphere(transform.position, 1);
    }
}

球体の表示

Gizmos.DrawWireCube


public class DrawWireCube : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.color = new Color(1, 0, 0, 0.5f);
        Gizmos.DrawWireCube(transform.position, new Vector3(1, 1, 1));
    }
}

ワイヤーのキューブの表示

Gizmos.DrawWireMesh


public class DrawWireMesh : MonoBehaviour
{
    public SkinnedMeshRenderer model;
    void OnDrawGizmos()
    {
        Gizmos.color = Color.green;
        Gizmos.DrawWireMesh(model.sharedMesh);
    }
}

ワイヤーのメッシュの表示

Gizmos.DrawWireSphere


public class DrawWireSphere : MonoBehaviour
{
    void OnDrawGizmos()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, 1);
    }
}

ワイヤーの球体の表示

使用例


例えばAIの循環ルートを設定するときやキャラクターの立ち位置を設定したり、ワープポイントの設定などに便利だ。

今ならDrawWireSphereを使ったほうが良いとは思いますが、私の使用例を載せておきます。

AIの循環ルートを表示するスクリプトです。

    public class PatrolEnemy : MonoBehaviour
    {
        public Vector3[] points;
        private int destPoint = 0;
        private NavMeshAgent agent;


        private void Start () {
            agent = GetComponent<NavMeshAgent>();
            agent.autoBraking = false;

            GotoNextPoint();

            this.UpdateAsObservable()
                .Where(_ => (agent!= null && !agent.pathPending && agent.remainingDistance < 0.5f)) //次の目的地への距離が0.5fより小さくなったら
                .Subscribe(_=>GotoNextPoint()).AddTo(gameObject);   //次のポイントへ
        }


        private void OnDrawGizmos() {
            foreach (var pos in points) {
                Gizmos.color = new Color(1,1,0,0.7f);
                Gizmos.DrawSphere(pos,0.2f);
            }

            for (var i = 0; i < points.Length-1; i++) {   
                Gizmos.color = new Color(0,0,1,0.7f);
                Gizmos.DrawLine(points[i],points[i+1]);
            }
            Gizmos.DrawLine(points[points.Length-1],points[0]);
        }
        private void GotoNextPoint() {
            // 地点がなにも設定されていないときに返します
            if (points.Length == 0)
                return;

            // エージェントが現在設定された目標地点に行くように設定します
            agent.destination = points[destPoint];

            // 配列内の次の位置を目標地点に設定し、
            // 必要ならば出発地点にもどります
            destPoint = (destPoint + 1) % points.Length;
        }
        
    }

実際に表示するとこんな感じ。

あわせて使うと良いもの


Handles


3D GUI の制御と描画のカスタマイズを行うクラスなのですが、文字情報や調整に便利。

docs.unity3d.com

public class ShowHandle : MonoBehaviour
{
#if UNITY_EDITOR
    private void OnDrawGizmos() 
    {
        Handles.Label(transform.position, "これは虚無です");
    }
#endif
}

もちろんUnityおなじみの各種ハンドルも表示できるがラベル表示ができるのはありがたい。

docs.unity3d.com

まとめ


以上、UnityでGizmosを使ってみようでした。
大きなステージを作る際や、デバッグに便利なのでぜひ利用してみてください。
明日はそらたまさんによる 自己紹介と今年の振り返りです。
お楽しみに。

adventar.org

【企画向け】ゲームでのマスターデータの構造を作るコツ

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / X )です。
今回はマスターデータの構造の定義についてです。

目次


なぜ今回マスターデータの定義について書くのか


普段マスターデータを作ることがあるのですが、
他の方が書かれたマスターが微妙なことが多い気がします。
そもそもとしてエンジニアではなく企画が書くことも多いため仕方がないのかなと思っていたのですが、
どうやらエンジニアが定義する場合も微妙になってしまうことが少なくないと最近知りました。

そこでエンジニアとして業務DB設計も行ったことがある私がマスターデータの定義の行い方を解説することにしました。

ここで言うマスターデータとは


ここでいうマスターとは業務エンジニアがよく言う元データ、初期データの意味ではなく
ゲーム業界で言われる必ずしもDBでもつ訳では無いデータ群、ユーザーによって変化する事のないデータ群のコト。
例えばアイテムテーブルなどがそれに該当します。

実際にマスター設計をしていく


実際にマスターの設計をしていってみましょう。

1.要件を洗い出す


何かしらデータが必要になったとします。
例えばRPGであればHP、防御力、攻撃力などです。
まず洗い出しやすい要件としては画面表示に必要なデータです。
次に、その上で表示しているデータが計算や複合情報によって成り立っているなら、
目に見えないデータも存在することになります。

一つ一つ洗い出しましょう。
キャラクター表示名は一意なのか、省略名称はないのか?
レベルテーブルはいちレベルごとに定義するのか、それとも計算式で出すのか……。
とにかく項目を洗い出します。

この項目をカラム、そして一つ一つのデータをレコードと呼びます。

2.正規化


一覧を出したらココから正規化というものを行います。
きれいにデータを扱うためのルールを適用するというぐらいの認識でOKです。
これは一般的にデータをきれいに作る上で一般的なものになります。

2.1 第一正規化


第一の条件は一つのセルには一つの値です。
例えばキャラクターの二つ名とキャラ名が同じレコードに入っている場合はルール違反です。
二つ名カラムとキャラ名カラムにわけましょう。
また、Jsonデータを1レコードにいれてるところを見たことがありますが、よほどのことが無い限り辞めましょう。

2.2 第二正規化


第二の条件は一意にレコードを検索できるようにすることです。
いわゆる主キーが一意に決められていればOKです。
そうでないものは検索した時に複数出てきてしまい、困ることになります。
主キーとサブキーで特定出来る場合は一応OKですが、極力一つの検索で済む方が良いかもしれません。

2.3 第三正規化(と第四正規形)


第三の条件は縦の列に同じ値を使い回す必要をなくすことです。
例えば属性とかはキャラクターのレコードごとに属性を入力するのではなく、
属性に対して属性テーブルを定義し、IDで入力が終わるようにするのがベストです。
いわゆる別レコードで持つべき値をカラムに持つのもやめるべきです。

特に企画陣営が出来ていないレコードの正規化です。

とはいえ、コレはゲーム業界においては嫌われるケースも存在します。
理由としてはIDではなんの値が入力されているかわからないなどです。
値に関しては文字列IDを使うなどで回避することもできますが
またプランナーはエクセルで入力することが多く、複数テーブルに分かれると複数のエクセルに対して入力をしなければならず、
それを嫌う人がいます。
正直プログラマーとしては正しく第三正規形を行ってほしいですが、分割すべきレコードが少ないのであれば
第二正規形で止めてしまうのもゲーム業界においてはありだと思います。

(まぁそもそもとしてDBで絞り込みを出来るようなツールがあればプランナーさんも不便なく
マスターデータの入力が出来ると思うので今後そういったDBがゲーム業界には必要だと感じています)

ただし、この場合、後から2つ属性を持たせたいなど要件が後から発生した場合、
属性セットテーブルを作れば良いところ、改変が大変になりますので、要件が増えそうな場合はやはり、しっかりと正規化しておくとよいです。

2.4 不必要な分割はやめよう(第五正規化)


逆に必要ない分割を行っているケースも見ます。
例えば、横に長い、カラム数が多いテーブル定義を2つのデータにわける行為です。
横に長いテーブルはデータ定義としてNGではありません。
一意に決定できるのならばテーブルをわける必要はありません。これらを統合しましょう。

そもそもとして横に長過ぎる場合は第三正規化/第四正規化がうまく出来ているかというチェックは必要だと思います。

以上!


単純にDB設計と同じですが、当たり前のことを当たり前に書いている記事が無かったので書かせて貰いました。
これらについて気をつけて設計してくれるときれいに作れるのかなと私は思いました。
関係データベースの正規化で検索すると正規化についてはいくつも記事が出てくるので参考にすると良いでしょう。

例えば↓とかがわかりやすかったです。
talosta.hatenablog.com

皆さんきれいな設計を心がけて言ってください!
以上、やまだたいしでした。