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を使う上でのセキュリティ対策に深い見識がある」というスーパーエンジニアの方は、気軽にお問い合わせください!



