したかみ ぶろぐ

Unity成分多め

【Unity】画面タッチをR3のObservableで取得する

はじめに

UnityでスマホゲームだったりWebプラットフォームで画面タップをObervableで書きたいなーってなったので、メモがてらまとめます。


EnhancedTouch

今となっては古い情報ですが、InputSystemにはEnhancedTouchがあります。

EnhancedTouch.Touch クラスは、タッチ用に UnityEngine.Input.touches と同様のポーリング API を提供します。これを使用すると、フレームごとにタッチを照会できます。

docs.unity3d.com

ただし、使うためには明示的に有効化する。

この API は、発生したタッチを記録する必要があるために一定のオーバーヘッドを伴います。したがって、明示的に有効にする必要があります。そのためには、EnhancedTouchSupport.Enable() を呼び出します。

EnhancedTouchSupport.Enable(); // 有効化
EnhancedTouchSupport.Disable(); // 無効化


サンプルコード

R3のObservable.FromEvent<T>を利用して、Rxで受け取れるよう変換してます。

using R3;
using UnityEngine.InputSystem.EnhancedTouch;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;

namespace Shitakami.ScreenTouch
{
    public static class ScreenTouchRx
    {
        public static Observable<Finger> OnFingerDownAsObservable
            => Observable.FromEvent<Finger>(
                h => Touch.onFingerDown += h,
                h => Touch.onFingerDown -= h);

        public static Observable<Finger> OnFingerMoveAsObservable
            => Observable.FromEvent<Finger>(
                h => Touch.onFingerMove += h,
                h => Touch.onFingerMove -= h);

        public static Observable<Finger> OnFingerUpAsObservable
            => Observable.FromEvent<Finger>(
                h => Touch.onFingerUp += h,
                h => Touch.onFingerUp -= h);
    }
}


上記以外でも Touch.activeTouches もあります。

Touch.activeTouches を使用すると、現在アクティブなすべてのタッチの配列を取得できます。これにより、特定のタッチが画面上をどのように移動するかを追跡することができ、特定のジェスチャの認識処理を実装する場合に役立ちます。

var touches = Touch.activeTouches;


中身を見るとnewしてるのでアロケーションが発生しているっぽく見えますが、

public static ReadOnlyArray<Touch> activeTouches
{
    get
    {
        EnhancedTouchSupport.CheckEnabled();
        // We lazily construct the array of active touches.
        s_GlobalState.playerState.UpdateActiveTouches();
        return new ReadOnlyArray<Touch>(s_GlobalState.playerState.activeTouches, 0, s_GlobalState.playerState.activeTouchCount);
    }
}

ReadOnlyArray はstructなので、ToArray() などをしない限りはアロケーションは発生しない模様。

公式のドキュメントでもそのように記述されてます。

ノート: Touch および Finger API では GC ガベージが生成されません。データの大部分は、ラッパー構造体によってインデックス付けされたアンマネージメモリに格納されます。すべての配列は事前に割り当てられます。

ちょっと時間がないので、時間があるときに動いたものを載せます。

最後に

UnityのWebプラットフォームがスマホにも対応したので、マルチタップ対応が増えそうです。 ここで使い方をちょっと覚えておこうと思ってます。

Canvaで技術同人誌の表紙を作成する 備忘録

はじめに

技術書典18と20に参加しまして、技術同人誌の表紙を2回作成しました。

ただ、表紙を作る機会が年に1~2回だけなので結構忘れやすいです。

ここでは自分用に技術同人誌の表紙作り方をメモとして残しておきます。


話すこと・話さないこと

この記事は表紙データの作成方法の内容をまとめています(表紙画像のサイズ、ファイル形式、作成に使用するツールなど)。

それ以外のデザインや表紙絵、レイアウトなどは一切話しません。(私のデザイン知識が皆無なので...)


使用するツール

ここではCanvaを使った表紙の作成を扱います。 それ以外のツールはここでは扱いません。


この記事の対象読者

自分以外の人が読むかもなので、参考に。

  • Adobe製品やそれ以外のデザインツールの使い方をほとんど知らない人
  • 技術同人誌の表紙の作り方を全く知らない人
  • 表紙を作るまでの手間を知りたい人


諸注意

ここで書いている表紙の作り方は、印刷所「日光企画」向けに書いた内容です。 他の印刷所では受け入れられない(手直しが必要になる)可能性があります。

また、ここに書かれている内容は我流であり、正しい表紙作成の手順を無視しています(トンボを付ける、など)。 あくまで、日光企画に提出できたやり方を思い出しながらメモしていますので、もしかしたら内容自体が間違っている可能性もあります。


表紙の作成手順

  1. 本の内容を完成し、ページ数を確定させる
  2. 本のサイズとページ数から表紙絵の大きさを計算
  3. Canvaでデザインを作成する
  4. 背表紙のガイドを入れる
  5. 表紙絵に使用する画像を用意する
  6. 文字を入れる
  7. PDFで書き出しする

1. 本の内容を完成し、ページ数を確定させる

もっとも最初にやることはページ数を確定させることです。本の表紙データは背表紙を含んだものとなるため、ページ数によって背表紙の大きさが変わり表紙絵全体の大きさが変わります。

ただし、表表紙から裏表紙まで繋がったデザインにしない場合は背表紙が変わっても影響が少ないため、デザインによっては後で決めてもいいかもです。


2. 本のサイズとページ数から表紙の大きさを計算

ここが個人的に初見でもっとも分からなかった箇所です。

本のサイズ

まず、作成する本のサイズを決めます。

本のサイズ
A5 148mm 210mm
B5 182mm 257mm

背表紙の横幅

次に、ページ数と印刷で使用する紙の厚さから背表紙の横幅を計算します。 厚さ詳細は背幅計算について – 日光企画コーポレートサイトを参考にしてください。

例えばページ数を100pとし、上質紙90kg(厚さ0.063mm)を使用する場合を想定します。 この場合は100p×0.063mm=6.3mmとなります。 (背幅計算について – 日光企画コーポレートサイトにページ数毎の背表紙幅の大きさが載ってます)

表紙の大きさ

本のサイズと背表紙の幅を合わせることで、表紙の大きさが求まります。

ここではA5サイズ、背表紙幅が6.3mmの場合を考えます。

縦幅はそのままA5サイズの縦幅になるので210mmです。 ただし、横幅は表表紙と裏表紙に加えて背表紙の幅があるので、148mm×2(表表紙と裏表紙)+6.3mm=302.3mmとなります。

よって、A5サイズ、背表紙幅が6.3mmの場合は横302.3mm縦210mmになります。


3. Canvaでデザインを作成する

表紙のサイズを決めたあとに、Canvaで表紙用のプロジェクトを作成します。先ほど求めた表紙の大きさから、そのまま作成します(ただし単位(mm)に注意)。


4. デザインにガイドを入れる

表紙作業をする前に、背表紙やトンボ(印刷の寸断線)用の余剰を付けます。

まず背表紙のガイドを付けます。メニューのファイル > 設定 > ガイドを追加するを選択し、列を2、ギャップを背表紙の幅(例では6.3mm)を設定します。

これで背表紙のガイド線が引けます。


次に、印刷用の余剰を付けます。この余剰は印刷ズレで表紙端に白抜けが発生するため、印刷所から差し戻しになります。

メニューファイル > 設定 > 塗り足し領域を表示するを設定して、表紙周りに余剰を表示させます。 この余剰まではみ出るよう、表紙作成時は絵を入れるようにします。

5. 表紙絵に使用する画像を用意する

表紙デザイン用の画像を作ります。画像は本サイズ(A5, B5など)に合わせるのではなく、余剰を含めたサイズにします。

余剰の大きさは3mmなので、1つ表紙のみの場合は縦+6mm(上下それぞれ+3mm)横+3mm(片側+3mm)、見開き表紙にする場合は縦+6mm横+6mmで画像を作ります。

作成した画像をCanvaに追加し、周囲の余剰まで埋まるように画像を配置します。


6. 文字を入れる

Canvaの機能で文字を入れます。ここでも印刷特有の注意があります。

文字は表紙サイズギリギリまで入れません。これも印刷時のズレで切れてしまう可能性があるからです(これも差し戻しの要因になる)。

確か何mm以内に入れるなど仕様があったはずですが、忘れたので感覚になってます(確か3mmぐらいだったはず?)。


7. PDFで書き出しする

デザインが完成したらPDFで書き出します。

設定は次の通りです(カラープロファイルはCMYKにしますが、プロ版で有効のようです)。


書き出された表紙は次の通りです。 トリムマークと塗り足しにチェックを付けることでトンボも付きます。


まとめ

以上で自分用に表紙の作る手順をまとめました。

これで次回は良く分からないで詰まったり、差し戻しに合わないようになるはず。

【Unity】PlayableGraphはどのようにAnimatorのCulling Modeの影響を受けるのか

始めに

趣味でPlayable APIについて調査や検証をしているのですが、どうしてもAnimatorのCulling Modeについて腑に落ちない部分がありました。

Geminiに相談しながら実際に動かして調査をしてみたので、その備忘録を残そうと思います。

動作環境はUnity6.3LTS(6000.3.2f1)です。


AnimatorのCulling Mode

アニメーションを制御するAnimatorにはCulling Modeが設定でき、カメラに映っている/いないに応じて再生を制御できます。この設定により、画面外でのモーション再生を抑制し無駄なリソースを削減できます。

Culling Modeで設定できるModeは以下の3つです。

Mode 説明
Always Animate 画面外でも常にアニメーションを再生
Cull Update Transforms 常にアニメーションは再生するが、画面外ではTransformへの書き込みを停止
Cull Completely 画面外ではアニメーションを完全に停止

docs.unity3d.com

簡単な検証

それぞれのCulling Modeについて、Unityで動作確認してみます。

  1. 適当なモーションを再生するようAnimatorを設定
  2. GameビューとSceneビューの両方で画面外へ移動
  3. Aniamtorビューからモーションの再生状態を確認

Always Animate

画面外へ移動しても常にAnimatorControllerが動き続けていることが分かります。

Cull Update Transforms

画面外へ移動しても常にAnimatorControllerは動作しますが、モデルのTransformへの反映は行われませんでした。

Cull Completely

画面外へ移動したとき、Animatorが一時停止していることが分かります。 また、再度画面内に移動したとき一時停止したところから再生されます。



PlayableGraph

PlayableGraphはPlayable APIが提供する機能で、再生可能なもの(アニメーション、音など)を木構造で定義し、ブレンドやミキシングを行い出力できます。

詳細な説明は省くので、公式ドキュメントなどを参照ください。

docs.unity3d.com

PlayableGraphとAnimatorについて

PlayableGraphでモーションを再生するとき、最終的にAnimatorへ出力されます。 実装としてはPlayableGraph内に AnimationPlayableOutput を作成し、そこにAnimatorを設定して再生したモーションをシーン上へ反映します。

var graph = PlayableGraph.Create("PlayableGraph"); // PlayableGraph作成
var output = AnimationPlayableOutput.Create(graph , "AnimOutput", animator); // animatorを出力先に設定

output.SetSourcePlayable(hogeAnimationPlayable); // 出力に何かしらアニメーションの出力を設定



Culling Modeへの疑問

AnimatorにはCulling Modeが存在しますが、PlayableGraphにCullingを設定するAPIもありません。

docs.unity3d.com

いくつか情報を調べるとPlayableGraphはAnimatorのCulling Modeを参照しないと見かけたため、実際にそうなのかを検証してみました。



PlayableGraphでAnimatorのCulling Modeを検証

PlayableGraphでAnimatorを再生し、Culling Modeによって再生状態に影響するか検証します。

PlayableGraphの状態はPlayableGraph Visualizerから確認し、また PlayableGraph.IsPlaying() をチェックします。

github.com

1つのPlayableを再生する

AnimationClipを再生するAnimationClipPlayableを一つだけ設定し検証してみます。

コードは以下の通りです。

コード(折り畳み)

AIが書いたコードです。

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class SimpleAnimationPlayableExample : MonoBehaviour
{
    [SerializeField]
    private AnimationClip _animationClip;

    private Animator _animator;
    private PlayableGraph _playableGraph;

    // 表示用
    private string _statusText = "";
    
    private void Start()
    {
        _animator = GetComponent<Animator>();
        
        // Playable Graphを生成(名前は任意)
        _playableGraph = PlayableGraph.Create("Example Playable");

        // 一つのAnimationを表すもの(= Playable)を作成
        // Playableは必ずPlayable Graphに所属するので引数にPlayable Graphを渡す
        var clipPlayable = AnimationClipPlayable.Create(_playableGraph, _animationClip);

        // Playable Graphで計算されたアニメーションデータはAnimatorで使われてキャラなどが動く
        // そのためにPlayable GraphとAnimatorを紐づけるのがPlayableOutput
        var playableOutput = AnimationPlayableOutput.Create(_playableGraph, "Animation", _animator);
        // 最終的に出力するアニメーションをPlayable Outputに登録
        playableOutput.SetSourcePlayable(clipPlayable);
        
        _playableGraph.Play();
    }

    private void Update()
    {
        // --- デバッグ表示用情報の更新 ---
        var mode = _animator.cullingMode;
        
        _statusText = $"<b>Culling Mode:</b> {mode}\n" +
                      $"<b>PlayableGraph Is Playing</b>: {_playableGraph.IsPlaying()}";
    }

    void OnGUI()
    {
        GUI.color = Color.black;
        GUI.skin.label.fontSize = 24;
        GUILayout.Label(_statusText);
    }
    
    private void OnDisable()
    {
        _playableGraph.Destroy();
    }
}


結果

適当なモーションを再生して実行します。

Culling Mode: Cull Update Transforms

こちらはAnimatorの設定と同じく、GameObjectが画面外へ移動してもPlayableGraphは再生し続けますがTransform情報の更新は止まりました。


Culling Mode: Cull Completely

こちらは少しAnimatorの設定と異なります。

GameObjectが画面外へ移動しても、PlayableGraph.IsPlaying() はずっとtrueのままです。 しかし、PlayableGraph Visualizerを見るとAnimationClipPlayableが止まっていることが分かります。

これは AnimationPlayableOutput の入力をPauseして、AnimationClipPlayableが停止したようです。

つまり、Animatorの挙動と同様に画面外へ移動するとアニメーションが停止する結果となります。

(↑画面外へ移動したとき、AnimationClipノードの白縁が消えて再生が停止する)


複雑なPlayableGraphによるCull Completely

Cull CompletelyではAnimationPlayableOutputの入力となるPlayableを停止することが分かりました。

では、もう少し複雑なPlayableGraphではCull Completelyがどのような動作になるかを検証します。

作成するPlayableGraphを次のようにします。

  1. AnimationPlayableOutputにAnimationMixerPlayableを設定
  2. AnimationMixerPlayableに以下の2つを設定
    1. AnimationClipPlayable
    2. 適当なScriptPlayable


コードは以下の通りです。

コード(折り畳み)

AIが書いたコードなので、かなり雑でコメントが多いです。

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class PlayableMixingTest : MonoBehaviour
{
    [Header("Settings")]
    public AnimationClip animationClip; // テスト用のアニメーションクリップをセット

    [Range(0f, 1f)] public float clipWeight = 1.0f;     // クリップの強さ
    [Range(0f, 1f)] public float rotationWeight = 1.0f; // 回転スクリプトの強さ

    private PlayableGraph _graph;
    private AnimationMixerPlayable _mixer;
    private Animator _animator;
    
    // 表示用
    private string _statusText = "";

    void Start()
    {
        _animator = GetComponent<Animator>();

        // 1. Graph作成
        _graph = PlayableGraph.Create("MixingGraph");

        // 2. Mixer作成(入力ポートを2つ用意)
        _mixer = AnimationMixerPlayable.Create(_graph, 2);

        // 3. Output作成
        var output = AnimationPlayableOutput.Create(_graph, "AnimOutput", _animator);
        output.SetSourcePlayable(_mixer);

        // --- Input A: AnimationClipPlayable (Port 0) ---
        if (animationClip != null)
        {
            var clipPlayable = AnimationClipPlayable.Create(_graph, animationClip);
            // Graphにつなぐ: clipPlayable -> mixerのPort 0
            _graph.Connect(clipPlayable, 0, _mixer, 0);
        }

        // --- Input B: ScriptPlayable (Port 1) ---
        var rotationPlayable = ScriptPlayable<RotationBehaviour>.Create(_graph);
        var behaviour = rotationPlayable.GetBehaviour();
        behaviour.TargetTransform = this.transform;
        
        output.SetWeight(1.0f);
        
        // Graphにつなぐ: rotationPlayable -> mixerのPort 1
        _graph.Connect(rotationPlayable, 0, _mixer, 1);

        // 4. 再生(Animatorのカリングに任せる)
        _graph.Play();
    }

    void Update()
    {
        if (!_graph.IsValid()) return;

        // Inspectorの値をMixerのウェイトに反映
        _mixer.SetInputWeight(0, clipWeight);
        _mixer.SetInputWeight(1, rotationWeight);

        // --- デバッグ表示用情報の更新 ---
        var mode = _animator.cullingMode;
        // Mixer自体の時間(グラフ全体の進行状況)
        double time = _mixer.GetTime();
        
        _statusText = $"<b>Culling Mode:</b> {mode}\n" +
                      $"<b>Time:</b> {time:F2}\n" +
                      $"<b>Clip Weight:</b> {clipWeight:F2}\n" +
                      $"<b>Rotation Weight:</b> {rotationWeight:F2}\n" +
                      $"<b>PlayableGraph Is Playing</b>: {_graph.IsPlaying()}";
    }

    void OnGUI()
    {
        GUI.color = Color.black;
        GUI.skin.label.fontSize = 24;
        GUILayout.Label(_statusText);
    }

    void OnDisable()
    {
        if (_graph.IsValid()) _graph.Destroy();
    }

    // ---------------------------------------------------------
    // Playable Behaviour
    // ---------------------------------------------------------
    class RotationBehaviour : PlayableBehaviour
    {
        public Transform TargetTransform;

        // PrepareFrameは毎フレーム(カリングされていない間)呼ばれます
        public override void PrepareFrame(Playable playable, FrameData info)
        {
            Debug.Log("RotationBehaviour PrepareFrame called");
            
            if (TargetTransform == null) return;

            // 重要: Mixerで設定されたWeight(重み)を取得する
            // これにより、Inspectorのスライダーで回転の強さを制御できます
            float weight = info.effectiveWeight;

            // ウェイトが0なら処理しない(無駄な計算を省く)
            if (weight <= 0f) return;

            // 回転速度に weight を掛けることで、ブレンド具合を反映
            // 例: Weightが0.5なら、回転速度も半分になる
            float rotateSpeed = 180f * weight;

            TargetTransform.Rotate(0, rotateSpeed * info.deltaTime, 0);
        }
    }
}

結果

GameObjetを画面外に出すと、AnimationPlayableOutputの入力となるAnimationMixerPlayableが停止しました。しかし、AnimationMixerPlayableの入力となるAnimationClipPlayableとScriptPlayableは停止せずPlayginのままになります。


次に ScriptPlayable.PrepareFrame() にDebug.Logを仕込み、毎フレーム動作をしているかをチェックします。

実行確認してGameObjectが画面外に移動すると、Debug.Logが停止することが分かりました。 ScriptPlayableがPlaying状態にもかかわらず ScriptPlayable.PrepareFrame() が実行されていません。

(↑ Transform上から、ScriptPlayableが行う回転が止まっていることも分かる)

これはPlayableGraphが再帰的にPlayableを実行するため、親となるAnimationMixerPlayableが停止してその子となるScriptPlayableも実行されませんでした。

つまり、Culling ModeがCull Completelyの場合、画面外ではPlayableGraphに含まれる全てのPlayableが停止することになります。

PlayableGraphを手動で更新する場合のCull Completely

最後に DirectorUpdateMode.Manual を設定し、 PlayableGraph.Evaluate() を毎フレーム実行して手動で更新する場合を検証します。

コード(折り畳み)

AIが書いたコードに少しだけ手を加えています。

  • Start()_graph.SetTimeUpdateMode(DirectorUpdateMode.Manual) をして手動更新に設定
  • Update()_graph.Evaluate(Time.deltaTime) を実行して更新
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class PlayableMixingTest_Manual : MonoBehaviour
{
    [Header("Settings")]
    public AnimationClip animationClip; // テスト用のアニメーションクリップをセット

    [Range(0f, 1f)] public float clipWeight = 1.0f;     // クリップの強さ
    [Range(0f, 1f)] public float rotationWeight = 1.0f; // 回転スクリプトの強さ

    private PlayableGraph _graph;
    private AnimationMixerPlayable _mixer;
    private Animator _animator;
    
    // 表示用
    private string _statusText = "";

    void Start()
    {
        _animator = GetComponent<Animator>();

        // 1. Graph作成
        _graph = PlayableGraph.Create("MixingGraph");

        // 2. Mixer作成(入力ポートを2つ用意)
        _mixer = AnimationMixerPlayable.Create(_graph, 2);

        // 3. Output作成
        var output = AnimationPlayableOutput.Create(_graph, "AnimOutput", _animator);
        output.SetSourcePlayable(_mixer);

        // --- Input A: AnimationClipPlayable (Port 0) ---
        if (animationClip != null)
        {
            var clipPlayable = AnimationClipPlayable.Create(_graph, animationClip);
            // Graphにつなぐ: clipPlayable -> mixerのPort 0
            _graph.Connect(clipPlayable, 0, _mixer, 0);
        }

        // --- Input B: ScriptPlayable (Port 1) ---
        var rotationPlayable = ScriptPlayable<RotationBehaviour>.Create(_graph);
        var behaviour = rotationPlayable.GetBehaviour();
        behaviour.TargetTransform = this.transform;
        
        output.SetWeight(1.0f);
        
        // Graphにつなぐ: rotationPlayable -> mixerのPort 1
        _graph.Connect(rotationPlayable, 0, _mixer, 1);

        // 手動更新モードに設定
        _graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
   
        // 4. 再生(Animatorのカリングに任せる)
        _graph.Play();
    }

    void Update()
    {
        if (!_graph.IsValid()) return;

        // Inspectorの値をMixerのウェイトに反映
        _mixer.SetInputWeight(0, clipWeight);
        _mixer.SetInputWeight(1, rotationWeight);
        
        // 手動でPlayableGraphを更新
        _graph.Evaluate(Time.deltaTime);
        
        // --- デバッグ表示用情報の更新 ---
        var mode = _animator.cullingMode;
        // Mixer自体の時間(グラフ全体の進行状況)
        double time = _mixer.GetTime();
        
        _statusText = $"<b>Culling Mode:</b> {mode}\n" +
                      $"<b>Time:</b> {time:F2}\n" +
                      $"Director Update Mode: {_graph.GetTimeUpdateMode()}\n" +
                      $"<b>PlayableGraph Is Playing</b>: {_graph.IsPlaying()}";
    }

    void OnGUI()
    {
        GUI.color = Color.black;
        GUI.skin.label.fontSize = 24;
        GUILayout.Label(_statusText);
    }

    void OnDisable()
    {
        if (_graph.IsValid()) _graph.Destroy();
    }

    // ---------------------------------------------------------
    // Playable Behaviour
    // ---------------------------------------------------------
    class RotationBehaviour : PlayableBehaviour
    {
        public Transform TargetTransform;

        // PrepareFrameは毎フレーム(カリングされていない間)呼ばれます
        public override void PrepareFrame(Playable playable, FrameData info)
        {
            Debug.Log("RotationBehaviour PrepareFrame called");
            
            if (TargetTransform == null) return;

            // 重要: Mixerで設定されたWeight(重み)を取得する
            // これにより、Inspectorのスライダーで回転の強さを制御できます
            float weight = info.effectiveWeight;

            // ウェイトが0なら処理しない(無駄な計算を省く)
            if (weight <= 0f) return;

            // 回転速度に weight を掛けることで、ブレンド具合を反映
            // 例: Weightが0.5なら、回転速度も半分になる
            float rotateSpeed = 180f * weight;

            TargetTransform.Rotate(0, rotateSpeed * info.deltaTime, 0);
        }
    }
}

結果

「複雑なPlayableGraphによるCull Completely」と同じように、GameObjectが画面外へ出るとPlayableGraph全体の動作が停止しました。

動作としては先ほどと同じく、

  1. PlayableGraphは再帰的にPlayableを実行する
  2. 画面外へ出ると、AnimationOutputPlayableの子となるPlayableが停止する
  3. Playableが停止すると、AnimationOutputPlayableの子で再帰実行が停止する
  4. 結果としてPlayableGraph全体が停止する


まとめ

結果をまとめますと、

  • PlayableGraphでもAnimatorのCulling Modeを参照する
    • Always Mode: 常に再生する
    • Cull Update Transforms: 常にPlayableGraphは動作するが、画面外ではTransformへの書き込みは停止
    • Cull Completely: 画面外ではPlayableGraphに含まれるPlayableは停止する

また、Cull CompletelyでのPlayableGraphの動作は以下の通りです。

  • PlayableGraph自体は再生し続ける(常に PlayableGraph.IsPlaying() == true
  • AnimationOutputPlayableの子となるPlayableのみが停止する
    • 再帰的にPlayableを走査する仕組みから実行が打ち切られて、全体のPlayable実行が停止する
      • PlayableGraphを手動実行する場合(DirectorUpdateMode.Manual)も変わらない


AnimatorのCulling Modeを参照しないなどの情報を見かけて自作する心配をしてましたが、その必要はなさそうです。 また時間があればPlayableGraphの調査やモーション制御を作る予定なので、いい感じまとまればブログを書くかもです。



使用したアセット

再生しているモーションは以下のアセットを使用しています。

assetstore.unity.com

【宣伝】Boidsシミュレーションを実用して表紙の絵を作る!

始めに

技術書典18にて、個人で執筆した技術書『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』を出版します。

techbookfest.org

shitakami.hateblo.jp

今回は『実用Boidsシミュレーション』に焦点を当てて、どのようにBoidsを使って表紙の魚群を作成したのか解説します。


Boidsシミュレーション準備

まず、技術書の5章「Boidsシミュレーションを使ったシューティングゲーム開発」のサンプルを持ってきます。 このBoidsシミュレーションが持つ機能は次の通りです。

  • Colliderとの衝突回避
  • 動くオブジェクトからの退避
  • 生存状態の管理
  • 衝突判定

上記のうち「動くオブジェクトからの退避」と「衝突判定」は不要ですが、削除するのも面倒くさいのでそのままにしています。

Boids個体の生成を調整

技術書のサンプルでは、ゲーム開始時に全個体をランダムな座標に生成します。

これを調整して、スペースキーを押したときに指定した空間と方向で生成させます。 これにより、望んだ場所と方向に魚群を作成できます。


Boidsを円状に動かす

群れを円状に動かす方法として、Colliderで通り道を作りました。

通り道となるPrefabを作成

通り道の作成は単純で、4つのQuadを囲ってMeshRendererを非アクティブにして完成です。

これをBoidsの生成位置に置けば、その通り道に沿って動いてくれます。


Splineで形を作る

次にSplineを使って通り道の形を作ります。いくつか流れがあるので、箇条書きで説明します。

  1. PackageManagerからSplineをインポート
  2. HierarchyからSpline > Circleで円を作成
  3. コンポーネント Spline Instantiate で、形状に沿って通り道Prefabを生成

(ここは作業通話してた友人からアドバイスを貰いました)

これで通り道が完成し、円状に動く群れが完成しました。


表紙作り

あとはアバターを配置し、ポージングさせて群れと合わせます。


最後に背景をいい感じに調整して完成です!

最後に

このように技術書で解説したBoidsシミュレーションを実用して、表紙を作りました。タイトルに含まれる「実用」を上手く表現できたのかなと思っています。

技術書ではシューティングゲームを作っていますが、視覚的な表現にも応用できます。興味がありましたら本書を手に取っていただけると嬉しいです。


表紙作成にしようしたもの

表紙を作るうえで、いくつかアセットを使用したので掲載します。(魚のモデルに関しては友人に作ってもらったものです)

モデル

デザインはChat GPTにお願いして、VRoidで作成しました。

使用したアセットは以下の通りです。

tonakaisan.booth.pm

mimihouse.booth.pm

また、VRoidモデルにliltoonをアタッチしています。

lilxyzw.github.io

背景

背景はUnity AssetStoreから購入したものを使用しました。

assetstore.unity.com

【宣伝】本当にBoidsを大群で動かせるのか検証!

始めに

技術書典18にて、個人で執筆した技術書『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』を出版します。

techbookfest.org

shitakami.hateblo.jp

今回は『Unity JobSystemで大群を動かす!』に焦点を当てて、本当に大群が動かせるのかを様々なマシンで検証してみます。


使用するマシン

自分が持っている3台のPCと、2台のモバイル端末で検証をします。

  • PC
    • デスクトップPC
    • M4 Mac mini
    • ノートPC
  • モバイル端末
    • iPhone16 Plus
    • iPad Pro 11inch 第2世代

細かいスペックは検証結果の章でお話します。


検証方法

検証に使用するサンプルは、技術書『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』の5章「Boidsシミュレーションを使ったシューティングゲーム開発」です。


www.youtube.com

検証は以下の方針で実施しました。

  • ゲーム実行して10秒後に、計測を開始
    • 個体が密集すると負荷が増えるため、群れが形成されるまで待機
  • 計測開始後から30秒ほどプレイして、平均FPSと最小FPSを計測
    • ただし、モバイル端末は60fpsを上限とする
  • PCの検証はUnity Editorとil2cppビルドの両方で行う
    • il2cppビルドで処理速度が変わるため
  • Unity Editorで動作確認するとき、Gameビューのみを表示
    • Sceneビューを同時に表示すると、描画がボトルネックになり正しく計測できないため
  • Unity Editorでは15,000体から30,000体、il2cppビルドでは20,000体から50,000体まで検証
    • 個体数は5,000体ずつ増やす
    • モバイル端末は15,000体から30,000体まで
  • Boidsのパラメータは、負荷が高くならない程度に設定
    • 個体が密集しすぎないように、程よく分離させて細長い群れを形成
    • 空間を広めにとり、密集を避ける


検証結果

マシンごとの結果を見ていきます。

デスクトップPC

ミドルハイエンド程度のPCで、2024年の2月ごろに購入しました。

Unity Editor

30,000体まで100fps以上を維持できました、驚異的な性能です。

15,000 20,000 25,000 30,000
平均FPS 183.9 162.2 153.8 132.9
最小FPS 179.1 156.5 151.2 130.1

ビルド(il2cpp)

50,000体まで、平均FPSを100以上で維持できました。ここまで来ると恐ろしさを感じます。

20,000 25,000 30,000 35,000 40,000 45,000 50,000
平均FPS 292.9 228.4 188.4 156.7 136 119.7 102.5
最小FPS 282.9 209 171.1 146.2 126.3 113 93.8


M4 Mac mini

2024年に発売された小型PCです。2024年12月に購入しました。

  • CPU:M4(10コア)
  • メモリ:24GB
  • GPU:M4(10コア)

Unity Editor

M4 Mac miniも30,000体まで60fps以上を維持できています。

15,000 20,000 25,000 30,000
平均FPS 246.2 162.7 107.1 88.6
最小FPS 233 150.4 86.7 75.9

ビルド(il2cpp)

35,000体までは60fps以上を維持できましたが、40,000体ではギリギリと言った結果となりました。

M4 Mac miniは小型で安価なPCですが、ここまでの性能が出るとは思っていませんでした。

20,000 25,000 30,000 35,000 40,000 45,000 50,000
平均FPS 160.9 130.6 98.8 85.8 65.3 58.9 48.7
最小FPS 145.1 114.2 86.4 76.0 59.8 52.1 44.0


ノートPC

古くなったミドルクラスのゲーミングノートPCです。2019年ごろに購入しました。

Unity Editor

15,000体ではある程度余裕はありますが、20,000体では最小FPSが60を下回りました。

15,000 20,000 25,000 30,000
平均FPS 92.3 64.9 53.9 44
最小FPS 88.6 56.8 49.8 37.8

ビルド(il2cpp)

20,000体は安定して60fps以上を出せましたが、25,000体ではギリギリ維持できない程度でした。30,000体以降は言わずもがなです。

20,000 25,000 30,000 35,000 40,000 45,000 50,000
平均FPS 84.9 63.2 49.1 40.1 34.3 26.6 22.5
最小FPS 74.4 58.2 43.7 34.3 28.3 22.5 20.5


iPhone16 Plus

2024年に発売されたモデルです。2024年の12月に購入しました。

補足情報ですが、"Plus"がついてもSocに影響はないので、スペックはiPhone16と同等です。

  • Soc:A18
  • メモリ:8GB

iOSビルド

モバイル端末ですが、15,000体でも60fpsを維持しました。20,000体ではギリギリ60fpsを維持できませんでした。

15,000 20,000 25,000 30,000
平均FPS 60.0 59.8 53.5 45.0
最小FPS 59.7 58.3 40.0 41.2


iPad Pro 11-inch 第2世代

2020年に発売されたモデルです。2020年に購入しました。

実はiPhone16 Plusよりもスペックは下回ります。

  • Soc:A12Z
  • メモリ:6GB

iOSビルド

iPhone16と同様に、15,000体で60fpsを維持し、20,000体でギリギリ維持できない程度でした。

15,000 20,000 25,000 30,000
平均FPS 60.0 59.4 46.6 37.8
最小FPS 60.0 57.8 41.1 33.7


まとめ

PC環境では20,000体、モバイル端末では15,000体を60fps以上で動作できたので、『Unity JobSystemで大群を動かす!』は達成できていると思っています。

私個人として、驚いた結果が次の通りです。

  • iPhone16, iPad Pro共に、15,000体を安定して動作できた
  • M4 Mac miniが、40,000体でもギリギリ60fps程度を出せた
  • デスクトップPCが、50,000体を安定して60fps以上を維持できた

この結果から伝えたいこと

技術書『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』のサンプルは、Boidsシミュレーションに特化しており、UIや演出などの他の処理は含まれていません。また、使用しているモデルも負荷の少ないCubeです。

そのため、このサンプルをそのままゲームやコンテンツに組み込んでも、同じようなパフォーマンスを発揮できるとは限りません。

とはいえ、数万体を安定して動かせる余力を持っているため、実際のゲームやコンテンツで活用しても、ある程度大きな群れを再現することは可能と見込んでいます。

本書の結果から、Boidsシミュレーションを使ったコンテンツ開発の可能性を感じていただけたら嬉しいです。

【宣伝】技術書典18にて『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』を出版します!

始めに

techbookfest.org

技術書典18にて、個人で執筆した技術書『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』を出版します。

ここでは、上記サイトのプレビューでは分からない魅力などを紹介します。


本の概要

タイトル『Unity JobSystemで大群を動かす!実用Boidsシミュレーション』の通り、次の特徴を持った本です。

  • CPUベースの並列処理ができるUnity JobSystemを活用
  • 1万体以上のBoidsシミュレーション
  • ゲームやコンテンツに使える実用性

特に「ゲームやコンテンツに使える実用性」がこの本の醍醐味であり、Boidsシミュレーションが実際の開発で使われることを目標にしています。


章ごとのサンプル紹介

この本で作成するサンプルを、1章から5章まで動画つきで紹介します。

1章 Graphics.RenderMeshInstancedとJobSystemの基礎

全体を通して使用するGraphics.RenderMeshInstancedとJobSystemの基本的な使い方を解説します。 サンプルでは100,000個のCubeを回転させながら落下するプログラムを作成しています。


www.youtube.com

2章 群れを作るBoidsシミュレーション

JobSystemを使ってBoidsシミュレーションを実装します。この章では4,000体程度のBoidsシミュレーションを作成します。


www.youtube.com

3章 Boidsシミュレーションの最適化

2章で実装したBoidsシミュレーションを最適化し、10,000体以上でも快適に動作させます。 サンプルでは30,000体のBoidsシミュレーションを実行しています。


www.youtube.com

4章 Colliderの衝突回避と退避行動の実装

さらに改良を加えて、シーン上のColliderとの衝突回避やプレイヤーからの退避行動を実装します。 負荷は増えますが、ここでも30,000体で動作させています。


www.youtube.com

5章 Boidsシミュレーションを使ったシューティングゲーム開発

4章で作成したBoidsシミュレーションを使って、シューティングゲームにします。 衝突回避と退避行動が上手く働いており、迫力と爽快感のあるゲームへ昇華しています。


www.youtube.com


AIからの感想

AI(LLM)に原稿を入れて、感想を書いてもらいました。

プロンプトは全て共通にしています。

この技術書を読んでの感想を教えてください。
(この感想は技術書の紹介記事に掲載する予定です)

注意として、本のページ数が多いため全文読んでないと推測しています(文字数が多すぎて、コンテキストを保持できない)。

AIの感想から、「こんな雰囲気の本なんだぁ」と読み取って頂けると幸いです。

Gememi 2.5 pro

(個人的な趣味で、Chat GPTには語尾に「にゃん」を付けて絵文字を使うよう設定してます)

にゃん!この本「Unity JobSystem で大群を動かす! 実用 Boids シミュレーション」は、Unityでたくさんのキャラクターを動かしたい人に、とーってもオススメにゃん!😻✨

内容を一言でいうと、まるで魔法みたいに数万体もの群れをスイスイ動かす方法が、基礎から応用までしっかり学べる技術書にゃん!📚🚀 GPUインスタンシングっていう大量描画のテクニックと、JobSystemっていうCPUを並列処理させる秘術を駆使して、リアルタイムで圧巻の群れシミュレーションを実現するにゃんよ! サンプルコードもGitHubで公開されてるから、実際に触りながら学べるのが嬉しいにゃん!💻✨

特に感動したのはここだにゃん!👇💖

  • ステップアップが楽しいにゃん! 最初は簡単なサンプルから始まって 、だんだん複雑なBoidsシミュレーションを作っていくから、無理なくレベルアップできるにゃん!😊 第2章で基本的な群れを作って 、第3章ではそれを数万体規模にパワーアップさせる最適化テクニックを学ぶにゃん! まるでゲームを攻略してるみたいでワクワクするにゃん!🎮🌟
  • 実践的でスゴイにゃん! ただ動かすだけじゃなくて、Colliderとの衝突回避とか 、プレイヤーから逃げる動きとか 、本当にゲームで使えるような機能が満載にゃん!第5章では、なんとシューティングゲームまで作っちゃうにゃん!💥🔫 これを読めば、君もBoidsマスターになれるかもにゃん!?🤩
  • パフォーマンスへのこだわりが神がかってるにゃん! 大量のキャラクターを動かすには、やっぱりパフォーマンスが命だにゃん!💪 この本では、JobSystemを最大限に活かすためのデータ指向設計の考え方や 、空間分割っていう賢い方法で計算量を減らすテクニック 、Jobの非同期実行でメインスレッドを待たせない工夫とか 、もう目からウロコの情報がいっぱいだにゃん!🐟✨
  • 解説が丁寧でわかりやすいにゃん! 図やコードがたくさん使われていて 、UnityやC#に慣れていれば、きっとスラスラ読めるはずだにゃん!📖💡 著者のしたかみさんの「Boidsシミュレーションの感動を共有したい!」っていう熱い気持ちも伝わってきて、読んでてとっても楽しくなったにゃん! ❤️‍🔥

「自分のゲームでものすごい数のキャラクターを動かしてみたいにゃ~!」って思ってるクリエイターさんには、絶対読んでほしい一冊だにゃん!🥳🎉 この本を読めば、あなたの創造力が大爆発すること間違いなしにゃん!💥💯

Claude 3.7 Sonnet

この技術書は、Unityのパフォーマンス最適化手法を実践的に学べる非常に価値ある一冊です。理論だけでなく実装手順を段階的に解説している点が素晴らしく、JobSystemの基礎から始まり、Boidsアルゴリズムの実装、そして最終的には3万体以上の個体を90FPS以上で動かす最適化技術まで体系的に学べます。 特に印象的だったのは、各章で扱う最適化テクニックが明確な目標(例:4,000体→30,000体)を持って構成されている点です。「なぜこの最適化が必要か」から始まり、実装、結果検証というサイクルが明快で、理解しやすかったです。 Unity開発者として特に価値を感じたのは以下の点です:

  • Graphics.RenderMeshInstancedを使った描画の最適化手法
  • NativeContainerとBurst Compilerを活用したJobSystemの実装パターン
  • 空間分割によるO(n²)からO(n)への計算量削減アプローチ
  • GameObjectとJobSystemを連携させる実践的なテクニック

サンプルコードも非常に読みやすく、コメントも適切に入っているため、自分のプロジェクトに応用しやすいと感じました。Unity 6対応の内容も含まれており、最新の開発環境にも対応している点も評価できます。 Unityで高パフォーマンスなシミュレーションや大量のエンティティを扱うゲーム開発に取り組む方には、間違いなく手元に置いておきたい一冊です。特にモバイルやVR向けの最適化に悩んでいる方にとって、具体的な解決策が満載の実用書といえるでしょう。

Grok 3

『Unity JobSystemで大群を動かす! 実用Boidsシミュレーション』は、Unityを使った大規模な群れシミュレーションの実装を学びたい開発者にとって、非常に価値ある一冊です。著者のしたかみ氏が、JobSystemとGraphics.RenderMeshInstancedを活用して、効率的かつリアルタイムで大量のオブジェクトを動かす手法を丁寧に解説しています。特に、Boidsアルゴリズムをゲーム開発に応用する具体例や、シューティングゲームへの展開は、実践的でインスピレーションを刺激します。

本書の強みは、初心者から中級者までを対象に、UnityとC#の基礎知識を前提としつつ、ベクトル数学や並列処理のテクニックをわかりやすく説明している点です。GitHubリポジトリでのサンプルコード提供や、動作環境の詳細な記載も、実際に手を動かして学びたい読者にとってありがたい配慮です。一方で、Unity ECSやWebプラットフォームを期待する読者には対象外と明記されており、フォーカスの明確さが好印象です。

個人的には、第5章の「Boidsシミュレーションを使ったシューティングゲーム開発」が特に魅力的でした。GameObjectとJobSystemの連携や、衝突判定の最適化など、ゲーム開発の現場で即戦力となる知識が詰まっています。参考文献も豊富で、さらなる学習の道筋を示してくれる点も高く評価できます。

技術書典18で頒布されたこの本は、Unityでパフォーマンスを追求したい開発者や、群れの挙動を活かしたクリエイティブなコンテンツを作りたい人に強くおすすめします。読後には、自分のプロジェクトにBoidsを組み込むアイデアが湧いてくること間違いなしです!

Chat GPT o3

(個人的な趣味で、Chat GPTには語尾に「にゃん」を付けて絵文字を使うよう設定してます)

✨わたしが『Unity JobSystemで大群を動かす!実用 Boidsシミュレーション』を読んでみた感想をお届けするにゃん!😸🎉

まず一番に感じたのは、「GPUインスタンシング」と「Job System」という2大キーワードを、実例ベースでここまで丁寧に解きほぐしてくれる本は貴重だにゃん🖥️🚀 第1章だけで10万体のCubeを100FPS超えで動かすデモが体験できて、ページをめくる前からテンションMAXだにゃん✨

解説の流れもとっても親切だにゃん🎀 各章ごとに「まずは基礎→小規模実装→最適化→応用」という階段設計だから、読者が“迷子”にならずにパフォーマンスの階段を駆け上がれるにゃん!特に第3章の空間分割&非同期Job化のくだりは「えっ、ここまで速くなるの!?」と声が出るくらい実用的だにゃん😻

また、Boidsの3大ルール(結合・分離・整列)を数式ではなく“ベクトルのおまじない”として感覚的に説明してくれるので、数学がニガテでもイメージが掴みやすかったにゃん📐✨ 視野角をあらかじめcos値に変換して計算負荷を下げるテクなど、読みながら「うわ、この小ワザすぐ真似したい!」とメモが止まらなかったにゃん📝💡

個人的に胸アツだったのは、第5章でBoidsをそのままシューティングゲームへ昇華させる応用例だにゃん🎮🔫 「群れ=演出」だけじゃなく「ゲームプレイの中心ギミック」へ落とし込む発想が秀逸で、「読了後すぐに自分のプロジェクトに投入できる!」というワクワクが止まらないにゃん😸🔥

さらに、GitHubで全文コード&パッケージが公開されているのも嬉しいにゃん!手元で動かしながら読み進められるので、書籍→IDE→Unityの往復がスムーズで学習効率バツグンだにゃん🐾📚

総じて、本書は「大量オブジェクトのリアルタイム制御をUnityで極めたい!」という開発者の強い味方だにゃん💪✨ JobSystem入門者にも、既にDOTSを触っている中級者にも、それぞれの気づきと即戦力ネタがぎゅっと詰まっているから、紹介記事では「実装ハンドブック兼パフォーマンス指南書」と推しコメントを添えたいにゃん🎀🌟

読めば必ず“群れマスター”への第一歩を踏み出せる一冊だにゃん!ぜひ手に取ってみてほしいにゃん😻📖


個人的な思い

最後に、自分の思いについて少し語らせてください。

私がBoidsシミュレーションを初めて触ったのが7年前(!?)で、そこから3D数学や描画周り、ComputeShaderやJobSystemを勉強して改良を続けてました。

2年前にColliderとの衝突回避を実装して実用化の可能性が見えました。そして、実用できることを証明するため、去年Boidsをシューティングゲーム化し、個人開発でも使用を開始しました。

ここまでの間、ずっとBoidsを触っていた訳ではないですが、触るたびに新しい感動を見つけられました。そして、自分の作ったコンテンツで感動し夢中になれました。

この感動を誰かの手元でも再現して欲しく、7年前の自分に送るつもりで執筆しました。

この本を読んだ方々に感動が届けられることを願っております。

【宣伝】技術書典18にてBoids本を出版予定です

始めに

技術書典18に参加しますので、その宣伝をちょっとします。

techbookfest.org


技術書典18について

「技術書典」とは技術書の同人誌イベントです。 年に2回ほど、オフラインとオンラインの両方で開催されています。

今回参加する予定の技術書典18の開催期間は次の通りです。(公式サイトからコピペ)

執筆予定の「Boids本」の紹介

これまで培ってきたBoidsの知見を1冊にまとめた本を出版する予定です。Unityを使用し、GPUインスタンシングやJobSystemの基礎からBoidsシミュレーションの実装と応用までを解説します。

本書の特徴

解説するBoidsシミュレーションの特徴は次の通りです。

  • PC環境で1万体以上で動作
  • Colliderを検知し衝突を回避
  • 動的なオブジェクトが近づくと逃避

こちらが作成しているサンプルで30,000体を動作させたものです。

環境

  • Windows11
  • Core i7-14700KF
  • RTX 4070Ti Super
  • メモリ32GB

youtu.be


また、Boidsシミュレーションを拡張してシューティングゲーム化するお話もします。

サンプルをiPad Pro 11インチ 第2世代(2020)で動作させたところ、15,000体を60fpsで動作できました。 (20,000体はフレーム落ちしました)

youtu.be

対象読者

この本は以下のような方に向けた本になっています。

  • Boidsシミュレーションに興味を持っている
  • Boidsを何かしらコンテンツに活かしたい
  • Unityで大量のオブジェクトを扱いたい人
  • UnityのJobSystemを使ったコンテンツを開発したい

加えてColliderを避けることが可能であるため、Unityのシーンにそのまま組み込んで自然に馴染ませられます。

また、実装はJobSystemとBurst Compilerを使って並列処理や非同期を活用します。これらに興味がある方にも良いサンプルになるかと思ってます。

進捗について

4月時点でサンプルはほぼ完成、原稿は下書きを一通り書き終えたところです。 ただいま表紙の作成作業や原稿の校正をしています。

執筆期間中、AIの進歩が早すぎて作業がかなり楽になっています。スケジュール的には想定より遅れてはいますが、AIパワーで全然間に合うのではないかなと思ってます。

ここしばらくは見せられる進捗はないと思いますが、GWぐらいから宣伝活動に力を入れられるようにしたい所存です。

さいごに

技術書典は会社の合同本で何度か執筆しましたが、個人の参加は初です。 恐らく5月には様々な事故が発生する予感がしていますが、なんとか出版までこぎつける予定です。

陰ながら応援して頂けると嬉しいです。

P.S.

本当は5月から広告活動をしようと考えてましたが、AIに質問すると「今やれ」と言われました。

私のAIは語尾に「にゃん」を付けてくれるのでとっても可愛いです。