yotiky Tech Blog

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

ピックアップ記事

コラム

yotiky.hatenablog.com

設計

yotiky.hatenablog.com

yotiky.hatenablog.com

yotiky.hatenablog.com

技術

yotiky.hatenablog.com

その他

Unity - CancellationTokenの生成・使い方完全ガイド

UniTask(Cysharp製)で非同期処理を行う際に欠かせない CancellationToken の使い方を、生成方法・基本構文・応用・GC削減テクニック・ベストプラクティスまで、完全網羅で解説します。


🗂 目次


✅ CancellationToken の生成・取得手段【完全リスト】

1. 明示的に生成する

1-1. 通常の CancellationTokenSource

var cts = new CancellationTokenSource();
var ct = cts.Token;

1-2. タイムアウト付き CancellationTokenSource

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var ct = cts.Token;

1-3. CancelAfterSlim を使ったキャンセル予約

var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(5));
var ct = cts.Token;

2. UniTask 独自の生成方法

2-1. TimeoutController による生成

var tc = new TimeoutController();
var ct = tc.Timeout(TimeSpan.FromSeconds(5));
tc.Reset(); // 再利用可能にする

2-2. 外部 CancellationTokenSource を渡して管理

var cts = new CancellationTokenSource();
var tc = new TimeoutController(cts);

3. Unity のライフサイクルと連動した生成

3-1. GameObject 破棄時

var ct = this.destroyCancellationToken;        // Unity 2022.2+
var ct = this.GetCancellationTokenOnDestroy(); // 全バージョン対応

4. システムイベント起因の生成

4-1. アプリ終了時

var ct = Application.exitCancellationToken;

5. 複数トークンの合成生成

5-1. CancellationTokenSource.CreateLinkedTokenSource

var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct1, ct2, ct3);
var ct = linkedCts.Token;

6. 外部から受け取るトーク

6-1. メソッド引数で受け取る

private async UniTask ExecuteAsync(CancellationToken ctParam)
{
    await SomeTask(ctParam);
}

7. 特殊な固定トーク

7-1. CancellationToken.None

await UniTask.Delay(5000, cancellationToken: CancellationToken.None);

🔧 基本的な使い方一覧

項目 説明
cts.Cancel() 任意のタイミングでキャンセルを発行する。
cts.Token CancellationToken を取得して非同期処理に渡す。
ct.ThrowIfCancellationRequested() キャンセル済みなら即座に例外を投げる。
await UniTask.XYZ(..., cancellationToken: ct) 非同期タスク中にトークンを渡し、キャンセル可能にする。

サンプルコード

cts.Cancel()

var cts = new CancellationTokenSource();
button.onClick.AddListener(() => cts.Cancel());

cts.Token

var cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

ct.ThrowIfCancellationRequested()

void DoWork(CancellationToken ct)
{
    ct.ThrowIfCancellationRequested();
    Debug.Log("キャンセルされていないので処理継続");
}

await UniTask.Delay(..., cancellationToken: ct)

var cts = new CancellationTokenSource();

try
{
    await UniTask.Delay(5000, cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
    Debug.Log("処理がキャンセルされました");
}

🚀 応用的な使い方一覧

応用技法 説明
CreateLinkedTokenSource(...) 複数トークンをまとめ、どれかがキャンセルされたら全体を中断。
TimeoutController.Reset() タイムアウトトークンを再利用するために呼び出す。
GetCancellationTokenOnDisable() GameObject が無効化されたときに自動キャンセル。
Application.exitCancellationToken アプリ終了時にキャンセルが発生。
SceneManager.activeSceneChanged + cts.Cancel() シーン変更時に手動キャンセル。
EditorApplication.quitting + cts.Cancel() Unity Editor 終了時にキャンセル処理。
CancellationToken.None キャンセル不可な処理。
CancellationToken.Register キャンセル発生時の処理を登録。
DOTS / TaskScheduler連携 外部スレッドやジョブにキャンセルを反映。

サンプルコード

CreateLinkedTokenSource(...)

var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
await UniTask.Delay(10000, cancellationToken: linkedCts.Token);

TimeoutController.Reset()

var tc = new TimeoutController();
var ct = tc.Timeout(TimeSpan.FromSeconds(5));

try
{
    await UniTask.Delay(10000, cancellationToken: ct);
}
catch (OperationCanceledException)
{
    Debug.Log("タイムアウトによりキャンセルされました");
}

tc.Reset();

GetCancellationTokenOnDisable()

await UniTask.Delay(3000, cancellationToken: this.GetCancellationTokenOnDisable());

Application.exitCancellationToken

await UniTask.Delay(5000, cancellationToken: Application.exitCancellationToken);

SceneManager.activeSceneChanged

var cts = new CancellationTokenSource();

SceneManager.activeSceneChanged += (oldScene, newScene) => {
    cts.Cancel();
};

await SomeAsyncTask(cts.Token);

EditorApplication.quitting

#if UNITY_EDITOR
var cts = new CancellationTokenSource();
EditorApplication.quitting += () => cts.Cancel();
await UniTask.Delay(5000, cancellationToken: cts.Token);
#endif

CancellationToken.None

await UniTask.Delay(3000, cancellationToken: CancellationToken.None);

CancellationToken.Register

var cts = new CancellationTokenSource();
cts.Token.Register(() => 
{
    // キャンセル発生時の処理
});

DOTS/Task連携(擬似例)

var cts = new CancellationTokenSource();
NativeArray<bool> cancelFlag = new NativeArray<bool>(1, Allocator.Persistent);

JobHandle job = new CancelableJob { cancelFlag = cancelFlag }.Schedule();

UniTask.Void(async () =>
{
    await UniTask.Delay(3000, cancellationToken: cts.Token);
    cancelFlag[0] = true;
});

🧪 実践

キャンセル処理とタイムアウトの組み合わせ

public async UniTask RunTaskWithFlexibleCancellation(CancellationToken externalToken)
{
    var manualCts = new CancellationTokenSource();
    var timeoutCts = new CancellationTokenSource();
    timeoutCts.CancelAfterSlim(TimeSpan.FromSeconds(10));
    var destroyToken = this.GetCancellationTokenOnDestroy();

    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        manualCts.Token, timeoutCts.Token, destroyToken, externalToken
    );

    try
    {
        await UniTask.Delay(30000, cancellationToken: linkedCts.Token);
    }
    catch (OperationCanceledException ex)
    {
        if (timeoutCts.IsCancellationRequested)
            Debug.Log("タイムアウトでキャンセル");
        else if (destroyToken.IsCancellationRequested)
            Debug.Log("破棄でキャンセル");
        else if (externalToken.IsCancellationRequested)
            Debug.Log("親トークンでキャンセル");
        else
            Debug.Log("手動キャンセル");
    }
}

♻️ 再利用可能な設計で GC 削減(Stackベース)

static class CtsPool
{
    private static readonly Stack<CancellationTokenSource> pool = new();

    public static CancellationTokenSource Rent() =>
        pool.Count > 0 ? pool.Pop() : new CancellationTokenSource();

    public static void Return(CancellationTokenSource cts)
    {
        if (cts.IsCancellationRequested)
            cts.Dispose();
        else
        {
            cts.Reset();
            pool.Push(cts);
        }
    }
}

🏆 ベストプラクティス

ObjectPool と UnsafeRegister によるゼロアロケーション設計

この設計は、UniTask の作者でもある Cysharp の開発者が解説している公式記事:

👉 CancellationTokenとゼロアロケーション(neue.cc, 2022年7月13日)

の解説です。


🎯 このコードの目的

  • CancellationTokenSourceObjectPool で再利用し GCを抑制
  • UnsafeRegister により 複数トークンを軽量に統合
  • 発生源に応じた 適切な例外の再スロー
  • 明示的な Dispose管理とプール返却

📄 実装コード(整形済み)

class Client : IDisposable
{
    readonly TimeSpan timeout;
    readonly ObjectPool<CancellationTokenSource> timeoutTokenSourcePool;
    readonly CancellationTokenSource clientLifetimeTokenSource;

    public TimeSpan Timeout { get; }

    public Client(TimeSpan timeout)
    {
        this.Timeout = timeout;
        this.timeoutTokenSourcePool = ObjectPool.Create<CancellationTokenSource>();
        this.clientLifetimeTokenSource = new CancellationTokenSource();
    }

    public async Task SendAsync(CancellationToken cancellationToken = default)
    {
        var timeoutTokenSource = timeoutTokenSourcePool.Get();

        CancellationTokenRegistration externalCancellation = default;
        if (cancellationToken.CanBeCanceled)
        {
            externalCancellation = cancellationToken.UnsafeRegister(static state =>
            {
                ((CancellationTokenSource)state!).Cancel();
            }, timeoutTokenSource);
        }

        var clientLifetimeCancellation = clientLifetimeTokenSource.Token.UnsafeRegister(static state =>
        {
            ((CancellationTokenSource)state!).Cancel();
        }, timeoutTokenSource);

        timeoutTokenSource.CancelAfter(Timeout);

        try
        {
            await SendCoreAsync(timeoutTokenSource.Token);
        }
        catch (OperationCanceledException ex) when (ex.CancellationToken == timeoutTokenSource.Token)
        {
            if (cancellationToken.IsCancellationRequested)
            {
                throw new OperationCanceledException(ex.Message, ex, cancellationToken);
            }
            else if (clientLifetimeTokenSource.IsCancellationRequested)
            {
                throw new OperationCanceledException("Client is disposed.", ex, clientLifetimeTokenSource.Token);
            }
            else
            {
                throw new TimeoutException($"The request was canceled due to the configured Timeout of {Timeout.TotalSeconds} seconds elapsing.", ex);
            }
        }
        finally
        {
            externalCancellation.Dispose();
            clientLifetimeCancellation.Dispose();
            if (timeoutTokenSource.TryReset())
            {
                timeoutTokenSourcePool.Return(timeoutTokenSource);
            }
        }
    }

    async Task SendCoreAsync(CancellationToken cancellationToken)
    {
        // 通信処理など
    }

    public void Dispose()
    {
        clientLifetimeTokenSource.Cancel();
        clientLifetimeTokenSource.Dispose();
    }
}

🔍 処理解説(要点まとめ)

処理 解説
ObjectPool.Get() / Return() 再利用によってGC削減
UnsafeRegister(...) 複数トークンのキャンセルを1つに束ねる高速API
CancelAfter(...) タイムアウト機能
TryReset() 再利用可否チェック(.NET 6+)
Dispose() の明示管理 メモリリーク防止・登録解除保証

✅ メリット

  • ゼロアロケーションnew を避けることでGC抑制
  • 正確な例外制御:キャンセル原因に応じた例外構築
  • 拡張性と汎用性:大規模アプリにも適用可能

⚠️ 注意点

  • TryReset().NET 6 以上限定
  • UnsafeRegister を使う場合は 登録解除(Dispose)を必ず行う
  • Unityで使用する場合は、**ObjectPool を拡張導入(NuGetなど)**が必要
  • UniTask 環境なら CancelAfterSlim へ差し替えると軽量化

🛠 トラブルシューティング:CancellationToken 利用時のよくある問題と対策


❌ 1. CancellationTokenSource が使い回されていて、即キャンセルされる

原因:

cts.Cancel() 呼び出し後に CancellationTokenSource をそのまま再利用している。

解決策:

if (cts.IsCancellationRequested)
{
    cts.Dispose();
    cts = new CancellationTokenSource();
}

❌ 2. CreateLinkedTokenSource() の使いすぎによる GC 発生

原因:

頻繁に CreateLinkedTokenSource() を呼び出すことで GC アロケーションが蓄積される。

解決策:

  • UnsafeRegister() による手動連携で GC を抑制。
  • 再利用可能なキャンセル設計に切り替える。

❌ 3. CancellationToken.None がキャンセルされると誤解している

原因:

CancellationToken.None は「絶対にキャンセルされない固定トークン」であり、キャンセル機能は無い。

解決策:

中断可能な処理には必ず CancellationTokenSource.Token を使用する。


❌ 4. UnsafeRegister() の解除忘れによるリーク

原因:

UnsafeRegister() で登録したキャンセルリスナーを .Dispose() せずに放置。

解決策:

var reg = ct.UnsafeRegister(...);
try
{
    await SomeAsyncTask(ct);
}
finally
{
    reg.Dispose();
}

❌ 5. OperationCanceledException を補足せずクラッシュ

原因:

非同期処理中にキャンセルされたとき、例外補足をしておらず unhandled exception に。

解決策:

try
{
    await SomeAsyncTask(ct);
}
catch (OperationCanceledException)
{
    Debug.Log("キャンセルされました");
}

❌ 6. LinkedTokenSource を再利用して例外になる

原因:

CreateLinkedTokenSource() で作成したトークンは1回限りの使い切り用。再利用は未サポート。

解決策:

  • 処理ごとに新規で生成し、終わったら Dispose()
  • 頻繁な再利用が必要な場合は UnsafeRegister 方式へ移行。

❌ 7. MissingReferenceException: DestroyCancellation token should be called...

エラーメッセージ:

UnityEngine.MissingReferenceException: DestroyCancellation token should be called atleast once before destroying the monobehaviour object

原因:

非同期処理の中で、すでに破棄された MonoBehaviourdestroyCancellationToken を参照した。

再現コード(NGパターン):

await UniTask.Delay(1000);
// この時点で this が破棄済み
var ct = this.destroyCancellationToken; // → MissingReferenceException

解決策(OKパターン):

var ct = this.destroyCancellationToken; // 事前にキャッシュ
await UniTask.Delay(1000, cancellationToken: ct);

補足と参考リンク:


デバッグ支援Tips

テクニック 効果
ct.IsCancellationRequested をログ出力 キャンセル発生のタイミング調査に有効
Debug.LogException(e) 発生した例外のスタックトレースと原因を把握
.TryReset()CTS 再利用可否を判定 GC抑制しつつ安全に再利用可能

このセクションは、UniTask + CancellationToken のトラブルを未然に防ぎ、再発を抑えるための実践的なチェックリストです。 特に「非同期処理+Unityのライフサイクル」はバグの温床となるため、上記の設計・パターンで安定性を確保することが重要です。


参考

Unity - 横断的な例外処理

UniRxやUniTaskなどを使った場合の横断的な例外処理についてのメモ書き。

基本的にUniRxでのExceptionは、ストリームが壊れないように堅牢な方向に機能が実装されている。ストリームの内から外へ例外を搬出するのは楽ではない。そういう閉じた世界(例えば画面単位など)で使うのがベターそう。

SubjectのSubscribeのonErrorコールバック引数

  • ストリームで起きたエラーが入ってくる
  • Subscribeで起きたエラーは入らない
.Subscribe(
    _ => throw new Exception(),
    e => Debug.Log("ここには来ない"));

前段にDoをかます

  • 後段のSubscribeのonErrorに流れる
.Do(_ => throw new Exception())
.Subscribe(
    _ => {},
    e => Debug.Log("ここには来る"));
  • ただし、Do(async _ => xxx)と非同期にしてると流れない
  • 間にObserveOnMainThreadしてもダメ
.Do(async _ => throw new Exception())
.Subscribe(
    _ => {},
    e => Debug.Log("ここには来ない"));

ストリームからのthrow

  • throwしてもストリームの外には伝搬しない
  • 外でUniTaskCompletionSourceで状態待機してる場合は、completed.TrySetException(e)で外のタスクに例外を起こさせることができる
.Do(_ => throw new Exception())
.Subscribe(
    _ => {},
    e => _completed.TrySetException(e));

ReactivePropertyのストリーム

  • Subjectと同じ

MessageBrokerでPub/Sub

  • 使い方と使うとこ絞れば便利そう
// Pub
MessageBroker.Default.Publish(new AbortApplicationException());

// Sub
MessageBroker.Default.Receive<AbortApplicationException>()
    .Subscribe(_ =>
    {
        Debug.LogError("MessageBroker.AbortApplicationException");
        GoToTitle();
    })
    .AddTo(this);

Lopp + while のステートマシン

  • 例外が自然に伝搬するのでこれが楽そう
private async UniTask MainLoop()
{
    while(current != State.End)
    {
        switch(current)
        {
            // 各ステートの処理
            case State.Start:
                await Hoge();
                current = State.Next;
                break;
        }
        await UniTask.Yield();
    }
}

AsyncReactiveProperty

  • WaitAsyncだとタイミングで取りこぼして停止してしまう
private async UniTask MainLoop()
{
    while(current.Value != State.End)
    {
        // 取りこぼして停止する可能性がある
        var next = await current.WaitAsync();
        switch(next)
        {
            // 各ステートの処理
            case State.Start:
                await Hoge();
                current = State.Next;
                break;
        }
    }
}
  • UniTask.LinqQueueを挟んでメソッドチェーンであれば、例外の伝搬もされる
private async UniTask MainLoop()
{
    await current
        .WithoutCurrent() // 初期値無視
        .Queue() // 取りこぼし防止
        .ForEachAwaitAsync(async x =>
        {
            switch(x)
            {
                // 各ステートの処理
                throw new Exception();
            }
        });
}

UnhandledExceptionっぽいやつ

  • 動かなかったり扱いが難しそうだったりで使えないかも
AppDomain.CurrentDomain.UnhandledException +=
    (_, args) => Debug.LogError("AppDomain.CurrentDomain.UnhandledException " + args.ExceptionObject);

UniTaskScheduler.UnobservedTaskException +=
    exception => Debug.LogError("UniTaskScheduler.UnobservedTaskException " + exception);

Application.logMessageReceivedThreaded +=
    (condition, trace, type) => Debug.Log($"Application.logMessageReceivedThreaded {type}: {condition} ({trace})");

参考

Unity - UnityWebRequestをToUniTaskしてキャンセルするとAbortが呼ばれるようになってる

メモ書き

2018年の記事。Abortが呼ばれなかった様子。

[UniRx.Async] UnityWebRequestAsyncOperationConfiguredAwaiter周辺で困った話 #UniTask - Qiita

その話のIssue。

UnityWebRequest cancellation is not call UnityWebRequest.Abort · Issue #361 · neuecc/UniRx

Cycharpに移管されてAbortが呼ばれるよう更新されてる。

UniTask/src/UniTask/Assets/Plugins/UniTask/Runtime/UnityAsyncExtensions.cs at master · Cysharp/UniTask

ことの発端はサーバーで"context canceled"が発生する話。 API呼び出しててキャンセルすることはあまり意識しないけど、恐らくCancellationTokenが発火して起きてるんじゃなかろうか。(未検証)

WebサーバーとブラウザのTCP接続が切れると"context canceled"になる

Rider - Tips

目次

Tips

どこでも検索 (Search Everywhere)

ショートカットキーは、 Shift 2回 もしくは Ctrl + ,

使い方は以下を参考に。

スコープとファイルカラー

検索ダイアログ一般

Ctrl + Shift + F (複数ファイル内検索)や Shift 2回 (どこでも検索)で表示される検索ダイアログについて。

主な検索範囲は以下のような感じ。

  • ソリューション
    • ソリューションに含まれるファイル。「非ソリューション項目を含める」にチェックを入れるとLibraryやobjなども含まれる。
  • プロジェクト
    • 特定のプロジェクト配下。
  • ディレクト
  • スコープ
    • すべての場所
      • 上記ソリューションの「非ソリューション項目を含める」にチェックしたときと同じ。
    • ソリューション
      • 上記ソリューションの「非ソリューション項目を含める」にチェックしなかったときと同じ。
    • プロジェクトとライブラリ
      • Libraryやobjの配下が含まれる。すべての場所と同じかも?
    • 最近表示したファイル
    • 最近変更したファイル
    • 開いているファイル
    • その他
      • 独自に定義したスコープ内。

注意が必要なのは、ヘッダーバーに「◯◯以上」一致と出力されている場合、検索結果がすべて表示しきれていない。さらに、この画面では表示される結果や並び順も変動する。 「◯◯以上」と言われず件数が断言されている場合はすべて表示されているものと思われる。

「◯◯以上」一致の場合は、ダイアログ右下の「検索ウィンドウで開く」をクリックすると、検索ウィンドウに完全な検索結果が表示される。

検索対象に含まれるのはインデックスが作成されているものとなる。 「インデックスなし」となっている場合、いくら検索しても結果に含まれないので注意すること。

インデックスの作成や停止のコマンドは、エクスプローラーでプロジェクトやフォルダ、ファイルを右クリックするとツール内に表示される。

このコマンドは注意が必要で、プロジェクトやフォルダ、ファイルごとに設定を保持しているせいか、コマンドを実行しても上手く切り替わらないことが多い。

「1回目のコマンドでフォルダ配下の各要素が持ってるフラグを消して、2回目で作成を開始する」みたいな?挙動をしている。停止する時は3回位実行しないと反映されない。

個別に設定してると理由(わけ)がわからなくなって混乱するのでかなり危険である。 プロジェクトを指定した設定はいじらず、ルートフォルダに対してだけ実行するのが無難な気がする。

スコープ(Scope)

外部アセットや、サブプロジェクトをUPMで読み込んでいたりすると、検索する際にターゲットを絞れず余計な結果がノイズにのることがある。そんな時に便利なのがスコープの定義。

検索ダイアログの一番右にあるタブが「スコープ」。

「すべての場所」のプルダウンを開くと定義したスコープが選べる。デフォルトでは未定義なので選択するものがない。 隣りにある「…」もしくは設定から、外観&振る舞い > スコープ でスコープの設定画面を開ける。

「+」ボタンから定義を追加する。

インデックスを追加していなければデフォルトだとUnityプロジェクトのフォルダ配下が表示されている。

SamplePackageのRuntimeフォルダのインデックスを追加すると次のような表示になる。

スコープに含めるディレクトリを選択して、「再帰的に含める」を押せば「パターン」に設定される。 ここでも注意が必要で、インデックスを追加した場合のトップのディレクトリはパターンで表現できない。同じ階層に同じディレクトリ名(例えばAssets)がいると出し別けれない。

以下では、Runtime を選択して「再帰的に含める」を押したのに、パターンが file:*/ になって SentryTest2 まで含まれている。

登録しておくと便利そうなスコープは以下の通り。

  • Current
    • 現在Unityプロジェクト
    • file:Assets//*
  • SubProject
    • インデックスで追加したサブプロジェクト(ディレクトリ)のみを含む
  • AllProjects
    • 上記のCurrentとSubProjectをすべて含む

これで検索する時にスコープのプルダウンで選べるようになる。

ファイルカラー

設定で、外観&振る舞い > ファイルカラー からスコープに色を設定できる。

追加ボタンを押すと、次のようなスコープに対して設定できる。色が選べるように見えるが、どれを選択してもパレットが出てきてゼロから色を選択することになるのでバグってそう。

「エディタータブで使用する」をチェックすると開いているタブに対して、「プロジェクトビューで使用する」をチェックするとエクスプローラーや検索ダイアログなどに対して、色が表示されるようになる。 検索ダイアログだけ設定することができない、検索結果ウィンドウには反映されない、など、かゆい所がかゆい使い勝手かもしれない。。

使い所

検索ダイアログの表示に関しては以下の設定ができる。デフォルトだと、最近使用したファイルと機械学習によって出力が変化する。

また検索ダイアログでは、結果の一覧にディレクトリやプロジェクトなどが表示されないし、フィルタもできないので結構不便である。

そこで「プログラムを調査する」など、検索を長時間使い続ける場面では、いちいち設定を弄る必要はあるもののスコープと色付けが有効である。 以下のようにスコープ毎に色が分かれるので、ぱっとみてどこのスコープのファイルか一目瞭然になる。

参考

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;
});

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

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

    // こちらは機能する
    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.SetTagScope.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);
}