yotiky Tech Blog

とあるエンジニアの備忘録

Unity - Sentry を利用する

目次

検証環境

  • Unity 6000.0.34f1
  • Sentry Unity 3.0.2

導入

公式ドキュメント。

docs.sentry.io

  1. SentryのWebページから、Settings > Projects から特定のProjectを選択 > Client Keys(DSN) のページを開いて、DSNをコピー

  2. パッケージマネージャーからGit URLでパッケージを追加する https://github.com/getsentry/unity.git#3.0.2

  3. メニュー > Tools > Sentry で構成画面を開く

  4. DSNを設定する

後はエラーなどを発生させるとレポートが送信される。 Webページに反映されるには、数秒程度のラグがある。

基本

Unity SDKは .NET SDK をベースに構築されている。

.NET SDK では、SentrySdk.Init を使ってコードベースで初期化している。 Unityでは、構成画面を開くと Resources/Sentry/SentryOptions.asset (ScriptableObject)が生成され、それを元に自動で初期化される。

構成画面

スクロールバーとか表示されないので、見切れてる項目には注意が必要。

Core

  • 基本的には、DSNさえ設定すれば動く

Enrichment

  • Stacktrace For Logs にチェックを入れると、スタックトレースを送信してくれる
  • Attach Screenshot にチェックを入れると、送信する際にスクショを添付してくれる
    • 契約プラン毎に月の容量があるので注意

Transport

Advanced

Options Config

  • Option Config Script を設定すると送信前にタグを追加するなどの処理を仕込める
public class SentryOptionConfiguration : SentryOptionsConfiguration
{
    public override void Configure(SentryUnityOptions options)
    {
        // Here you can programmatically modify the Sentry option properties used for the SDK's initialization
        options.SetBeforeSend(@event =>
        {
            @event.SetTag("lib", "SentryTest");
            return @event;
        });
    }
}

Debug Symbols

プログラム

初期化

.NET には初期化のために SentrySdk.Init が用意されている。 Unityは基本構成画面から生成されるScriptableObjectを使うことになるが、SentryUnity.Initメソッドも一応用意されている。

SentryUnity.Init(options =>
{
    options.Dsn = HoloearthUnityDsn;
});

ただし、ドキュメントには明記されておらず、サンプルコードの一部にSentrySdkとSentryUnityが混在しているので混乱する。(改善要望提案中)

動きも若干怪しい所があるため、使う際は注意が必要である。

    // こちらは機能する
    options.AttachStacktrace = true;
    // こちらは機能しない
    options.AttachScreenshot = true;

Screenshotに関しては、自分でEventProcessorを追加することでスクショが送信されるようになることは確認できた。

    options.AttachScreenshot = true;
    options.ScreenshotQuality = ScreenshotQuality.Low;
    options.AddEventProcessor(new ScreenshotEventProcessor(options)); 

スコープ

Scopeと呼ばれる設定の箱が用意されており、定義したり破棄したりできるみたい。

スコープには、イベントとともに送信される有用な情報が保持されます。たとえば、コンテキストやパンくずリストはスコープに保存されます。スコープがプッシュされると、親スコープからすべてのデータが継承され、ポップされるとすべての変更が元に戻されます。

SentrySdk.ConfigureScope(scope =>
{
    scope.User.Id = "12345";
    scope.User.Username = "yotiky";
    scope.User.Email = "example@example.com";
    scope.Environment = "dev";
    scope.SetTag("GroupId", "67890");
    scope.SetTag("GroupName", "hoge");
});

ドキュメントには、ログアウト時にユーザーの設定を解除するときのサンプルとして以下が掲載されているが、個別にすべてを初期化しないとダメなのか、読み取れず。しなきゃいけない気もする。

SentrySdk.ConfigureScope(scope =>
{
    scope.User = new SentryUser();
});

その他、色々カスタマイズするできるっぽいので以下参照。とても読みやすいとは言えないけれど。。

docs.sentry.io

パッケージの分離

コアライブラリにSentryを設定して、アプリケーション側からはUPM参照して使う場合、以下のことに注意しておく必要がある。

  • PackageにScriptableObjectがあったとしても、アプリケーション側で画面を開くと新たに設定ファイルが生成される
  • SentryUnity.Initの動きが怪しい

Web画面

Issues

同様の特性を持つイベントごとにグループ化されて表示される。

「Custom Search」はSaveすることができる。一番良く使うものをデフォルトに設定しておくと良い。

リアルタイムに更新したい場合、右上にボタンがあるので押しておく。

Discover

Discoverにエラーのレポートが表示される。

Consoleに出力されたErrorがすべて送信されているわけではなさそう。 UnityEngine.Debug:LogErrorの出力を転送しているっぽい。UnityEngine自体が自身で出力しているエラーは送信されていなかった。

エラーの詳細画面を開くと、スタックトレースやパンくず、タグやコンテキスト情報などが参照できる。 パンくずについては、以下引用から。

Sentry は、パンくずリストを使用して、問題が発生する前に発生したイベントの軌跡を作成します。

Tag

Attach Screenshot を有効にするとタグの右側にスクショが表示される。

独自のタグを設定するには、SentryEvent.SetTagやScope.SetTagなどで設定する。 また、Scope.Environmentに設定したenvironmentは、Tagsの中に表示される。

Environmentは、デフォルトでも設定される。その場合、Unity Editorで実行している場合はeditor、それ以外の場合はproductionが設定される。

User

Contextsには様々な情報が表示される。次の画像はUserのブロック。 IP AddressやGeographyが出力されることがあるが、出力条件がはっきりしない。

Scope.UserからUserの情報を設定できる。new SentryUser()するとすべて上書きされるため、Userのプロパティを直接編集するとIP Addressとかは残りそう。 と思ったが、出たり出なかったりしたのでよく分からなくなった。

  scope.User = new SentryUser()
  {
      Id = "12345",
      Username = "yotiky",
      Email = "example@example.com",
      IpAddress = "127.0.0.1",
      Other = new Dictionary<string, string>()
      {
          { "hoge", "1" },
          { "fuga", "2" },
      }
  };

一覧のUSER.DISPLAYには、Email、Username、Idの順に設定されてるものから表示される。何も設定されていなければ自動生成された値がIdに設定されてそれが表示される。

IP Addressは、Web側の設定で収集しないようにもできるらしい。

LitMotion Animation を拡張する

注意点として、Awake で実行すると予期せぬ動きになるのでStart以降で実行するのが良さそう。

完了

モーションを完了させる。未再生の場合は再生してから完了させる。 モーションは再生中のままになる。

public static void Complete(this LitMotionAnimation animation)
{
    if (!animation.IsPlaying) animation.Play();
    
    foreach (var component in animation.Components)
    {
        var handle = component.TrackedHandle;
        handle.TryComplete();
        component.TrackedHandle = handle;
    }
}

キャンセル

再生中のモーションをキャンセルする。 備え付けのStopだとモーションの状態が破棄されてオブジェクトの元の状態に戻る。 Cancelはキャンセルした時の状態を残す。

public static void Cancel(this LitMotionAnimation animation)
{
    foreach (var component in animation.Components)
    {
        var handle = component.TrackedHandle;
        handle.TryCancel();
        component.TrackedHandle = handle;
    }
}

再生速度を指定して再生

再生速度を指定して再生する。引数は倍率。1fで等倍、0で一時停止。

public static void Play(this LitMotionAnimation animation, float playbackSpeed)
{
    if (!animation.IsPlaying) animation.Play();
    
    foreach (var component in animation.Components)
    {
        var handle = component.TrackedHandle;
        handle.PlaybackSpeed = playbackSpeed;
        component.TrackedHandle = handle;
    }
}       

モーションの最初の状態にする

モーションの最初の状態に遷移させる。再生はキャンセルするので未再生状態になる。

public static void Rewind(this LitMotionAnimation animation)
{
    if (!animation.IsPlaying) animation.Play();
    
    foreach (var component in animation.Components)
    {
        var handle = component.TrackedHandle;
        handle.Time = 0;
        component.TrackedHandle = handle;
    }
    animation.Cancel();
}

こっちは上手く動かないバージョン。連続で呼ぶと機能しないっぽい。

public static void RewindBug(this LitMotionAnimation animation)
{
    animation.Restart();
    animation.Cancel();
}

モーションの最初の位置を取得する(不可)

settings.StartValueはアクセスできないので位置を取得することは不可能だった。

public static void StartPosition(this LitMotionAnimation animation)
{
    var startPos = (TransformPositionAnimation)animation.Components.FirstOrDefault(x => x is TransformPositionAnimation);
    if (startPos == null) return;
    // アクセス不可
    //Debug.Log(startPos.settings.StartValue);
}

Fork - git worktreeを利用する

検証環境

  • Fork 2.6.0.0

使い方

Fork で Worktreeを使う場合、メニューから「Add Worktree...」を実行する。 現状メニューからしか追加できない。

Worktreeに追加できるのは、ローカルに存在するブランチのみ。ロケーションでディレクトリを指定する。

次の場合、ローカルにはmainしかないので、mainしか選べない。

Worktreeが追加されると、左のペインに項目が表示されるようになる。

あとは右クリックから、タブで開いて普通に作業したりコミットしたり。Deleteもここから行える。

補足

チェックアウトした通常のブランチとWorktreeで追加したブランチがディレクトリ毎に共存することになる。

各ブランチはBranchesか、Worktreesと対になる。1つのブランチをBranchesとWorktreesの両方に紐づけたり、Worktreesの中で複数持ったりはできない。

Worktreeの中の .git はフォルダではなくファイルとなる。中身はBranchesでチェックアウトしてる元のディレクトリの中の .gitフォルダへのパス。 (より正確にはその中に含められたWorktreeの情報へのパス)

あと、32GBくらいのブランチをWorktreeしたら16GBくらいのディレクトリが出来上がったので、メインの作業ブランチほどではないがそれなりには容量は食われることは頭の片隅に置いておきたい。

簡単な操作は以下参考。

qiita.com

ユースケース

  • PRのレビュー用
  • 差分や動きの確認のために読み取り専用ブランチを置いておくなど
  • マルチなブランチの作業時に
    • 次の作業に移ったけど、レビュー結果の修正を行うために元のブランチを触るなど
  • すべての作業をworktreeで実施するケースも

参考

Feature request: Support multiple working tree · Issue #527 · fork-dev/Tracker · GitHub

Unity - 自作パッケージの dependencies の gitベースの依存関係を解決する

Unity Package Managerで自作のパッケージを参照する場合、自作のパッケージのpackage.jsonにdependenciesを設定しても、gitベースの依存関係は解決してくれない。
以下のパッケージを追加することで、gitベースの依存関係を解決してくれるようになる。

github.com

ただし、サブモジュールなどに置いた自作のパッケージをディレクトリ参照してるとそもそも対象にならない。
自作のパッケージ自体がgitベースで参照されてないとだめなようだ。

そこで以下の内容を雑に対応した。

github.com

自作パッケージ側のpackage.jsonの例

    "dependencies": {
      "com.unity.localization": "1.5.4"
    },
    "gitDependencies":{
      "com.mycompany.mypackage": "https://github.example.com/myuser/myrepository.git",
      "com.mycompany.mypackage2": "git+ssh://git@github.example.com/myuser/myrepository2"
    },

参考

UnityにおけるDI依存症への警告 (Warning excessive dependence on dependency injection in Unity)

はじめに

昨今Unity界隈では、DIが流行していると言っても過言ではない。 依存性注入(DI)とDIコンテナは、モダンなソフトウェア開発において広く使用されているパターンだが、その使用には慎重な考慮が必要である。特にUnityのようなゲーム開発環境では、設計が複雑になりがちであり、適切に理解して使用することが重要である。

目次

適切な使用方法

DIは設計の中心に置くのではなく、特定の目的を持って限定的に使用すべきである。

  • モジュール境界やレイヤー境界での依存性逆転に使用する
  • 同じインターフェイスで実装を置き換える処理
  • テスタビリティ向上が必要な箇所に導入する
  • ロギングなど横断的関心事を分離する

DIの本質的な課題

依存性注入(DI)、特にDIコンテナを使用した時の主な課題は以下の点にある。

  • 静的なコード解析では、参照関係やオブジェクトのライフサイクルが追いづらい
  • 動的なコード解析では、実行順やある時点での相関関係を把握しづらい

DIはコンポーネント(クラス)内だけに注視した時、意識する範囲が狭くなるため、可読性が高まるとも言える。一方でその境界線を超えた範囲を意識した時、コードのリーダビリティとデバッガビリティは低下していると言わざるを得ない。

これは特に保守運用フェーズにおいて致命的になりかねない。バグFIXや機能改修では、変更点の影響範囲を調べることになるが、その場合においても難しさを招くだろう。

また、コードは書く時間より読む時間の方が長いとも言われる。参照が辿れないことは、読むことを難しくする。

DI(IoC)コンテナはオブジェクトを作成する責務をユーザーから奪う。 ある時点でオブジェクトが注入される時、注入されたオブジェクトにも何かが注入され、注入は連鎖し、依存関係のチェーンによって注入のウォーターフォールが形成される。 プログラムが成長すると連鎖は巨大になり、大量のオブジェクトをあらゆる場所に注入し、手に負えなくなる。

さらに未来を見ると、 DIを使った設計は、例えオープンクローズドなコンポーネントを設計していたとしても、依存関係はスコープのないグローバル変数のようなものなので、影響範囲が未来に向かって無限にオープンになってしまう。

複雑さのトレードオフ

複雑さは言い換えると、使いにくい、理解が難しい、メンテしにくい、覚えきれない、実行が難しい、などと言える。

ソフトウェアの複雑さには、単一コンポーネント内の複雑さとコンポーネント間の関係性の複雑さという2つの側面がある。DIを導入することで、一方の複雑さを解決しようとすると、別の形の複雑さが生まれる可能性がある。

これらはシステム的な視座を上げると、マイクロサービスとも似たような話である。 DIもマイクロサービスも手にしたメリットのうらで複雑さを生み出して、問題の所在を移動しているだけであることは常に意識したい。

実態の所在

目的を見失ったDIにおいては、オブジェクトの生成、どこからでもアクセス可能な点でSingletonと本質は同じとも言える。インスタンスを能動的に取りに行くか、受動的に受け取るかの違いしかない。荒い言い方をすれば、SingletonかServiceLocatorかDIか、は、実態までの距離の違いだ。

問題の所在を見えない所に移動しているDIは、リーダビリティやデバッガビリティという点では最悪で、コードもおえてデバッグがしやすいSingletonやServiceLocatorの方がまだましな場合すらある。

Clean Architecture(クリーンアーキテクチャ本)

クリーンアーキテクチャの書籍では、DIコンテナはメインコンポーネント(エントリーポイント)のみに使用すると説明していたはずだ。メインコンポーネントからプログラムでわざわざ参照関係を構築している。あの話では、フレームワークと結婚するなという題目で紹介されていたと思う。

しかし、重要なのはそれだけではない、裏に隠されたより重要なことがある。

DIコンテナの使用方針が定まらず、侵食がプログラム全体に及ぶと、DIありきの思想が設計を蝕む。これこそが重大な懸念であり、これが「フレームワークと結婚すべきではない」を超えた理由となる。単にフレームワークを使ってるからという枠を超えて設計の中枢に影響を及ぼしかねない。

昨今のクリーンアーキテクチャ解説では、DIフレームワークが当たり前のように説明されてることも多いが、ここは結構重要なところだと考える。

1つのエントリーポイントとまでは言わないまでも、「適切なサイズでグルーピングして、そのグループへのエントリーポイントを作る」ことで、DIコンテナへの依存を局所的にすることは可能だろう。

Unity とDIの関係

そもそものそもそもの話、Unityそれ自体がDIコンテナでIoCであると言える。InspectorやScriptable Objectでポチポチやってるのは、依存性を注入してるわけだ。 つまり、Unity EditorはDIコンテナのビジュアルエディタである。 Scriptから見ると、見えない所(Inspector)で参照関係を築くものだったり、シリアライズできる何かだったりに見えるが、実態は「注入するための機構」である。

プログラマから見たらDIは実態が見えづらく、Singleton、ServiceLocatorはまだ見えやすい距離と言える。一方で、プログラマ以外のゲームデザイナーやアニメーターなどUnity Editorしか触らない人にとっては、Unity Editorで参照関係が見える方が便利である。

プログラム目線からの使用目的だけでなく、プロジェクトやチームの状況に応じてもDIをどう使うかは変わってくると言える。

設計における考慮事項

良い設計や設計力を身につけるためには、以下の点を考慮すると良いだろう。

  • DIを使わなくても設計できる能力を身につける
  • 複雑さのバランスを考慮する(コンポーネント内の複雑さvs関係性の複雑さ)
  • システムの柔軟性と複雑さのトレードオフを意識する

DIを使わないと設計できないのと、DIを使っても使わなくても設計できることには雲泥の差がある。

おすすめの記事

DIコンテナの本当の使いどころ | ウルシステムズ株式会社

2005年のJava界隈の記事。今からおよそ20年前の業務系のシステムでDIコンテナが流行ってた頃の追体験ができる。DIのアンチパターンなどは読んでおいて損はない。

TODO: Unity 3.5くらいの時代に、UnityにIoCコンテナを持ち込んだ人が後にIoCコンテナを投げ捨てた記事があったので後で貼る

関連記事

yotiky.hatenablog.com

yotiky.hatenablog.com

yotiky.hatenablog.com

複数のリポジトリをモノレポ化する (multirepo to monorepo)

TL;DR

  • 複数のリポジトリをモノレポ化した際の備忘録
  • tomono スクリプトを修正したスクリプトを使用
  • リポジトリを移行することにフォーカスした記事
  • 以下の内容はこの記事では扱わない
    • モノレポ自体の説明や移行の検討
    • CI/CD、ツールなど
    • 運用ルール
  • サンプルのリポジトリを公開してるのでお試し可能

目次

概要

移行前

移行後

  • リポジトリ:monorepo
    • ModuleAフォルダ
      • READMEほか
    • ModuleBフォルダ
      • READMEほか
    • ModuleCフォルダ
      • READMEほか
    • ModuleDフォルダ
      • READMEほか

手順

1. 移行するものを決める

  • HEADのファイル、履歴、Git LFSファイル
  • タグ
  • ブランチ
  • issue
  • プルリク

今回は以下を移行対象とした

  • 履歴
  • Git LFSファイル
  • タグ
  • ブランチの一部

2. 新しいリポジトリを作る

新しいリポジトリを作成する。リポジトリの設定など必要な情報を引き継ぐ。

3. 準備

作業ディレクトリを作成する。

tomono リポジトリから以下のファイルを作業ディレクトリに保存する。

github.com

  • tomono
  • tomono.pushall
  • tomono.removeremotes

以下の内容を repos.txt という名前で作成する。

[git@github.com](mailto:git@github.com):yotiky/monorepo-test.ModuleA.git ModuleA
[git@github.com](mailto:git@github.com):yotiky/monorepo-test.ModuleB.git ModuleB
[git@github.com](mailto:git@github.com):yotiky/monorepo-test.ModuleC.git ModuleC
https://github.com/yotiky/monorepo-test.ModuleD.git ModuleD ModuleD lfs

4. リポジトリを移行する

Git Bashを起動して作業ディレクトリに移動する。

以下の内容を順に実行していく。

export MONOREPO_NAME=Monorepo
export MONOREPO_BRANCHES="main|branch-3$|^ModuleD"

./tomono < repos.txt

cd Monorepo

git remote add origin git@github.com:yotiky/monorepo-test.monorepo.git

../tomono.removeremotes  < ../repos.txt
../tomono.pushall ../merged_branches.txt

5. 検証

  • ブランチの作成、プルリク、マージが行えるか
  • git lfs fetch --all でGit LFSのチェック
  • アプリなどの動作確認

補足

サンプルリポジトリ

  • 各リポジトリでタグ、ブランチなどを設定済み
  • ModuleBはModuleCをサブモジュールとして参照
  • ModuleDはGit LFSを利用

tomono スクリプト

  • 異なるリポジトリの同じ名前のブランチは1つにマージされる
    • 例) ModuleA/main、ModuleB/mainは、origin/mainにマージ
  • タグはリポジトリ名で名前空間毎に割り振られる
    • 例) ModuleA/release1、ModuleB/release1
  • LFSを使ってる場合はSSHではなくHTTPSのアドレスを指定する
  • repos.txt は最後に空行が必要
  • MONOREPO_BRANCHES は正規表現が使える
    • "main|develop" - mainまたはdevelopブランチのみ
    • "release.*" - releaseで始まる全てのブランチ
    • ".*" - 全てのブランチ

注意点

フィーチャーブランチ

フィーチャーブランチなど、ブランチを複数持っていく場合は、移行後のリポジトリでマージのコンフリクトが発生するので注意する必要がある。
移行前に最新にしていたとしても、移行の過程で各ブランチにファイルを追加、移動するコミットが入ることになる。フィーチャーブランチで変更していたものは基本的に衝突することになる。
ファイルの削除やリネームを行っていた場合、コンフリクトの解消では解消できないのでより注意が必要である。マージしようとした時にフィーチャーブランチでファイルを削除していたとしても、そもそも移行すること自体がファイルの追加であるため、追加されていないファイルを削除することができない状態となるようだ。一度追加したmainブランチで解消した上で削除するなどする必要があった。(Fork上)

できる限り少ないブランチを対象にすることをおすすめする。

Git LFSファイル

今回修正したスクリプトは、git lfs push する時にブランチを指定している。 ブランチに関わらずすべてのLFSファイルを引っ越したい場合は、元のリポジトリに新リポジトリをremoteとして追加して、git lfs push <新リポジトリ> --allして、新リポジトリ側にPUSHしてしまうのが良いだろう。

移行元リポジトリでLFSをPUSH

手順の新しいリポジトリを作成した後に、移行元のリポジトリ、今回だとModuleDで以下の作業を実施する。

git lfs fetch --all
git remote add monorepo git@github.com:yotiky/monorepo-test.monorepo.git
git lfs push monorepo --all
git remote remove monorepo

サンプルリポジトリ

ModuleA~D

Monorepo

error: Authentication required: You must have push access to verify locks

複数アカウントを使ってて、操作対象のリポジトリとは異なるアカウント情報が使われてる時に起きてた。

リポジトリを持つアカウントで操作するように設定したら行けた。