2026年7月9日、「GPT-5.6」が公開されました。

GPT-5.6は

■Sol(ソル)
最上位のモデル。高度なコーディング向け

■Terra(テラ)
GPT-5.5に匹敵する性能を維持しつつ、より低コストで利用できるバランス型モデル

■Luna(ルナ)
簡単な応答や単純処理向け。高速かつ、使用量コストが少ないのが長所。

と、3つのモデルが提供されています。

今回は最上位モデルの「Sol」を使用して、Unityでどの程度の事が出来るのか調査してみました。

生成結果


ただ、調査といっても実際にはチャットで要件を伝えただけで、それっぽいものが動いてしまったので、いきなり成果物をお出しします。

赤いブロックに潰されるとゲームオーバーになり、シーンのリロード&リトライを行います。

プロジェクトにあらかじめVRMモデルや公式のサンプルシーンが用意されている前提ではありますが、
なんと、「VRMモデルのアニメーション」「ゲームオーバー処理」「3Dモデルの消滅処理」までを、AIへの指示だけで実装してくれました!

プロンプトと実行結果は以下のような感じです。

PlayerArmatureにPinchEscapeResolverというクラスが存在しています。

プレイヤーが壁と挟まった時に、強制的な移動先を設定するスクリプトなのですが、
退避先が無い場合は
「退避先なし。クラッシュ判定を発火」と表示させています。

この処理が実行された場合、ゲームオーバー処理を実装したいです。

内容としては、
・プレイヤーの戦闘不能アニメーションの再生
・shaderを調整してdissolveによる消滅演出を実行
・dissolveによる消滅演出後、画面の暗転(黒色フェードアウト)
・一定時間後にシーンのリロード

としたいです。

※PinchEscapeResolveは以前Claude Codeによって作成してもらったスクリプトです。
こんな感じのコードです。

■別件でClaude Codeに作成してもらった、挟まれ検知スクリプト

/// <summary>
/// 壁・床(静的コライダー)と移動体(Rigidbody持ちコライダー)に挟まれた際の解決を行う。
/// 水平(壁との挟み込み)・垂直(落下物との挟み込み)の両方に対応する。
///
/// 1. Update(ThirdPersonControllerより先に実行):
///    移動体への深い重なり+脱出方向が静的コライダーに塞がれている=挟まれ状態を検出し、
///    CharacterController内蔵の自動押し出し(上へのワープ等)が走る前に退避またはクラッシュ判定を行う
/// 2. LateUpdate: 静的コライダーへのめり込みをスイープでクランプ(薄い壁のすり抜け防止)
/// </summary>
[DefaultExecutionOrder(-100)] // ThirdPersonControllerのUpdate(Move)より先に実行する
[RequireComponent(typeof(CharacterController))]
public class PinchEscapeResolver : MonoBehaviour
{
    [Header("めり込み補正")]
    [SerializeField, Tooltip("チェック対象のレイヤー")]
    private LayerMask obstacleLayers = ~0;

    [SerializeField, Tooltip("誤検出防止のためカプセル判定を縮める量 (m)")]
    private float skinTolerance = 0.02f;

    [SerializeField, Tooltip("壁の手前で確保する隙間 (m)")]
    private float wallMargin = 0.02f;

    [Header("挟まれ判定")]
    [SerializeField, Tooltip("移動体にこの深さ以上めり込んでいたら挟まれ候補とみなす (m)")]
    private float pinchDepthThreshold = 0.15f;

    [SerializeField, Tooltip("脱出方向の塞がり確認で追加する余裕距離 (m)")]
    private float blockCheckMargin = 0.1f;

    [Header("退避先の探索")]
    [SerializeField, Tooltip("探索の刻み幅 (m)")]
    private float escapeSearchStep = 0.25f;

    [SerializeField, Tooltip("この距離まで探索して空きが無ければクラッシュ判定 (m)")]
    private float maxEscapeDistance = 4f;

    [Header("クラッシュ")]
    [Tooltip("クラッシュ判定時に呼ばれる(死亡処理・リスポーン等を登録)")]
    public UnityEvent onCrushed;

    [SerializeField, Tooltip("動作ログを出す(検証用)")]
    private bool logEvents = true;

    private CharacterController cc;
    private readonly Collider[] overlaps = new Collider[16];
    private readonly RaycastHit[] hits = new RaycastHit[16];
    private readonly Vector3[] escapeDirections = new Vector3[8];
    private Vector3 lastSafePosition;
    private bool crushNotified;

    private void Start()
    {
        cc = GetComponent<CharacterController>();
        lastSafePosition = transform.position;
    }

    /// <summary>
    /// 挟まれ検出と解決。移動体のFixedUpdate移動の後、
    /// CharacterController.Move(ThirdPersonControllerのUpdate)の前に実行される。
    /// </summary>
    private void Update()
    {
        // 移動体への最も深いめり込みとその脱出方向(MTV)を取得
        float depth = GetDynamicPenetration(out Vector3 escapeDir);
        if (depth < pinchDepthThreshold)
        {
            crushNotified = false;
            return;
        }

        // 脱出方向が静的コライダーに塞がれていなければ、CCTの通常の押し出しに任せる
        // (開けた場所で移動体に押されているだけの状態)
        if (!SweepStatic(transform.position, escapeDir, depth + blockCheckMargin, out _))
        {
            return;
        }

        // 挟まれ確定。クラッシュ通知済みの間は探索を省略(onCrushed側の処理に委ねる)
        if (crushNotified) return;

        if (logEvents)
        {
            Debug.Log($"[PinchEscape] 挟まれ検出 (深さ: {depth:F3}, 脱出方向: {escapeDir})");
        }

        // 退避先を探索
        int dirCount = BuildEscapeDirections(escapeDir);
        for (float d = escapeSearchStep; d <= maxEscapeDistance; d += escapeSearchStep)
        {
            for (int i = 0; i < dirCount; i++)
            {
                if (TryTeleport(escapeDirections[i], d)) return;
            }
        }

        // 退避先なし → クラッシュ判定(1回だけ通知)
        crushNotified = true;
        if (logEvents)
        {
            Debug.LogWarning($"[PinchEscape] 退避先なし。クラッシュ判定を発火 (めり込み深さ: {depth:F3})");
        }
        onCrushed?.Invoke();
    }

    /// <summary>静的コライダーへのめり込みをクランプ(壁すり抜け防止)</summary>
    private void LateUpdate()
    {
        if (!HasStaticOverlap(transform.position))
        {
            lastSafePosition = transform.position;
            return;
        }

        ClampToWall();

        if (!HasStaticOverlap(transform.position))
        {
            lastSafePosition = transform.position;
        }
    }

    /// <summary>
    /// 退避方向の候補を作る。
    /// 挟まれが水平方向なら壁面に沿った左右、垂直方向なら水平8方位。
    /// </summary>
    private int BuildEscapeDirections(Vector3 escapeDir)
    {
        Vector3 horizontal = Vector3.ProjectOnPlane(escapeDir, Vector3.up);
        if (horizontal.sqrMagnitude > 0.01f)
        {
            // 水平の挟まれ: 壁面に沿った横方向(左右)
            Vector3 lateral = Vector3.Cross(Vector3.up, horizontal).normalized;
            escapeDirections[0] = lateral;
            escapeDirections[1] = -lateral;
            return 2;
        }

        // 垂直の挟まれ(落下物など): 水平8方位を探索
        for (int i = 0; i < 8; i++)
        {
            float angle = i * 45f * Mathf.Deg2Rad;
            escapeDirections[i] = new Vector3(Mathf.Sin(angle), 0f, Mathf.Cos(angle));
        }
        return 8;
    }

    /// <summary>
    /// 安全位置から現在位置へのスイープを行い、静的コライダーの手前に位置を戻す。
    /// </summary>
    private void ClampToWall()
    {
        Vector3 delta = transform.position - lastSafePosition;
        float distance = delta.magnitude;

        if (distance > 1e-5f)
        {
            Vector3 direction = delta / distance;
            if (SweepStatic(lastSafePosition, direction, distance, out RaycastHit hit))
            {
                Vector3 clamped = lastSafePosition + direction * Mathf.Max(hit.distance - wallMargin, 0f);
                SetPosition(clamped);
                if (logEvents)
                {
                    Debug.Log($"[PinchEscape] '{hit.collider.name}' の手前にクランプ (法線: {hit.normal})");
                }
                return;
            }
        }

        // スイープで検出できない場合(その場でめり込んだ等)は安全位置へ戻す
        SetPosition(lastSafePosition);
        if (logEvents)
        {
            Debug.Log("[PinchEscape] スイープ不能のため安全位置へ復帰");
        }
    }

    /// <summary>指定方向・距離の退避を試みる。経路と退避先が安全なら移動してtrue</summary>
    private bool TryTeleport(Vector3 direction, float distance)
    {
        Vector3 candidate = transform.position + direction * distance;

        // 退避先が全コライダーに対して空いているか
        if (!IsFree(candidate)) return false;

        // 移動経路が静的コライダー(壁)を跨いでいないか
        if (SweepStatic(transform.position, direction, distance, out _)) return false;

        SetPosition(candidate);
        lastSafePosition = candidate;
        if (logEvents)
        {
            Debug.Log($"[PinchEscape] 退避先へ移動: {candidate}");
        }
        return true;
    }

    /// <summary>静的コライダー限定のカプセルスイープ。最も近いヒットを返す</summary>
    private bool SweepStatic(Vector3 origin, Vector3 direction, float distance, out RaycastHit nearest)
    {
        GetCapsuleAt(origin, out Vector3 p0, out Vector3 p1, out float radius);
        int count = Physics.CapsuleCastNonAlloc(p0, p1, radius, direction, hits, distance,
            obstacleLayers, QueryTriggerInteraction.Ignore);

        nearest = default;
        float nearestDistance = float.MaxValue;
        bool found = false;
        for (int i = 0; i < count; i++)
        {
            RaycastHit hit = hits[i];
            if (hit.collider.transform.root == transform.root) continue;
            if (hit.collider.attachedRigidbody != null) continue;
            if (hit.distance <= 0f && hit.point == Vector3.zero) continue; // 開始時から重なっているコライダーは除外
            if (hit.distance < nearestDistance)
            {
                nearestDistance = hit.distance;
                nearest = hit;
                found = true;
            }
        }
        return found;
    }

    /// <summary>静的コライダーと重なっているか</summary>
    private bool HasStaticOverlap(Vector3 position)
    {
        GetCapsuleAt(position, out Vector3 p0, out Vector3 p1, out float radius);
        int count = Physics.OverlapCapsuleNonAlloc(p0, p1, radius, overlaps,
            obstacleLayers, QueryTriggerInteraction.Ignore);

        for (int i = 0; i < count; i++)
        {
            if (overlaps[i].transform.root == transform.root) continue;
            if (overlaps[i].attachedRigidbody != null) continue;
            return true;
        }
        return false;
    }

    /// <summary>Rigidbody持ちコライダーへの最も深いめり込みと、その脱出方向を返す</summary>
    private float GetDynamicPenetration(out Vector3 escapeDir)
    {
        GetCapsuleAt(transform.position, out Vector3 p0, out Vector3 p1, out float radius);
        int count = Physics.OverlapCapsuleNonAlloc(p0, p1, radius, overlaps,
            obstacleLayers, QueryTriggerInteraction.Ignore);

        float maxDepth = 0f;
        escapeDir = Vector3.up;
        for (int i = 0; i < count; i++)
        {
            Collider col = overlaps[i];
            if (col.transform.root == transform.root) continue;
            if (col.attachedRigidbody == null) continue;

            if (Physics.ComputePenetration(
                    cc, transform.position, transform.rotation,
                    col, col.transform.position, col.transform.rotation,
                    out Vector3 direction, out float distance))
            {
                if (distance > maxDepth)
                {
                    maxDepth = distance;
                    escapeDir = direction;
                }
            }
        }
        return maxDepth;
    }

    /// <summary>候補位置でカプセルがどのコライダーとも重ならないか</summary>
    private bool IsFree(Vector3 candidate)
    {
        GetCapsuleAt(candidate, out Vector3 p0, out Vector3 p1, out float radius);
        int count = Physics.OverlapCapsuleNonAlloc(p0, p1, radius, overlaps,
            obstacleLayers, QueryTriggerInteraction.Ignore);

        for (int i = 0; i < count; i++)
        {
            if (overlaps[i].transform.root == transform.root) continue;
            return false;
        }
        return true;
    }

    /// <summary>CharacterControllerを考慮して位置を直接設定する</summary>
    private void SetPosition(Vector3 position)
    {
        cc.enabled = false;
        transform.position = position;
        cc.enabled = true;
        Physics.SyncTransforms();
    }

    /// <summary>指定位置でのCharacterControllerカプセル形状(skinTolerance分縮小)</summary>
    private void GetCapsuleAt(Vector3 position, out Vector3 p0, out Vector3 p1, out float radius)
    {
        Vector3 center = position + transform.rotation * Vector3.Scale(cc.center, transform.lossyScale);
        radius = Mathf.Max(cc.radius - skinTolerance, 0.01f);
        float halfSegment = Mathf.Max(cc.height * 0.5f - cc.radius, 0f);
        p0 = center + Vector3.up * halfSegment;
        p1 = center - Vector3.up * halfSegment;
    }
}

今回、GPT-5.6への依頼で作成してもらったメインの処理となる「PlayerCrushGameOverController」も載せておきます。

■GPT-5.6に作成してもらったゲームオーバー系処理のスクリプト

/// <summary>
/// PinchEscapeResolver が退避不能を通知した際に、プレイヤーの戦闘不能演出から
/// シーン再読み込みまでを一続きで実行する。挟まれたまま通常操作が継続すると
/// CharacterController の押し出しが再発するため、演出開始時に操作系と衝突判定を停止する。
/// lilToon の Dissolve は実行時に複製したマテリアルだけへ適用し、元アセットを変更しない。
/// </summary>
[RequireComponent(typeof(PinchEscapeResolver))]
public class PlayerCrushGameOverController : MonoBehaviour
{
    [Header("戦闘不能アニメーション")]
    [SerializeField, Tooltip("戦闘不能アニメーションを再生するVRMモデル側のAnimator。未設定なら子から自動検索する")]
    private Animator defeatAnimator;

    [SerializeField, Tooltip("Animator Controllerに戦闘不能ステートがある場合のステート名。存在しなければ倒れ込み演出を使用する")]
    private string defeatStateName = "Defeat";

    [SerializeField, Tooltip("戦闘不能アニメーションを見せる時間(秒)")]
    private float defeatAnimationDuration = 1.2f;

    [SerializeField, Tooltip("専用ステートが無い場合にモデルを倒す角度")]
    private float fallbackFallAngle = 82f;

    [SerializeField, Tooltip("専用ステートが無い場合にモデルを沈ませる距離(m)")]
    private float fallbackDropDistance = 0.15f;

    [Header("ゲームオーバーカメラ")]
    [SerializeField, Range(0.05f, 1f), Tooltip("ゲームオーバー直後に適用する時間経過速度。0.5なら通常の半速")]
    private float slowMotionScale = 0.5f;

    [SerializeField, Min(0f), Tooltip("スローモーションとズームを継続する実時間(秒)")]
    private float slowMotionDuration = 0.5f;

    [SerializeField, Range(0f, 30f), Tooltip("スローモーション中に狭めるカメラのField of View")]
    private float zoomFieldOfViewDelta = 8f;

    [SerializeField, Min(0.01f), Tooltip("ズーム後にカメラを見下ろし位置へ移動する時間(秒)")]
    private float overheadMoveDuration = 1.1f;

    [SerializeField, Tooltip("プレイヤーから見た見下ろしカメラの高さ(m)")]
    private float overheadHeight = 6.5f;

    [SerializeField, Tooltip("見下ろし時にプレイヤー背面方向へ離す距離(m)")]
    private float overheadBackwardDistance = 2.2f;

    [SerializeField, Tooltip("カメラが注視するプレイヤー上の高さ(m)")]
    private float cameraLookAtHeight = 0.8f;

    [Header("Dissolve演出")]
    [SerializeField, Tooltip("Dissolveでプレイヤーが消えるまでの時間(秒)")]
    private float dissolveDuration = 1.5f;

    [SerializeField, Tooltip("Dissolve境界の発光色")]
    private Color dissolveEdgeColor = new Color(0.2f, 0.85f, 1f, 1f);

    [SerializeField, Tooltip("事前生成したグレースケールのDissolveマスク兼ノイズテクスチャ")]
    private Texture2D dissolveMaskTexture;

    [SerializeField, Range(-1f, 2f), Tooltip("Dissolve開始時の範囲値。小さいほど全体が表示される")]
    private float dissolveStartThreshold = -0.05f;

    [SerializeField, Range(-1f, 2f), Tooltip("Dissolve終了時の範囲値。1より大きいと完全に消える")]
    private float dissolveEndThreshold = 1.05f;

    [SerializeField, Range(0.001f, 0.5f), Tooltip("Dissolve境界のぼかし幅")]
    private float dissolveEdgeWidth = 0.08f;

    [SerializeField, Range(0f, 1f), Tooltip("Dissolve境界へ加えるノイズの強さ")]
    private float dissolveNoiseStrength = 0.1f;

    [Header("画面遷移")]
    [SerializeField, Tooltip("黒画面へフェードする時間(秒)")]
    private float fadeDuration = 1f;

    [SerializeField, Tooltip("暗転完了後、シーンを再読み込みするまでの待機時間(秒)")]
    private float reloadDelay = 1f;

    private static readonly int DissolveMaskId = Shader.PropertyToID("_DissolveMask");
    private static readonly int DissolveNoiseMaskId = Shader.PropertyToID("_DissolveNoiseMask");
    private static readonly int DissolveNoiseStrengthId = Shader.PropertyToID("_DissolveNoiseStrength");
    private static readonly int DissolveParamsId = Shader.PropertyToID("_DissolveParams");
    private static readonly int DissolveColorId = Shader.PropertyToID("_DissolveColor");

    private readonly List<Material> runtimeMaterials = new List<Material>();
    private PinchEscapeResolver pinchResolver;
    private Transform visualRoot;
    private Image fadeImage;
    private CinemachineVirtualCamera gameOverCamera;
    private CinemachineTransposer gameOverTransposer;
    private bool isGameOver;

    /// <summary>ゲームオーバー演出が進行中ならtrue。</summary>
    public bool IsGameOver => isGameOver;

    private void Awake()
    {
        pinchResolver = GetComponent<PinchEscapeResolver>();
        if (pinchResolver.onCrushed == null)
        {
            pinchResolver.onCrushed = new UnityEngine.Events.UnityEvent();
        }
        pinchResolver.onCrushed.AddListener(BeginGameOver);

        ResolveVisualReferences();
    }

    private void OnDestroy()
    {
        if (pinchResolver != null && pinchResolver.onCrushed != null)
        {
            pinchResolver.onCrushed.RemoveListener(BeginGameOver);
        }

        for (int i = 0; i < runtimeMaterials.Count; i++)
        {
            if (runtimeMaterials[i] != null)
            {
                Destroy(runtimeMaterials[i]);
            }
        }

    }

    /// <summary>挟まれ以外の死亡要因からも呼べるゲームオーバー開始API。</summary>
    public void BeginGameOver()
    {
        if (isGameOver)
        {
            return;
        }

        isGameOver = true;
        Debug.Log("[CrushGameOver] ゲームオーバー演出を開始", this);
        StartCoroutine(RunGameOverSequence());
    }

    private IEnumerator RunGameOverSequence()
    {
        StopPlayerControl();
        StartCoroutine(PlayGameOverCameraSequence());
        yield return PlayDefeatAnimation();
        yield return PlayDissolve();
        yield return FadeToBlack();
        yield return WaitUnscaled(reloadDelay);

        Time.timeScale = 1f;
        AudioListener.pause = false;

        Scene activeScene = SceneManager.GetActiveScene();
        if (activeScene.buildIndex >= 0)
        {
            SceneManager.LoadScene(activeScene.buildIndex);
        }
        else
        {
            Debug.LogError($"[CrushGameOver] '{activeScene.path}' がBuild Settingsに未登録のため再読み込みできません。", this);
        }
    }

    private void StopPlayerControl()
    {
        Time.timeScale = 1f;
        AudioListener.pause = false;

        DisableIfPresent(GetComponent<ThirdPersonController>());
        DisableIfPresent(GetComponent<StarterAssetsInputs>());
        DisableIfPresent(GetComponent<PlayerInput>());
        DisableIfPresent(GetComponent<PlayerAttackDash>());
        DisableIfPresent(GetComponent<PauseManager>());
        DisableIfPresent(GetComponent<ClaudeMCPSample.VRMAnimatorBridge>());
        DisableIfPresent(pinchResolver);

        CharacterController characterController = GetComponent<CharacterController>();
        if (characterController != null)
        {
            characterController.enabled = false;
        }
    }

    private IEnumerator PlayDefeatAnimation()
    {
        if (defeatAnimator != null &&
            defeatAnimator.runtimeAnimatorController != null &&
            defeatAnimator.HasState(0, Animator.StringToHash(defeatStateName)))
        {
            defeatAnimator.CrossFadeInFixedTime(defeatStateName, 0.1f, 0, 0f);
            yield return WaitScaled(defeatAnimationDuration);
            yield break;
        }

        if (visualRoot == null)
        {
            Debug.LogWarning("[CrushGameOver] VRMモデルが見つからないため、倒れ込み演出を省略します。", this);
            yield return WaitScaled(defeatAnimationDuration);
            yield break;
        }

        if (defeatAnimator != null)
        {
            defeatAnimator.enabled = false;
        }

        Quaternion startRotation = visualRoot.localRotation;
        Quaternion endRotation = startRotation * Quaternion.Euler(0f, 0f, fallbackFallAngle);
        Vector3 startPosition = visualRoot.localPosition;
        Vector3 endPosition = startPosition + Vector3.down * fallbackDropDistance;

        float elapsed = 0f;
        while (elapsed < defeatAnimationDuration)
        {
            elapsed += Time.deltaTime;
            float t = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / Mathf.Max(defeatAnimationDuration, 0.01f)));
            visualRoot.localRotation = Quaternion.Slerp(startRotation, endRotation, t);
            visualRoot.localPosition = Vector3.Lerp(startPosition, endPosition, t);
            yield return null;
        }
    }

    private IEnumerator PlayGameOverCameraSequence()
    {
        CreateGameOverCamera();

        float originalTimeScale = Time.timeScale;
        float initialFieldOfView = gameOverCamera != null
            ? gameOverCamera.m_Lens.FieldOfView
            : (Camera.main != null ? Camera.main.fieldOfView : 50f);
        float zoomedFieldOfView = Mathf.Max(15f, initialFieldOfView - zoomFieldOfViewDelta);

        Time.timeScale = Mathf.Clamp(slowMotionScale, 0.05f, 1f);
        float elapsed = 0f;
        while (elapsed < slowMotionDuration)
        {
            elapsed += Time.unscaledDeltaTime;
            float t = Mathf.SmoothStep(0f, 1f,
                Mathf.Clamp01(elapsed / Mathf.Max(slowMotionDuration, 0.01f)));
            if (gameOverCamera != null)
            {
                gameOverCamera.m_Lens.FieldOfView = Mathf.Lerp(initialFieldOfView, zoomedFieldOfView, t);
            }
            yield return null;
        }

        Time.timeScale = originalTimeScale > 0f ? originalTimeScale : 1f;
        if (gameOverTransposer == null)
        {
            yield break;
        }

        Vector3 startOffset = gameOverTransposer.m_FollowOffset;
        Vector3 endOffset = Vector3.up * overheadHeight - transform.forward * overheadBackwardDistance;
        elapsed = 0f;
        while (elapsed < overheadMoveDuration)
        {
            elapsed += Time.unscaledDeltaTime;
            float t = Mathf.SmoothStep(0f, 1f,
                Mathf.Clamp01(elapsed / Mathf.Max(overheadMoveDuration, 0.01f)));
            gameOverTransposer.m_FollowOffset = Vector3.Lerp(startOffset, endOffset, t);
            yield return null;
        }

        gameOverTransposer.m_FollowOffset = endOffset;
    }

    private void CreateGameOverCamera()
    {
        if (gameOverCamera != null)
        {
            return;
        }

        var cameraObject = new GameObject("CrushGameOverCamera");
        gameOverCamera = cameraObject.AddComponent<CinemachineVirtualCamera>();
        gameOverCamera.Priority = 100;
        gameOverCamera.Follow = transform;
        gameOverCamera.LookAt = visualRoot != null ? visualRoot : transform;

        GameObject baseCameraObject = GameObject.Find("PlayerFollowCamera");
        CinemachineVirtualCamera baseCamera = baseCameraObject != null
            ? baseCameraObject.GetComponent<CinemachineVirtualCamera>()
            : null;
        if (baseCamera != null)
        {
            gameOverCamera.m_Lens = baseCamera.m_Lens;
        }
        else if (Camera.main != null)
        {
            gameOverCamera.m_Lens.FieldOfView = Camera.main.fieldOfView;
        }

        gameOverTransposer = gameOverCamera.AddCinemachineComponent<CinemachineTransposer>();
        gameOverTransposer.m_BindingMode = CinemachineTransposer.BindingMode.WorldSpace;
        gameOverTransposer.m_XDamping = 0f;
        gameOverTransposer.m_YDamping = 0f;
        gameOverTransposer.m_ZDamping = 0f;
        gameOverTransposer.m_FollowOffset = Camera.main != null
            ? Camera.main.transform.position - transform.position
            : new Vector3(0f, 1.5f, -4f);

        CinemachineComposer composer = gameOverCamera.AddCinemachineComponent<CinemachineComposer>();
        composer.m_TrackedObjectOffset = Vector3.up * cameraLookAtHeight;
        composer.m_HorizontalDamping = 0.15f;
        composer.m_VerticalDamping = 0.15f;
    }

    private IEnumerator PlayDissolve()
    {
        PrepareDissolveMaterials();
        if (runtimeMaterials.Count == 0)
        {
            Debug.LogWarning("[CrushGameOver] Dissolve対応マテリアルが見つからないため、消滅演出を省略します。", this);
            yield return WaitUnscaled(dissolveDuration);
            yield break;
        }

        float elapsed = 0f;
        while (elapsed < dissolveDuration)
        {
            elapsed += Time.unscaledDeltaTime;
            float threshold = Mathf.Lerp(dissolveStartThreshold, dissolveEndThreshold,
                Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / Mathf.Max(dissolveDuration, 0.01f))));
            SetDissolveThreshold(threshold);
            yield return null;
        }

        SetDissolveThreshold(dissolveEndThreshold);
    }

    private void PrepareDissolveMaterials()
    {
        if (runtimeMaterials.Count > 0 || visualRoot == null)
        {
            return;
        }

        if (dissolveMaskTexture == null)
        {
            Debug.LogError("[CrushGameOver] Dissolveマスクが未設定です。Setup Crush Game Overを再実行してください。", this);
            return;
        }

        var uniqueMaterials = new HashSet<Material>();
        Renderer[] renderers = visualRoot.GetComponentsInChildren<Renderer>(true);
        for (int rendererIndex = 0; rendererIndex < renderers.Length; rendererIndex++)
        {
            Material[] materials = renderers[rendererIndex].materials;
            for (int materialIndex = 0; materialIndex < materials.Length; materialIndex++)
            {
                Material material = materials[materialIndex];
                if (material == null || !material.HasProperty(DissolveParamsId) || !uniqueMaterials.Add(material))
                {
                    continue;
                }

                SwitchLilToonToCutout(material);
                material.SetTexture(DissolveMaskId, dissolveMaskTexture);
                if (material.HasProperty(DissolveNoiseMaskId))
                {
                    material.SetTexture(DissolveNoiseMaskId, dissolveMaskTexture);
                }
                if (material.HasProperty(DissolveNoiseStrengthId))
                {
                    material.SetFloat(DissolveNoiseStrengthId, dissolveNoiseStrength);
                }
                material.SetColor(DissolveColorId, dissolveEdgeColor);
                material.SetVector(DissolveParamsId,
                    new Vector4(1f, 0f, dissolveStartThreshold, dissolveEdgeWidth));
                runtimeMaterials.Add(material);
            }
        }
    }

    private static void SwitchLilToonToCutout(Material material)
    {
        string shaderName = material.shader != null ? material.shader.name : string.Empty;
        Shader cutoutShader = null;
        if (shaderName == "lilToon")
        {
            cutoutShader = Shader.Find("Hidden/lilToonCutout");
        }
        else if (shaderName == "Hidden/lilToonOutline")
        {
            cutoutShader = Shader.Find("Hidden/lilToonCutoutOutline");
        }

        if (cutoutShader != null)
        {
            material.shader = cutoutShader;
        }
    }

    private void SetDissolveThreshold(float threshold)
    {
        for (int i = 0; i < runtimeMaterials.Count; i++)
        {
            Material material = runtimeMaterials[i];
            if (material == null)
            {
                continue;
            }

            material.SetVector(DissolveParamsId, new Vector4(1f, 0f, threshold, dissolveEdgeWidth));
        }
    }

    private IEnumerator FadeToBlack()
    {
        EnsureFadeOverlay();
        float elapsed = 0f;
        while (elapsed < fadeDuration)
        {
            elapsed += Time.unscaledDeltaTime;
            float alpha = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(elapsed / Mathf.Max(fadeDuration, 0.01f)));
            fadeImage.color = new Color(0f, 0f, 0f, alpha);
            yield return null;
        }

        fadeImage.color = Color.black;
    }

    private void EnsureFadeOverlay()
    {
        if (fadeImage != null)
        {
            return;
        }

        var canvasObject = new GameObject("GameOverFadeCanvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
        Canvas canvas = canvasObject.GetComponent<Canvas>();
        canvas.renderMode = RenderMode.ScreenSpaceOverlay;
        canvas.sortingOrder = short.MaxValue;

        var imageObject = new GameObject("BlackFade", typeof(RectTransform), typeof(Image));
        imageObject.transform.SetParent(canvasObject.transform, false);
        RectTransform rectTransform = imageObject.GetComponent<RectTransform>();
        rectTransform.anchorMin = Vector2.zero;
        rectTransform.anchorMax = Vector2.one;
        rectTransform.offsetMin = Vector2.zero;
        rectTransform.offsetMax = Vector2.zero;

        fadeImage = imageObject.GetComponent<Image>();
        fadeImage.color = new Color(0f, 0f, 0f, 0f);
        fadeImage.raycastTarget = true;
    }

    private void ResolveVisualReferences()
    {
        if (defeatAnimator == null)
        {
            UniVRM10.Vrm10Instance vrmInstance = GetComponentInChildren<UniVRM10.Vrm10Instance>(true);
            if (vrmInstance != null)
            {
                visualRoot = vrmInstance.transform;
                defeatAnimator = vrmInstance.GetComponent<Animator>();
            }
        }

        if (visualRoot == null && defeatAnimator != null)
        {
            visualRoot = defeatAnimator.transform;
        }

        if (visualRoot == null)
        {
            Debug.LogWarning("[CrushGameOver] VRMモデルのルートが見つかりません。InspectorでAnimatorを設定してください。", this);
        }
    }

    private static void DisableIfPresent(Behaviour behaviour)
    {
        if (behaviour != null)
        {
            behaviour.enabled = false;
        }
    }

    private static IEnumerator WaitUnscaled(float seconds)
    {
        float endTime = Time.unscaledTime + Mathf.Max(0f, seconds);
        while (Time.unscaledTime < endTime)
        {
            yield return null;
        }
    }

    private static IEnumerator WaitScaled(float seconds)
    {
        float elapsed = 0f;
        while (elapsed < Mathf.Max(0f, seconds))
        {
            elapsed += Time.deltaTime;
            yield return null;
        }
    }
}


使用量については、Businessプランの月間利用枠の約15%を消費しました。
※OpenAIの利用枠は毎月リセットされます。


ちなみに、ClaudeCodeとCodexを比較すると、
Codexは、特に指示を与えていない場合はコメントが少ない印象でした。
また、Unityエディター側で既存のPrefabを利用するのではなく、new GameObjectでUIなどを動的生成するコードが多く見られました。

この辺は、作成したもらった後に、細かく調整依頼をすることになりそうです。

Unityとの連携について

最後に、連携方法についても軽く触れておきます。
連携する際には、UnityとCodex両方で、それぞれ設定を行う必要があります。

Unity側の設定

Package Managerから「MCP Unity Server」を追加しましょう。
「Install package from gitURL」から、以下のURLを指定します。

https://github.com/CoderGamester/mcp-unity.git

※gitリポジトリのURL

https://github.com/CoderGamester/mcp-unity

次に、Tools -> MCP Unity -> Server Windowを開き、連携が有効になっていることを確認しましょう。

Codex側の設定

こちらは、チャットで依頼してしまいましょう。Codexに「Unity MCPを設定して」と依頼すると、必要な設定を自動で進めてくれます!

以上で連携完了です!

AIエンジニアも募集中です!

弊社では、AIを活用できるベテランエンジニアを募集しています!
「AIを使って面白いことをしている!」という方のほか、
クラウドサーバーで独自のAI環境を構築できる」「AIを使う上でのセキュリティ対策に深い見識がある」というスーパーエンジニアの方は、気軽にお問い合わせください!