ゆべねこの足跡

IT系のはなしを残しておく場所です

OpenGLでゲーム向け3次元剛体シミュレーションをやる

最近こちらの本(以降参考書)を読みました。

タイトル通り、物理エンジンの内部でどのように剛体シミュレーションが行われているかについて解説している書籍となっています。一通り読み終わったので、復習も兼ねて軽く剛体シミュレーションについて解説しようと思います。

できたもの

公開されているサンプルコードを参考にしつつ、自分の環境でも剛体シミュレーションをやってみました。

youtu.be

シェーディングがおかしいことには目を瞑ってください...。

使ったライブラリ:

https://github.com/yubeneko/PhysicsSimulation

剛体シミュレーションとは

3次元空間にある剛体には重力がかかるので、何もしないと剛体は下に向かって落ちていきます。また、剛体が他の剛体と衝突した際はそれぞれの剛体が跳ね返り、速度や位置、姿勢が変化します(反発)。他にも、ある剛体を他の剛体の上で滑らせた場合に、滑っている方の剛体がなかなか停止しなかったり、すぐに停止したりします(摩擦)。

剛体シミュレーションとは、これらの物理現象をコンピュータでシミュレートすることを意味します。

データ構造

剛体シミュレーションでは1つの剛体を表現するために「形状」、「状態」、「属性」の3つのデータ構造が必要となります。

「形状」はその名の通り剛体の形状を表し、剛体同士の衝突検出に使用されます。衝突検出を簡単にするため、剛体の形状は基本的に凸形状で表現されます。剛体が凹形状である場合、いくつかの凸形状を組み合わせることで1つの形状を表すようにします。

「状態」は剛体の座標や並進速度、姿勢、回転速度といった情報を保持します。

「属性」は質量や慣性テンソル、反発係数などを保持します。

シミュレーションの流れ

剛体シミュレーションは1タイムステップ中に以下の処理を行います。

1. 外力の適用

重力などの剛体にかかる外部の力を剛体に適用し、時間積分により剛体の速度、回転速度を更新します。

2. 衝突検出

3次元空間内に存在する全ての剛体のペアについて衝突が発生しているかを調べます。このフェーズでは2つの剛体が衝突しているかどうかを調査し、衝突している場合はめり込み量(貫通深度)と衝突点の法線方向を求めます。この処理は剛体シミュレーションの中でもかなり重い処理で、特に凸メッシュ同士の衝突判定はポリゴン数が多いとめちゃんこ重たくなります。そのため、全ての剛体について衝突しているか詳しく調べようとするととても時間がかかってしまいます。そこで、一般的な衝突検出フェーズは、衝突しているかもしれないペアを検出し、その後でそのペアが本当に衝突しているかどうか詳しく調べるという二段構成にすることで効率化されています。それぞれ「ブロードフェーズ」、「ナローフェーズ」と呼ばれており、お互いにいくつかの手法が存在します。

ブロードフェーズでは主に軸平行バウンディングボックス(AABB: Axis-Aligned Bounding Box)が使われます。ブロードフェーズは3次元空間中に存在する剛体の数が少ないならば総当たりでも良いのですが、数が多くなってくると調べなくてはならないペアが増えてくるのでしんどくなってきます。その際は、スイープ&プルーンや AABB tree といった高速化のためのアルゴリズムを使う必要があります。

ナローフェーズでは、分離軸の理論に基づいた衝突検出法、GJKアルゴリズムといった手法が使われます。参考書では分離軸の理論に基づいた手法について説明されていました。この部分は数多くの数学から成り立っているためかなり難しいです。自分も完全に理解しきれていません。もっと詳しく知りたいならば以下の本あたりを読むと良いかもしれません。

https://www.amazon.co.jp/ゲームプログラミングのためのリアルタイム衝突判定-Christer-Ericson/dp/493900791X

値段高っけー。

3. 拘束演算

衝突検出フェーズにて衝突が検出されたペアは互いに並進速度と回転速度が変化します。これらを求めるのが拘束演算フェーズの役割です。まず、今のままでは剛体同士がめり込んだままになっているので、それを解消するような速度を剛体に与えなくてはなりません。他にも、衝突点の法線方向に対して直角となる2つの軸方向(摩擦方向)の速度も考慮する必要があります。これらの条件を満たすような数式をそれぞれの軸(衝突点の法線方向 + 摩擦方向 x 2 の 3つ)ごとに立てて、それらを解くことで並進速度と回転速度それぞれの増分を求めます。これらを前のタイムステップでの速度に足しこむことで現在のタイムステップでの速度が求まることになります。また、例えば1つの剛体に対して複数の剛体が衝突している場合、1つの剛体の並進速度と回転速度を求めることで他の衝突のめり込みを増やすことになったりしてしまいますが、これは拘束の式を繰り返し解くことにより全ての剛体の速度がイイ感じになるよう収束させることが可能です。繰り返す回数を増やすほど精度は向上しますが、その分計算時間も増加してしまうので、ゲーム向けの剛体シミュレーションでは繰り返し回数を指定してしまって、その回数だけ拘束の計算をするようにしています。

衝突に関する拘束以外にも、ヒンジジョイントやスライダージョイントといった各種ジョイントの拘束の演算もこのフェーズで行われます。

4. 位置更新

拘束演算により現在の剛体の並進速度と回転速度がもとまったので、これをもとに剛体の座標と姿勢を更新します。こうして求まった座標と姿勢をゲーム内のオブジェクトに適用することで、あたかもゲームオブジェクトが物理法則に則った動きをしているかのように見えるのです。

今回の実装の改善の余地

剛体シミュレーションの並列処理化

剛体シミュレーションのゲームへの組み込みについては Unity と同じ感じです。シミュレーションを行うクラスとシミュレーションの結果をゲームオブジェクトに適用するコンポーネントクラス(RigidBodyコンポーネント)を作成し、剛体シミュレーションの対象にしたいオブジェクトにそのコンポーネントを付けることで剛体シミュレーションを行うことができます。

シミュレーションを挟むタイミングですが、今回はゲームオブジェクトの更新処理と描画の間に挟むようにしました。シミュレーションの結果はRigidBodyコンポーネントにて更新されます。ただし、このままだとシミュレーションが重たい場合、描画されるまでの時間が延期されてしまってゲーム全体のフレームレートが落ちるといった問題が生じます。そこで、Unityのように物理シミュレーションだけは別のスレッドで一定のタイムステップ間隔で実行し続け、RigidBodyコンポーネントがシミュレーション結果をゲームループの更新タイミングで取得し、ゲームオブジェクトに適用するという感じにすると、物理シミュレーションが重たくなったとしてもゲーム全体のフレームレートは落とさずに済むようになります。

詳細はこちらの記事をご覧ください。

Cocos2d-xにおけるマルチスレッドを利用した並列処理技法―物理演算パフォーマンスの最適化に向けて― - LINE ENGINEERING

衝突検出の最適化

今回の実装では凸メッシュのみを使った衝突検出を行いました。凸メッシュを使えばかなり幅広い形状の衝突検出ができるのは間違いないのですが、球やカプセルといったプリミティブ形状でゲームオブジェクトの形状を近似することができるのであればそちらを使った方が効率的に衝突検出が行える場合があります。ただし、形状の組み合わせごとに衝突検出処理が必要になってくるのでそこは大変ですね。

また、今回のブロードフェーズの実装は総当たりでやっているので、この部分をスイープ&プルーンや AABB tree といったアルゴリズムに置き換えるとさらに効率化することができます。

まとめ

ゲームにおける剛体シミュレーションの内部実装について軽く紹介しました。書籍についてですが、理論と実践のバランスがちょうど良くまとまっていてかなり理解しやすかったと思います。物理エンジンでどのように剛体シミュレーションが実装されているのか知りたい人にはオススメの一冊です。

C++ と OpenGLでゲームを作ってみた

最近こちらの本(以降参考書)を読みました。

内容は、ゲームエンジンを使わずに C++OpenGL を使ってゲームを作る上での基本(ゲームオブジェクトの概念や基本的なレンダリング技術、衝突判定やUI、オーディオ等)を習得しようというものでした。一通り読み終わったので、復習のためにシンプルな2Dゲームを作ってみました。今回はそのプロジェクトで学んだことなどを書こうと思います。

目次

作ったもの


www.youtube.com

プレイヤーは右からやってくる敵のUFOをレーザーで倒します。WASDで移動、スペースキーでレーザーの発射が可能です。プレイヤーが敵のUFOと衝突するか、敵が1体でも画面の左端に到達してしまったらゲームオーバーとなります。どれだけ長く敵の侵攻を食い止められるかがこのゲームの楽しみ方です。

敵の生成インターバルは時間が経つにつれて早くなっていき、最短の状態になっていると1発の外しが致命的になってきます。

github.com

※ 上の github リポジトリではソースファイルの公開のみ行っています。実行ファイルは配布していません。

(本当は実行ファイルも配りたかったのですが、Apple のセキュリティ周りとかライセンス関連が大変そうだったので断念してしまいました。)

使ったライブラリ、フレームワークなど

OpenGL

言わずと知れたグラフィックスAPIですね。

SDL

こちらは知らない人もいるかもしれません。SDL (Simple DirectMedia Layer) はキーボードやマウスといった入力デバイスOpenGL/Direct3D 等を経由したグラフィックスハードウェアへのアクセスを提供する開発ライブラリです。よく OpenGL と一緒に使われる GLFW に近い存在ですね。C言語で書かれていて、C++から利用することが可能です。

SDL_ttf

SDL には機能を拡張するような周辺ライブラリがいくつかあり、SDL_ttf はそのうちの1つです。このライブラリには TrueType フォントのレンダリングのための機能が含まれていて、ゲーム内の UI に使うことができます。参考書でもゲーム内のUIテキストの描画のために使われています。

GLEW

OpenGL拡張機能を有効化するためのライブラリですね。似たようなライブラリに GLAD があります。

SOIL

OpenGL で画像を描画する際は OpenGL に画像のピクセルデータを渡す必要があります。しかし、画像ファイルからピクセルデータを読み出す処理は OpenGL から提供されていないので、その部分は自分でなんとかするしかありません。選択肢としては、画像ファイルのデータフォーマットを紐解いてデータを読み込む処理を自分で実装するか、画像ファイルを読み込むライブラリを導入するかのどちらかになります。

参考書ではどうしていたかというと、SOIL という OpenGL 向けの画像読み込みライブラリを利用していました。しかし、このライブラリが公開されていたWebページはどうやら無くなってしまったみたいです。一方で、githubリポジトリは残っていました。

画像ファイルを読み込んで、OpenGLが直接読める形式でピクセルデータを扱ってくれるのでかなり便利です。

github.com

FMOD

FMOD はゲーム開発に使われるサウンドミドルウェアの1つです。こちらも参考書で使われていたので採用しました。

GLM

OpenGL と親和性の高い数学系ライブラリです。参考書ではベクトルが列ベクトルではなく行ベクトルで表されていました。つまり変換行列を作る時の掛け算の順番が変換を適用する順番と同じになります。大学の講義で散々列ベクトルを扱ってきた自分にとって、行ベクトルには大きな違和感を感じていました。かといって自前で列ベクトル形式で演算できるコードを書くのも時間がかかりそうで面倒だったので、ベクトルが列ベクトルで表されている数学系ライブラリのGLMを採用することにしました。

このライブラリはヘッダオンリーなライブラリなので導入が簡単でいいですね。

学んだこと

レンダリングに関する基本的な知識

私は今までCG分野では Unity にしか触れたことがありませんでした。しかも、レンダリング系には特別興味を抱いていなかったため、レンダリング系の知識はほぼ0と言ってもよいくらいでした。そんな私でしたが、今回のゲーム開発を通して基本的なレンダリングの知識を習得することができたと思います。特に、今回はテクスチャについて深く知ることができました。

参考書ではテクスチャのアニメーションのために、フレームごとの画像ファイルを1枚1枚ロードして、フレームごとにテクスチャを切り替えてアニメーションを実現するような実装が紹介されていました。自分も最初はそのような方式でアニメーションを実装していたのですが、爆発アニメーションを実装する時に、テクスチャアトラスの形をとった素材しか見つけることができませんでした。テクスチャアトラス形式の画像からフレームごとの領域を切り出してそれぞれ1つの画像データとしてロードして描画することも考えましたが、画像の特定の領域を切り出すツールを探しても良さげなものが見つかりませんでした。結局、テクスチャアトラスを1枚の画像データとしてロードして、描画時は利用するUV座標を切り替えることでアニメーションさせるようにしました。これについては参考書には書かれていなかったテクニックだったので、ネットで調べながら実装していきました。

実装の手間は少しかかりますが、テクスチャアトラスを利用することでロードするテクスチャの数を減らすことができてメモリ効率が良くなったりするので、テクスチャを複数個利用する際はなるべく画像データをテクスチャアトラス化してまとめておくのがいいですね。

継承

参考書では継承が至るところで使われていました。中でも、派生クラス独自の振る舞いを記述できるメソッドと基底クラスで絶対に実行する処理を記述するメソッドを分割するテクニックが興味深かったですね。

class Actor
{
public:
    virtual void Update(float deltaTime);

private:
    std::vector<Component*> mComponents;
};
  • Actor クラスはゲームオブジェクトを意味し、Actor クラスのインスタンスは1つのゲームオブジェクトとしてシーンに存在します。
  • Actor は Component 型のオブジェクト(ポインタ)を保持します。
  • シーンに存在する全 Actor はゲームループを管理する Game クラスに保持されています。
  • Game クラスは保持している Actor の Update を毎フレーム呼び出します。

Update メソッドはオーバーライド可能で、派生クラスで独自の振る舞いを定義することが可能です。しかし、Actor クラスの Update メソッドには保持しているコンポーネントUpdate メソッドを呼び出す処理が記述されています。

void Actor::Update(float deltaTime)
{
    for (auto comp : mComponents)
    {
        comp->Update(deltaTime);
    }
}

この処理は派生クラスであっても絶対に実行されなければいけない処理です。そのため、派生クラスでUpdateメソッドをオーバーライドする場合は以下のように書かなくてはなりません。

void DerivedActor::Update(float deltaTime)
{
    Actor::Update(deltaTime);

    // 以降 DerivedActor の独自の振る舞いを記述
}

こうすることで派生クラスであっても基底クラスの処理を実行することができますが、この1文を書き忘れてしまうことで致命的なバグが生じてしまいます。これを解決するために、参考書では以下のようにしていました。

class Actor
{
public:
    void Update(float deltaTime);
    virtual void UpdateActor(float deltaTime);

private:
    std::vector<Component*> mComponents;
};
void Actor::Update(float deltaTime)
{
    for (auto comp : mComponents)
    {
        comp->Update(deltaTime);
    }

    // 派生クラスでオーバーライド可能な UpdateActor を呼び出す
    UpdateActor(float deltaTime);
}

// 基底クラスでは何も実装しない
void Actor::UpdateActor(float deltaTime)
{
}

このように、どんな派生型であっても実行されることが求められる処理を仮想メソッドから非仮想メソッドに切り出してあげて、そのメソッド内で派生クラスで独自に再定義できる仮想メソッドを呼ぶようにしてあげることで先ほどの問題点を解決することができます。

CMake

参考書ではIDE(Visual StudioXcode)を使った開発を推奨していましたが、いつも使っている VScode を使って開発したかったので VScode で開発を進めていました。ソースコードのビルドには、はじめは独自に記述した Makefile を使っていました。

以下はその頃に使っていた Makefile を模したものです。

# ソースファイルが置かれるディレクトリ
SRC_DIR = src
# ビルドにより生成されたファイルを配置するディレクトリ
BUILD_DIR = build/debug
# ソースファイルのリスト
SRC_FILES = $(wildcard $(SRC_DIR)/*.cpp)
# 実行ファイル名
PROGRAM = my_app
# コンパイラ
CXX = g++

# インクルードファイルのパス
INCLUDE_PATHS += -I/usr/local/Cellar/glfw/3.3.5/include
INCLUDE_PATHS += -I/usr/local/Cellar/glew/2.2.0_1/include

# 使用するC++バージョン
FLAGS = -std=c++11
# コンパイルオプション
CXXFLAGS = $(FLAGS) -Wall -O0 -g $(INCLUDE_PATHS) -DGL_SILENCE_DEPRECATION

# ライブラリファイルのパス
LDFLAGS += -L/usr/local/Cellar/glfw/3.3.5/lib
LDFLAGS += -L/usr/local/Cellar/glew/2.2.0_1/lib

# リンクするライブラリ
LDLIBS = -framework OpenGL -lGLEW -lglfw

# ビルドコマンド
all:
    $(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $(SRC_FILES) -o $(BUILD_DIR)/$(PROGRAM)

# 実行ファイルのクリーン
.PHONY: clean
clean:
    -rm -rf build/debug/*

このやり方の問題は、ビルドのたびに全ソースファイルが再コンパイルされてしまうことです。オブジェクトファイルごとに生成方法を定義してあげればこの問題は回避できるのですが、新しくソースファイルを作成したり削除したりするごとに Makefile を編集するのは少し面倒です。

一方で、CMake を利用することでビルドシステムを自動生成することができ、ビルドシステムの選択肢の一つとして Makefile を生成することができるのですが、その Makefile はそのときのビルドに必要なソースファイルだけがコンパイルされるように書かれているのでとっても優秀です。上記の Makefile を使ったビルドのビルド時間の長さにはうんざりしていたので、どうにかこれを解決しようと CMake について色々と調べ上げて、なんとか最低限の CMake の書き方を習得することができました。これのお陰で開発の時間の短縮につながったと思います。

ライセンス関連

今までライセンスについてはそこまで詳しくなかったのですが、エンジニアになるのであればライセンス周りのことも知っとかなきゃならんだろうということで、ライセンス関連の知識を仕入れました。

分かったことは、コピーレフト型のライセンスの元で公開されているライブラリを使うときは気をつけなければならないなーということですね。あと、GPL系のライセンスには本当に気をつけないとダメですね。過去には日本でもGPLライセンスに関わる事件が発生しています。

過去に日本で起こった事件としては、2005年にAQUAPLUSのソフトウェアに GPLでライセンスされた動画デコーダーのライブラリが使われていることがわかり、 郵送によりゲーム本体も含めた当該ソースコード(画像やテキストデータなどのアセットを除く)が開示されたことが有名である。

https://dic.nicovideo.jp/a/gpl

余談ですが、GLM からは2つのライセンスが提供されているので利用するときはどちらかを選んで使うことになります。ひとつは MIT ライセンス。もう1つは Happy Bunny License という可愛らしい名前のライセンスです。このライセンス、ほとんど MIT ライセンスと同じなのですが、それに加えて1つ制限が加えてあります。

Restrictions: By making use of the Software for military purposes, you choose to make a Bunny unhappy.

https://github.com/g-truc/glm/blob/master/copying.txt

つまり、軍事目的での利用を禁止しているのですね。名前は可愛いのに中身は可愛くない...。時間と余裕があればいろんなライセンスの中身を見てみたいものです。

まとめ

支離滅裂で読みづらい文章となってしまいました。とりあえず、今回のプロジェクトを通してC++の基礎の基礎はある程度固めることができたと実感しているのでそこそこ満足しています。冒頭で紹介した本にも大変満足しています。

また、今回のプロジェクトでは動的オブジェクトの扱いのために生のポインタをそのまま扱っていました。しかし、生ポインタで動的オブジェクトを扱うのはメモリリークの危険がつきまといます。だから、早いところスマートポインタを理解して正しく使えるようになりたいですね。

macOS で動的ライブラリを動的リンクするとき

macOSにて、自作のCやC++アプリケーションで動的ライブラリを実行時に読み込ませる方法について調べてみたのでまとめてみます。動的ロードではなく、動的リンクの話です。

実行環境: macOS 12.1

目次:

動的ライブラリをどこに置いておくか

まず、動的ライブラリをどこに置いておくかですが、自分だけが利用するようなライブラリであれば /usr/local/lib あたりに置いておけば基本的に問題なさそうです。なぜなら、アプリを実行する時にリンカがそこを探してくれるからですね。

しかし、作ったアプリを外部に配布するとなると、配布先のPCの /usr/lib/usr/local/lib あたりにアプリで利用するライブラリが存在するとは限らないので、その場合はアプリと一緒に動的ライブラリも配布する必要があります。それだけでなく、配布する実行アプリが同梱した動的ライブラリを読み込めるようにしてあげる必要もあります。実は、今回はこの話がメインです。

動的ライブラリの詳細を追う

まずは、動的ライブラリの詳細を追ってみましょう。

以下のような単純な動的ライブラリを作ってみます。

mylib.cpp:

int my_sum(int a, int b)
{
    return a + b;
}
# コンパイル
$ g++ -c mylib.cpp

# 動的ライブラリ生成
$ g++ -shared mylib.o -o libmylib.dylib

次に、作ったライブラリを利用する実行ファイルを作ってみます。

main.cpp:

#include <iostream>
#include <mylib.h>

int main ()
{
    int a = my_sum(1, 5);
    std::cout << a << std::endl;
}

フォルダ構成は以下のように多少整理しておきました。

.
├── main
├── main.cpp
└── mylib
    ├── include
    │   └── mylib.h
    ├── lib
    │   └── libmylib.dylib
    ├── mylib.cpp
    └── mylib.o

コンパイルとビルド時リンクを行っていきます。

$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -o main

このコマンドにより実行ファイルmainが出来上がります。実行ファイル main があるディレクトリでこのファイルを実行すると...

$ ./main
dyld[17548]: Library not loaded: libmylib.dylib

残念ながらmainを実行したときにリンカが ./mylib/lib/libmylib.dylib を見つけることができなかったようです。

さて、実行時にリンカがアプリで利用される動的ライブラリをどのように見つけるかがここでは問題になっています。デフォルトではリンカは /usr/local/lib や実行ファイルを実行したディレクトリ等を探すようになっています。では、自分好みの位置に配置した動的ライブラリをリンカが見つけられるようにするにはどうしたら良いのでしょう。

これを知るには、実行ファイルが実行時にどのようにライブラリをロードするかを見てみる必要があります。

実行ファイルや動的ライブラリの中にはいろんな情報が書き込まれているのですが、macOS ではこれらの情報を otool コマンドにより調べることが可能です。

# 動的ライブラリのID名(自身のパス)を表示
$ otool -D libmylib.dylib

# 依存する動的ライブラリを表示
$ otool -L libmylib.dylib

# ロード時のコマンドを表示
$ otool -l libmylib.dylib

# 他にもあります

-l オプションでロード時のコマンドを見ることができるので、これで main を見てみましょう。

$ otool -l main
main:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot 0x00000000
 initprot 0x00000000
   nsects 0
    flags 0x0

... 略 ...

Load command 8
          cmd LC_LOAD_DYLINKER
      cmdsize 32
         name /usr/lib/dyld (offset 12)
Load command 9
     cmd LC_UUID
 cmdsize 24
    uuid E9747806-B1DE-3C85-BB7A-A7C1AC3A3502
Load command 10
      cmd LC_BUILD_VERSION
  cmdsize 32
 platform 1
    minos 12.0
      sdk 12.1
   ntools 1
     tool 3
  version 711.0
Load command 11
      cmd LC_SOURCE_VERSION
  cmdsize 16
  version 0.0
Load command 12
       cmd LC_MAIN
   cmdsize 24
  entryoff 15616
 stacksize 0
Load command 13
          cmd LC_LOAD_DYLIB
      cmdsize 40
         name libmylib.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 0.0.0
compatibility version 0.0.0
Load command 14
          cmd LC_LOAD_DYLIB
      cmdsize 48
         name /usr/lib/libc++.1.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1200.3.0
compatibility version 1.0.0
Load command 15
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libSystem.B.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 1311.0.0
compatibility version 1.0.0
Load command 16
      cmd LC_FUNCTION_STARTS
  cmdsize 16
  dataoff 49840
 datasize 16
Load command 17
      cmd LC_DATA_IN_CODE
  cmdsize 16
  dataoff 49856
 datasize 0

重要なのは、cmd LC_LOAD_DYLIBの部分。LC_LOAD_DYLIB は 「このパスで指定されたネイティブライブラリをロードしてくれ!」という意味を表します。

つまり、cmd LC_LOAD_DYLIBの部分をいい感じにいじってあげることで好みの位置に配置したライブラリを読み込むようにできる訳ですね。

絶対パスで動的ライブラリが読み込まれるようにする

macOSには実行ファイルの中身をいじることができるinstall_name_toolコマンドが用意されています。これを使って main の中身を書き換えてみましょう。

$ install_name_tool -change libmylib.dylib /Path/To/Workspace/Folder/mylib/lib/libmylib.dylib main

これは libmylib.dylib/Path/To/Workspace/Folder/mylib/lib/libmylib.dylib に置き換えるコマンドです。

これにより、mainを実行した時に/Path/To/Workspace/Folder/mylib/lib/libmylib.dylib をロードするように変更することができました。この段階で、mainはどのディレクトリにいても実行できるようになっています。

@executable_path と @rpath

自分の好みの位置にある動的ライブラリを読み込ませることに成功しました。しかし、これでは配布するアプリには不適切です。なぜなら、アプリを開発したマシン上の絶対パスをそのまま利用しているからですね。かといって、相対パスにすると「実行時のカレントディレクトリからの相対パスで動的ライブラリのパスを指定する」ことになるので、アプリを実行できるディレクトリが制限されてしまいます。

ロードするライブラリは絶対パスで設定したいけど、この絶対パスは実行時のパスを元にして作り上げたいですね。

これを可能にするのが@executable_path@rpathといったものです。

例えば、main を配布する際に以下のようなフォルダ構成で配布することを考えましょう。

distribution
├── lib
│   └── libmylib.dylib
└── main

この時、以下のコマンドを実行することで main の中身を編集します(main は新しく作り直したことを想定)。

$ install_name_tool -add_rpath @executable_path/lib main
$ install_name_tool -change libmylib.dylib @rpath/libmylib.dylib main

@executable_path には実行ファイルが実行された時のそのファイルが存在するディレクトリへの絶対パスが展開されます。例えば、distribution フォルダが /Users/<ユーザー名>/Downloads フォルダにあるとしましょう。

Downloads/distribution
├── lib
│   └── libmylib.dylib
└── main

この時、main を実行すると、@executable_path/Users/<ユーザー名>/Downloads/distributionに展開されることになります。

install_name_tool -add_rpath @executable_path/lib main は、@executable_path/libというパスを rpath に追加するコマンドになっています。

rpath はリンカが動的ライブラリを検索するパスです。つまり、今回の場合は実行ファイルがあるディレクトリと同じディレクトリにあるlibディレクトリの中からライブラリを探索するように設定していることになります。

さらに、ライブラリ自体のパスを @rpath/libmylib.dylib に書き換えることで、ライブラリ自体のパスを実質的に @executable_path/lib/libmylib.dylib に設定しています。

こうしておくことで、どんなmacOS環境でも、どのディレクトリからでも main を実行できるようになります。

まとめ

今回はmacOS で動的ライブラリを動的リンクするときのことについて書きました。

  • 自分だけが利用するようなライブラリであれば /usr/local/lib あたりにおいておけばOK。
  • 自作のアプリを外部に配布するとき、動的ライブラリも一緒に配布する必要があるならば、アプリが動的ライブラリを読み込めるように正しく設定してあげる必要がある。

おまけ

利用するライブラリのID(インストールパス)を @rpath/libXXX.dylib に書き換えておいて...

$ install_name_tool -id @rpath/libmylib.dylib libmylib.dylib

# 確認
$ otool -D libmylib.dylib 
libmylib.dylib:
@rpath/libmylib.dylib

そのライブラリをリンクすると...

$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -o main
$ otool -l main
main:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot 0x00000000
 initprot 0x00000000
   nsects 0
    flags 0x0

... 略 ...

Load command 13
          cmd LC_LOAD_DYLIB
      cmdsize 48
         name @rpath/libmylib.dylib (offset 24)
   time stamp 2 Thu Jan  1 09:00:02 1970
      current version 0.0.0
compatibility version 0.0.0

... 略 ...

mylib の name(ID) が @rpath/libmylib.dylib になります。

さらに、rpath はコンパイルオプションで設定できます。

$ g++ main.cpp -I./mylib/include -L./mylib/lib -lmylib -Wl,-rpath,@executable_path/lib -o main
 otool -l main
main:
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 72
  segname __PAGEZERO
   vmaddr 0x0000000000000000
   vmsize 0x0000000100000000
  fileoff 0
 filesize 0
  maxprot 0x00000000
 initprot 0x00000000
   nsects 0
    flags 0x0

... 略 ...

Load command 16
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/lib (offset 12)
Load command 17
      cmd LC_FUNCTION_STARTS
  cmdsize 16
  dataoff 49840
 datasize 16
Load command 18
      cmd LC_DATA_IN_CODE
  cmdsize 16
  dataoff 49856
 datasize 0

Load command 16 に rpath のことが書いてありますね。

つまり、あらかじめ利用するライブラリのIDを @rpath/libXXX.dylib に書き換えておいた上でアプリのビルド時に -Wl,-rpath,@executable_path/lib というオプションをつけてビルドすることで、install_name_toolコマンドを使う必要がなくなる訳ですね。

参考

Unity Cloud Diagnostics でクラッシュレポートを見る(iOS)

目次

前置き

とあるSDKを使ったiOSアプリを開発しようと思い、とりあえずサンプルシーンをビルドして実機で動かそうと思ったらシーンが始まると同時にアプリがクラッシュするといった事態に遭遇しました。しかも、Xcodeからアプリを起動すると問題ないのに、実機からアプリを起動する時に限ってクラッシュするんです。Unityの公式ドキュメントにiOSトラブルシューティングのページがあったので見てみたら、ドンピシャなものがありました。

ゲームが Xcode から起動したときには正しく実行されるが、デバイス上で手動で起動すると最初のレベルのロード中にクラッシュする

これにはいくつかの理由が考えられます。より多く詳細を確認するために、デバイスのログをチェックする必要があります。Mac にデバイスを接続し、Xcode を起動してメニューから Window > Devices and Simulators を選択します。ウィンドウの左ツールバーで使っているデバイスを選択し、Show the device console ボタンをクリックして慎重に最新のメッセージを確認します。さらにクラッシュレポートを検証する必要があるかもしれません。クラッシュレポートを取得するには、以下を参照してください。

Troubleshooting on iOS devices - Unity マニュアル

ということで、クラッシュレポートを検証することになりました。調べてみたところ、クラッシュレポートを見るにはいくつか方法があるみたいですが、今回はUnityのCloud Diagnosticsを使ってやってみることに。しかし、一度も使ったことがなかったのでどのようにして使うのかをいろいろ実験してみました。今回はその実験結果もとい使い方を記しておこうと思います。

しかし、今回は最終的にiPhoneのクラッシュレポートを自分で取得して自分でクラッシュレポートを解析することになったんですが、それについては後述します。

解説に誤りがあった場合はご報告お願いします!

環境

  • Unity: 2019.4.9f1(LTS)
  • Unityプラン: Student(Pro)
  • macOS: 10.15.6
  • Xcode: 11.7
  • iOS: 13.7

Unity Cloud Diagnosticsとは?

簡単にまとめると、ユーザーが遭遇したクラッシュや、例外などを自動で集めてくれたり、ユーザー執筆のバグレポートを集めてくれたりするサービスです。プランによって受けられるサービスに違いがあったりします。詳しくは公式ページをご覧ください。

Cloud Diagnostics - Unity

クラッシュレポートにはクラッシュしたスレッド、例外の種類、スタックトレースなどが載っているのですが、スタックトレースについては、どのメソッドでクラッシュしたのかについてはアドレスでしか記載されておらず、とても読めるものではありません。そのため、通常はシンボル化と呼ばれるアドレスをメソッド名に変換する処理を行う必要があります。ところがどっこい、Cloud Diagnostics を使うとすでにシンボル化されたクラッシュレポートを読むことができるのです。これは便利! ただし、メモリ不足によるクラッシュは Cloud Diagnostics にレポートされないのでその点は注意が必要です。

Unity Cloud Diagnosticsを使ってみる

とりあえずどんな感じに動いてくれるのか実験してみました。2019.4.9f1(LTS)で新規プロジェクトを作ってその中で実験してみます。丁寧な解説付きのチュートリアルページがあったのでそれをやってみます。このチュートリアルの3番と4番をやればUnity Editorで起こした例外をUnity Services Dashboardで確認できるようになります。

Cloud Diagnostics - Unity Learn

しかし、私が確認したいのは例外ではなくクラッシュレポートについてです。早速クラッシュを起こすようなコードを書いてクラッシュを起こしてみましょう。

UnityEngine.Diagnostics名前空間にあるUtilsクラスのForceCrashメソッドを使うことで意図的にクラッシュを起こすことができます。クラッシュの種類を引数に指定してForceCrashを実行します。AccessViolationは無効なメモリへのアクセスです。

using UnityEngine;
using UnityEngine.Diagnostics;
using UnityEngine.UI;

public class Crasher : MonoBehaviour
{
    public Button _button;
    void Start ()
    {
        _button.onClick.AddListener (Crash);
    }

    void Crash ()
    {
        Debug.Log ("クラッシュさせるぞー");
        Utils.ForceCrash (ForcedCrashCategory.AccessViolation);
    }
}

Unity Editor上で実行すると当然ながらUnity Editorもクラッシュしますので注意してください。また、Unity Editorでのクラッシュは Cloud Diagnostics にレポートされません。

ということで、ビルドして実機で実行してみます。プラットフォームをiOSにスイッチし、Build Settings は特に弄らず、 Player Settings は Other Settings > Identification の Bundle Identifier を適切に変更してビルドします。ビルドが成功したらいよいよiPhoneに転送してクラッシュさせましょう! ただし、クラッシュレポートが送信されるにはアプリをXcodeから起動するのではなく、実機から起動する必要があります。実機から起動した場合、クラッシュしたらアプリは終了されます。そしたら Unity Dashboard でクラッシュレポートが来ているか確認してみましょう。

※ 私の場合だけかもしれませんが、実機に転送後初めての実機からのアプリ起動によるクラッシュはなぜかレポートされませんでした。そのため、もしクラッシュを起こしたのに Dashboardでクラッシュを確認できない場合は何度かクラッシュをさせてみるといいかもしれません。

f:id:yubeshineko:20200906165321p:plain

おお、来てます!!

ページ下部にある Problems の項目でクラッシュの詳細を見てみましょう。しかし、 Unknown Function とか書かれてるのが気になるなぁ...

f:id:yubeshineko:20200906165954p:plain

スタックトレースを見てみると...

f:id:yubeshineko:20200906170215p:plain

んん? なんかシンボル化されてないのがあるっぽくね?

シンボル化されていない部分はアドレスの右脇に <system symbols missing> とありますね。しかし、 UnityFramework の部分だけはちゃんとシンボル化されているようです。これは一体どういうことでしょうか。

Cloud Diagnostics のドキュメントページのシンボルに関するページを見てみると、原因っぽい内容を見つけました。

Crash and exception reporting missing symbols | Unity Cloud Diagnostics

シンボルにはシステムシンボルとアプリケーションシンボルの二種類のシンボルがあり、システムシンボルはOSのサプライヤーから提供され、アプリケーションシンボルはUnityのプロジェクトがビルドされたときに提供されると書かれています。そして、システムシンボルが欠落している原因の多くは、UnityがそのバージョンのOS用のシンボルを持っていないことに起因するとも書かれています。

今回使用している iOS 13.7 は この実験をやった日のわずか5日前にリリースされました。そのため、まだUnityがiOS13.7向けのシステムシンボルを持っていなかったのでしょうか?

ちなみに、ビルド時に生成されるアプリケーションシンボルが正しくサーバーにアップロードされたかどうかは以下のファイルを見るとわかります。

~/Library/Logs/Unity/symbol_upload.log

確認してみると...

time="2020-09-06T16:44:06+09:00" level=info msg="b5701ffd9ba8312889708277314e2b92 successfully uploaded to cloud storage" 
time="2020-09-06T16:44:06+09:00" level=info msg="All 1 uploads completed with out error." 
time="2020-09-06T16:44:19+09:00" level=info msg="9a25edc0f70938afb2606b9f026189ac successfully uploaded to cloud storage" 
time="2020-09-06T16:44:19+09:00" level=info msg="All 1 uploads completed with out error." 

このログを見る限りアプリケーションシンボルとして使うファイルは2種類あるみたいで、そのどちらも正しくアップロードされているようです。

iOSのバージョンを変えればうまくシンボル化されるかもしれませんが、わざわざダウングレードするのも面倒なのでここで断念...。

まとめ

Unity の Cloud Diagnostics を使うことで簡単にクラッシュレポートを見ることができました。しかし、デバイスのOSのバージョンによってはシンボル化できない部分も出てきてしまうようです。

おまけ: 自力でiOSのクラッシュレポートをシンボル化する

結局、私の実行環境では Cloud Diagnostics を使って完全なシンボル化がされたクラッシュレポートを見ることができませんでした。さて、どうにかして完全なシンボル化がされたクラッシュレポートを手にしたいです。ということで、自力でシンボル化をしてみたいと思います。ちなみに、Cloud Diagnocticcs をONにしないと UnityFramework 関連の部分がシンボル化されなかった(私が試した中では)ので、 自力でシンボル化する場合も Cloud Diagnostics はONにしておくのがいいと思います。

1. クラッシュレポートをデバイスから取得する

実機に転送したアプリがクラッシュするとデバイスにクラッシュレポートが登録されます。

設定アプリ > プライバシー > 解析 > 解析データ

f:id:yubeshineko:20200906184458p:plain

目的のクラッシュレポートを選択してAirDropなどでMacに転送します。

2. シンボルファイルを取得する

シンボル化に必要なファイルは以下に配置されます。

~/Library/Developer/Xcode/DerivedData/<build id>/Build/Products/<build type>/

Cloud Diagnostics を有効にしてビルドすると、<build type>(下の画像の場合 ReleaseForRunning-iphoneos)フォルダ以下は以下のような構成になります。

f:id:yubeshineko:20200906233010p:plain

ミソなのは、このフォルダの中にUnityFramework.framework.dSYMが含まれていることです。Cloud Diagnocticcs をONにすることでこのファイルが<build type>以下に生成されます。これがないと UnityFramework に関する部分がシンボル化されませんでした。他に方法があるのかもしれませんが未検証です。

この中のappname.app.dSYMをコピーしてどこか違う場所に移動させます。クラッシュレポートと一緒に新しく作業用ディレクトリを作ってまとめておきます。

3. シンボル化する

シンボル化には以下のプログラムを使います。しかし、とても長いのでエイリアスを作っておきます。

alias symbolize="/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash"

また、パスを通しておきます。

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

上の二つのコマンドをコマンドラインに打ち込んだら、いよいよシンボル化を行なっていきます。コマンドは作業ディレクトリ内でやると楽です。

$ symbolize -v hoge.ips appname.app.dSYM > symbolicated.ips

シンボル化プログラムは標準出力に出力されるので、出力をファイルにリダイレクトします。

注意

シンボル化する際は以下のディレクトリ内にクラッシュしたOSのバージョンがあるかを確認しましょう。無い場合はUIKit等のframeworkのスタックトレースをうまく復元できないらしいです。ということは、おそらくこれがシステムシンボルの有無を表しているのでしょう。私の方では確認していませんが、参考記事によるとiOS DeviceSupportは端末をMacに接続し、XCode内でデバイスを選択すると、そのOSのディレクトリが作成されるとのことです。

ls ~/Library/Developer/Xcode/iOS\ DeviceSupport/

iOSのクラッシュログをSymbolicate(復元)して解析する - Qiita

4. シンボル化完了!

f:id:yubeshineko:20200906234944p:plain

上のスクリーンショットをみると、Cloud Diagnostics でシンボル化されなかった部分がシンボル化されているのが分かります。今後何度もシンボル化をすることになりそうなら上のパスとエイリアスを .zshrc あたりにでも書いておくといいですね。

参考

Unity crash and exception reporting | Unity Cloud Diagnostics

Unity iOSのクラッシュレポートについて調べてみた(活用方法や注意点など) - Qiita

平面直角座標と緯度経度の変換を行う(C#)

ARアプリを作る際に緯度経度と平面直角座標の相互変換の技術が必要になりそうなので、C#で変換をするためのスクリプトを書いてみました。

変換式は?

国土地理院のサイトに変換式についてのページがあります。

平面直角座標への換算 計算式

緯度、経度への換算 計算式

数式がいっぱいで頭が痛くなりそうですね...。幸いにもこれらの数式の中間結果の依存関係を図式化している方がいらっしゃったので、そちらを参考にしながらコーディングをしました。

sw1227.hatenablog.com

コード

平面直交座標と緯度経度の相互変換をする。

使い方

using System;
using GeoCoordinateUtility;

namespace ConvertGeoCoordinate
{
    class Program
    {
        static void Main (string[] args)
        {
            var latLon = GeoCoordinateConverter.Coordinate2LatLon (
                x0: 53999.2571,
                y0: -83939.5673,
                latOrigin_deg : 36.00000000,
                lonOrigin_deg : 139 + 50d / 60d);
            Console.WriteLine ($"緯度, 経度 = {latLon.X}, {latLon.Y}");

            var coordinate = GeoCoordinateConverter.LatLon2Coordinate (
                lat0_deg: 36.483011,
                lon0_deg: 138.896538,
                latOrigin_deg: 36.00000000,
                lonOrigin_deg: 139 + 50d / 60d);
            Console.WriteLine ($"X, Y = {coordinate.X}, {coordinate.Y}");
        }
    }
}

結果

緯度, 経度 = 36.48301100025305, 138.89653799991123
X, Y = 53999.25707184523, -83939.56729232075

ちなみに、サンプルで指定した場所は某車漫画の聖地です。

ざっくりとした変換の仕組みの解説

そもそもどうやって緯度経度を座標に変換するのでしょうか。考え方としては、ある地点の周りが平面になっていると仮定してその地点を平面直角座標の原点として座標系を定めるといった感じです。日本にはその「ある地点」が19個あります。19個もある理由ですが、これは一つの原点だけでは地球の丸みによるずれが生じてしまうためです。地点によって原点の緯度経度が決まります。例えば、群馬県なら系番号9に該当するため、平面直角座標の原点の緯度経度は北緯36度0分0秒、東経139度50分0秒となります。

わかりやすい平面直角座標系 | 国土地理院

本コードの変換メソッドはどちらも座標系原点の緯度経度を引数にとります。なお、引数の緯度経度の単位は10進数の度単位になっているので、度分秒表記の場合は度単位に変換する必要があります。

コード内の定数(a, F, m0)についての詳細はこちらをご覧ください。

日本の測地系 | 国土地理院

注意

緯度経度から座標に変換する際に国土地理院のサイトを使って変換してみると分かるのですが、どうやら測量分野ではXが南北方向を表し、Yが東西方向を表すようです。普段イメージする座標軸とは逆になっているので注意が必要です。

参考サイト

[C#]平面直角座標系と緯度経度を変換する

緯度経度と平面直角座標の相互変換をPythonで実装する - Qiita

Zig SimをUnityで使うためのアセットを作りました。

Zig Simという 1->10(ワントゥーテン)で開発されたアプリケーションがあります。

zig-project.com

ZIG SIMは『フィジカル・プロトタイピングをプロトタイピングする』ためのアプリケーションです。 従来、センサを利用したプロトタイピングを行う場合、電子工作、マイコンプログラミング、通信仕様策定、アプリケーションプログラミングと4つの作業を行う必要があり、異なるエンジニアリングスキルが必要とされていました。

ZIG SIMではスマートフォンを『センサとwifiモジュールを取り付けたフィジカル・プロトタイピングデバイス』とすることで、『センサ受信プログラム』のみでのプロトタイピングが制作可能になりました。

私は大学の講義の演習課題作成や趣味などで何かとこのアプリにお世話になっておりました(主にUnityで)。そして、今後も何かと使うことになると思ったのでUnityのアセットを作ることにしました。

詳細についてはgithubのREADMEを参考にしてください。 github.com

Unityパッケージのダウンロードはこちらから。

アセット作成の際に参考にさせていただいたサイト

[Unity]非同期処理(Task)の利用例(UDP送受信,SerialPortのRead) - Qiita

[C#] イベント入門 - Qiita

ZIG SIMが送信するセンサの種類とデータ構造について - Qiita

【Unity】知らなきゃ損! 「SingletonMonoBehaviour」の解説と使い方 | ゆみねこブログ

UnityでJson.NETとJsonUtilityのデシリアライズを比較してみる

たまにC#Jsonのデシリアライズをすることがあるんですけど、そのとき私はよくJson.NETを使うんですよね。「これが稀によくある」ってやつか!? ただ、毎回使い方を忘れているので使う度に調べる羽目になってしまっています。

そこで、今回はJson.NETでのデシリアライズ方法を過去のユースケースを用いて記録として残しておこうと思います。また、せっかくなんでUnityでのJson関連のAPIであるJsonUtilityのデシリアライズ方法も載せておきました。さらにどうせなので、この2つの処理性能を比較してみました。

実行環境

  • macOS 10.15
  • Unity2019.2.10f1

準備

Json.NETはAsset Storeにもあります。しかし、公式のJson.NETの最新バージョンに比べて少し古いバージョンを使っているようなので、今回は公式の最新リリースを使っていきます。

github.com

2019年11月1日時点では最新バージョンは 12.0.2 でした。上記ページからJson120r2.zipをダウンロードし、解凍した後Json120r2/Bin/net45以下にあるNewtonsoft.Json.dllをUnityプロジェクトのAssets/Plugins以下に配置します。

ケースその1

以下のデータからtype1timesを抽出してDateTimeのリストとして保存します。

{
 "type1": {
   "area": "tokyo",
   "times": [
     "2019-10-25 14:33",
     "2019-10-20 23:51",
     "2019-10-26 18:19",
     "2019-10-20 21:08",
     "2019-10-23 23:57",
     "2019-10-23 23:43",
     "2019-10-24 15:37",
     "2019-10-25 04:14",
     "2019-10-24 23:45"
   ]
 },
 "type2": {
   "area": "kyoto",
   "times": [
     "2019-10-22 23:13",
     "2019-10-25 13:19",
     "2019-10-25 12:24",
     "2019-10-20 07:06",
     "2019-10-21 16:10",
     "2019-10-22 11:49",
     "2019-10-20 03:14",
     "2019-10-21 21:33",
     "2019-10-21 04:32"
   ]
 }
}

Json.NETの場合はJson Utilityのようにパースするためのクラスを用意することなくリストを作成できるのがいいですね。リストを作成する際もLINQと合わせて使うことで直感的にデータを持ってくることができます。反対に、Json Utilityは使いたいデータがあるところまでの構造をクラスを使って再現してあげないといけません。このケースのようにネスト構造になっている場合はクラスを2つ作らないといけないため少し面倒ですね。ただ、必要なJsonフィールドの名前の変数だけ用意してあげればいいので少しは気が楽でしょうか?

処理の観測をする際は条件を整えるため1つずつ計測を行いました。例えば、Json.NETの観測をする際は上記コードのようにJson Utilityの部分をコメントアウトしています。

Profilerで見てみると以下の結果になっていました。

Json.NET f:id:yubeshineko:20191102164121p:plain

Json Utility f:id:yubeshineko:20191102164130p:plain

JObject.Parse(Json.NET) JsonUtility.FromJson(Json Utility)
速度(別メソッド呼び出し込み) 16.61ms 0.35ms
GC Alloc 11.4KB 2.8KB

Json Utilityの方が高速で、さらにGC Allocも少ないという結果になりましたね。うーむ、これはUnityでは積極的にJson Utilityを使った方がいいのでは?

ケースその2

以下のゴミの捨て方のデータよりdataオブジェクトの配列中のitemanswerの要素を持つクラスのリストを作ります。ちなみに、このデータはこちらのものを改変して作りました。

{
    "limit": 50,
    "lastUpdate": "2018-03-07 08:40:05",
    "data": [
        {
            "oid": 1,
            "item": "アイスクリームの容器",
            "synonym": "アイスクリームの容器/アイス容器",
            "answer": "1.紙製のものは「古紙類」として雑誌や新聞折込チラシと一緒に束ねて出してください。2.プラマークの表示があるものは「プラスチック製容器包装」として出してください ※ともに汚れが落ちないものは「燃やせるごみ」として出してください",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 2,
            "item": "アイスノン",
            "synonym": "アイスノン/あいすのん",
            "answer": "燃やせるごみ",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 3,
            "item": "アイロン",
            "synonym": "アイロン/あいろん",
            "answer": "燃やせないごみ",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 4,
            "item": "アイロン台",
            "synonym": "アイロン台/あいろん台",
            "answer": "1.足付きのものは、「燃やせないごみ」として出してください。2.板のみのものは、「燃やせるごみ」として出してください",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 5,
            "item": "アクセサリー類",
            "synonym": "アクセサリー類/アクセサリー/あくせさりー/アクセサリ",
            "answer": "燃やせないごみ",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 6,
            "item": "アコーディオン",
            "synonym": "アコーディオン/あこーでぃおん",
            "answer": "燃やせないごみ",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 7,
            "item": "アタッシェケース",
            "synonym": "アタッシェケース・アタッシュケース/アタッシュケース/アタッシェケース",
            "answer": "燃やせないごみ",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        },
        {
            "oid": 8,
            "item": "厚紙",
            "synonym": "厚紙/あつがみ/あつかみ",
            "answer": "古紙類 ※雑誌と一緒に束ねて出してください",
            "label1": "",
            "url1": "",
            "label2": "",
            "url2": "",
            "label3": "",
            "url3": "",
            "label4": "",
            "url4": ""
        }
    ]
}

itemanswerを要素とするクラスをGarbageクラスとしてGarbageクラスのリストを作成します。

こちらのケースでもJson.NETの場合は必要なデータのみを直感的に持ってこれているのが分かっていていいですね。反対に、Json Utilityの場合は2つのクラスを作ってあげないと必要なデータを持ってくることができません。

では、実行して処理結果を見てみましょう。ケース1と同じように片方ずつ実行しています。

Json.NET f:id:yubeshineko:20191102163144p:plain

Json Utility f:id:yubeshineko:20191102163258p:plain

JObject.Parse(Json.NET) JsonUtility.FromJson(Json Utility)
速度(別メソッド呼び出し込み) 26.92ms 0.32ms
GC Alloc 88.5KB 2.1KB

ケース1に比べてかなりの差ができました。やはりUnityではJson Utilityを使った方がパフォーマンス的に良いということですね。

Jsonシリアライズまとめ

  • Json.NET

    • + 必要な部分のみを直感的に引っ張ってこれる
    • Jsonの様々な処理を行える
    • Json Utilityに比べメモリ使用量が多く、処理も時間がかかる
  • Json Utility

    • − 必要な部分のみ引っ張ってきたいときもわざわざクラスを用意する必要がある
    • − 使える機能がJson.NETに比べて少ない
    • Json.NETに比べメモリ使用量が少なく、処理も高速

いわゆる一長一短ってやつですね。リソースが限られている時などはJson Utilityを積極的に使うべきで、そうでもない時はJson.NETを使ってもいいでしょう。

...とは言いつつも、Json Utilityの性能は素晴らしいですね。今後はUnityではJson Utilityを使っていきたくなってしまいます。

というわけで、Json.NETの使い方を残しておこうと記事を書いていたら、思いがけずJson Utilityのハイパフォーマンス性があらわになってしまったという記事でした(笑)

参考

JSON Utility

https://qiita.com/sea_mountain/items/6513b330983ffa003959

https://docs.unity3d.com/ja/2017.4/Manual/JSONSerialization.html

JSON.net

https://www.newtonsoft.com/json/help/html/Introduction.htm

https://www.newtonsoft.com/json/help/html/SerializingJSONFragments.htm

DateTimeカスタム書式設定

https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/custom-date-and-time-format-strings