【Cocos Creator 3.8】CornerCorrection の実装:アタッチするだけで「天井の角に頭をぶつけたときに自動で横にずらして通り抜けやすくする」汎用スクリプト

2Dアクションゲームでよくある「天井の角に頭が引っかかって、プレイヤーが前に進めない」問題を、キャラクター側の処理だけで軽減するコンポーネントを実装します。
この CornerCorrection をプレイヤーノードにアタッチするだけで、ジャンプ中に天井角へ頭が当たったときに、少し横へ押し出してスムーズに通り抜けられるようになります。

コンポーネントの設計方針

このコンポーネントは、次のような状況を想定しています。

  • 2Dゲーム(Physics2D / RigidBody2D / Collider2D)を使用
  • プレイヤーは上方向へジャンプし、天井ブロックの角に頭をぶつけることがある
  • その際、「ほんの少し横にずらせば」進めるのに、コリジョンの角に引っかかってしまう

CornerCorrection は、

  • プレイヤーの頭の両端(左・右)付近から短いレイキャストを飛ばして「天井との接触」を検出
  • 「上方向にぶつかっているが、横方向には少し余裕がある」場合に、プレイヤーノードの位置を少しだけ横へ移動させる

というシンプルなロジックで角引っかかりを軽減します。
物理挙動そのものを書き換えるのではなく、「位置補正」だけを行うため、既存のジャンプ・移動処理と比較的安全に共存できます。

外部依存をなくす設計アプローチ

  • ゲーム全体の GameManager やインプット管理などには一切依存しません。
  • 必要な情報はすべてインスペクタの @property から設定します。
  • RigidBody2D / Collider2D が存在しない場合は、ログを出して自動的に動作を止めます(防御的実装)。
  • 「何に対してぶつかったか」はレイヤーマスクで指定し、特定のタイルマップやブロックだけを補正対象にできます。

インスペクタで設定可能なプロパティ

以下のような @property を用意します。

  • enabledCorrection: boolean
    ・角補正を有効にするかどうかのフラグ。
    ・一時的に無効化したい場合に便利。
  • headOffsetY: number
    ・キャラクターの中心から「頭の高さ」までのオフセット(ローカル座標)。
    ・正の値で上方向。
    ・例: キャラの高さが 2.0 の場合、1.01.1 など。
  • headHalfWidth: number
    ・頭の左右端までの半幅(ローカル座標)。
    ・これをもとに「左端・右端」からレイを飛ばします。
    ・例: キャラの幅が 1.0 の場合、0.45 など。
  • upCheckDistance: number
    ・上方向に飛ばすレイの長さ。
    ・頭からどれくらい上に天井があるかを見る距離。
    ・例: 0.10.2 程度。
  • sideCheckDistance: number
    ・横方向に飛ばすレイの長さ(左右)。
    ・「横に少し動かせば抜けられる」かどうかを判定するために使用。
    ・例: 0.20.4 程度。
  • maxCorrectionDistance: number
    ・実際に横へ補正する最大距離。
    ・この値より大きく移動する必要がある場合は補正しない(不自然なワープ防止)。
    ・例: 0.2
  • minVerticalVelocity: number
    ・補正を行うために必要な「上向き速度」の下限。
    ・キャラがほぼ停止しているときには補正しないようにするためのしきい値。
    ・例: 1.0(上方向に 1.0 以上の速度で移動しているときだけ補正)。
  • ceilingLayerMask: number
    ・レイキャストの対象とする物理レイヤーのビットマスク。
    PhysicsSystem2D.instanceraycast で使用。
    ・天井ブロック用のレイヤーだけに限定することで、他のオブジェクトには反応しないようにできます。
  • debugDraw: boolean
    ・レイの可視化など、デバッグ用ログを出すかどうか。
    ・開発中のみ有効にし、リリース時はオフにする想定。

これらを組み合わせて、「どの高さのどの幅を『頭』とみなすか」「どのくらい近い天井を補正対象にするか」「どのレイヤーを天井とみなすか」を柔軟に調整できます。

TypeScriptコードの実装


import { _decorator, Component, Node, Vec2, Vec3, RigidBody2D, PhysicsSystem2D, ERaycast2DType, IPhysics2DRaycastResult, Layers, geometry, director, Color, Graphics, UITransform } from 'cc';
const { ccclass, property } = _decorator;

/**
 * CornerCorrection
 * ジャンプ中に天井の角に頭が引っかかったとき、
 * キャラクターを少し横にずらして通り抜けやすくする補正コンポーネント。
 *
 * 想定環境:
 * - 2D物理 (Physics2D)
 * - このノードに RigidBody2D + Collider2D が付与されていること
 */
@ccclass('CornerCorrection')
export class CornerCorrection extends Component {

    @property({
        tooltip: '角補正を有効にするかどうか。\nfalse にするとこのコンポーネントは何もしません。'
    })
    public enabledCorrection: boolean = true;

    @property({
        tooltip: 'キャラクター中心から頭の位置までのYオフセット(ローカル座標)。\n正の値で上方向。'
    })
    public headOffsetY: number = 0.9;

    @property({
        tooltip: '頭の左右端までの半幅(ローカル座標)。\nこの幅の左右端からレイを飛ばして天井を検出します。'
    })
    public headHalfWidth: number = 0.4;

    @property({
        tooltip: '上方向に飛ばすレイの長さ(ワールド空間)。\n頭の少し上にある天井を検出するための距離。'
    })
    public upCheckDistance: number = 0.15;

    @property({
        tooltip: '横方向に飛ばすレイの長さ(ワールド空間)。\nこの距離以内なら「少し横へずらせば抜けられる」とみなします。'
    })
    public sideCheckDistance: number = 0.3;

    @property({
        tooltip: '実際に横へ補正する最大距離(ワールド空間)。\nこれより大きい補正は行いません(ワープ防止)。'
    })
    public maxCorrectionDistance: number = 0.25;

    @property({
        tooltip: '補正を行うために必要な上向き速度のしきい値。\nRigidBody2D の線形速度がこの値以上のときのみ補正します。'
    })
    public minVerticalVelocity: number = 1.0;

    @property({
        tooltip: '天井とみなすオブジェクトのレイヤーマスク。\nPhysicsSystem2D の raycast で使用されます。'
    })
    public ceilingLayerMask: number = 0;

    @property({
        tooltip: 'デバッグ用にレイキャストを可視化したりログを出力するかどうか。'
    })
    public debugDraw: boolean = false;

    private _rigidBody: RigidBody2D | null = null;

    // デバッグ描画用(任意):UIモードの Graphics を使って簡易描画
    private _debugGraphics: Graphics | null = null;

    onLoad() {
        this._rigidBody = this.getComponent(RigidBody2D);
        if (!this._rigidBody) {
            console.error('[CornerCorrection] このノードに RigidBody2D が見つかりません。角補正は動作しません。');
        }

        // ceilingLayerMask が未設定なら、デフォルトですべてのレイヤーを対象にする
        if (this.ceilingLayerMask === 0) {
            this.ceilingLayerMask = 0xffffffff; // 32bit 全ビットON
        }

        if (this.debugDraw) {
            this._initDebugGraphics();
        }
    }

    start() {
        // 特に開始時の処理はなし。毎フレーム update でレイチェックを行う。
    }

    update(deltaTime: number) {
        if (!this.enabledCorrection) {
            return;
        }
        if (!this._rigidBody) {
            // RigidBody2D が無ければ何もしない(onLoad でエラー済み)
            return;
        }

        // 上向きにある程度動いているときだけ補正を試みる
        const velocity = this._rigidBody.linearVelocity;
        if (!velocity || velocity.y < this.minVerticalVelocity) {
            return;
        }

        // 頭の左右端のワールド座標を計算
        const node = this.node;
        const worldPos = node.worldPosition.clone();

        // ローカルオフセットをワールドに変換(スケールのみ考慮)
        const scale = node.worldScale;
        const headY = worldPos.y + this.headOffsetY * scale.y;
        const halfW = this.headHalfWidth * Math.abs(scale.x);

        const leftHead = new Vec3(worldPos.x - halfW, headY, worldPos.z);
        const rightHead = new Vec3(worldPos.x + halfW, headY, worldPos.z);

        if (this.debugDraw) {
            this._clearDebugGraphics();
        }

        // 左・右それぞれで天井に当たっているかチェック
        const leftHitUp = this._raycastUp(leftHead);
        const rightHitUp = this._raycastUp(rightHead);

        if (this.debugDraw) {
            this._drawRay(leftHead, new Vec3(leftHead.x, leftHead.y + this.upCheckDistance, leftHead.z), Color.GREEN);
            this._drawRay(rightHead, new Vec3(rightHead.x, rightHead.y + this.upCheckDistance, rightHead.z), Color.GREEN);
        }

        // どちらか一方だけが天井に当たっている場合に「角で引っかかっている」とみなす
        if (leftHitUp && !rightHitUp) {
            // 左側だけが頭をぶつけている → 右側へずらせるかチェック
            this._trySideCorrection(leftHead, rightHead, +1);
        } else if (!leftHitUp && rightHitUp) {
            // 右側だけが頭をぶつけている → 左側へずらせるかチェック
            this._trySideCorrection(rightHead, leftHead, -1);
        }
    }

    /**
     * 上方向へのレイキャスト。天井(ceilingLayerMask)に当たっているか判定する。
     */
    private _raycastUp(origin: Vec3): IPhysics2DRaycastResult | null {
        const physics = PhysicsSystem2D.instance;
        const from = new Vec2(origin.x, origin.y);
        const to = new Vec2(origin.x, origin.y + this.upCheckDistance);

        const results: IPhysics2DRaycastResult[] = [];
        physics.raycast(from, to, ERaycast2DType.Closest, this.ceilingLayerMask, results);

        if (results.length > 0) {
            return results[0];
        }
        return null;
    }

    /**
     * 横方向にスペースがあるか確認し、あればノードを横にずらす。
     * @param hitHead  頭が天井に当たっている側の頭位置
     * @param freeHead もう片方の頭位置(天井に当たっていない側)
     * @param direction +1: 右へ補正, -1: 左へ補正
     */
    private _trySideCorrection(hitHead: Vec3, freeHead: Vec3, direction: 1 | -1) {
        const physics = PhysicsSystem2D.instance;

        // 補正方向に向かって横方向のレイを飛ばし、「壁や天井がすぐ横にあるか」を確認
        const origin = hitHead.clone();
        const targetX = origin.x + this.sideCheckDistance * direction;
        const from = new Vec2(origin.x, origin.y);
        const to = new Vec2(targetX, origin.y);

        const results: IPhysics2DRaycastResult[] = [];
        physics.raycast(from, to, ERaycast2DType.Closest, this.ceilingLayerMask, results);

        if (this.debugDraw) {
            this._drawRay(
                new Vec3(origin.x, origin.y, origin.z),
                new Vec3(targetX, origin.y, origin.z),
                Color.YELLOW
            );
        }

        // 横方向にすぐ障害物があるなら補正しない
        if (results.length > 0) {
            if (this.debugDraw) {
                console.log('[CornerCorrection] 横方向に障害物があるため補正しません。');
            }
            return;
        }

        // もう片方の頭位置との差分から、どの程度ずらせば「角から外れる」かを計算
        const currentX = this.node.worldPosition.x;
        const desiredX = freeHead.x; // もう片方の頭がある位置までずらすイメージ
        let correction = desiredX - currentX;

        // direction と同じ向きの補正だけを許可する(逆方向への補正を防ぐ)
        if (correction * direction <= 0) {
            return;
        }

        // 最大補正距離を超える場合は制限する
        if (Math.abs(correction) > this.maxCorrectionDistance) {
            correction = this.maxCorrectionDistance * direction;
        }

        // 実際にノードのワールド位置を補正
        const newWorldPos = this.node.worldPosition.clone();
        newWorldPos.x += correction;
        this.node.setWorldPosition(newWorldPos);

        if (this.debugDraw) {
            console.log(`[CornerCorrection] 角補正を実行: x を ${correction.toFixed(3)} だけ ${direction > 0 ? '右' : '左'}へ移動しました。`);
        }
    }

    /**
     * デバッグ描画用 Graphics の初期化。
     * UIカメラがあるシーンの場合のみ有効です。
     * 必須ではないため、見つからない場合は警告のみでスキップします。
     */
    private _initDebugGraphics() {
        // Graphics を描画するためのノードを作成(このコンポーネントの子として)
        const gNode = new Node('CornerCorrectionDebug');
        this.node.addChild(gNode);
        this._debugGraphics = gNode.addComponent(Graphics);

        if (!this._debugGraphics) {
            console.warn('[CornerCorrection] Graphics コンポーネントを追加できませんでした。デバッグ描画は無効です。');
            return;
        }

        this._debugGraphics.lineWidth = 2;
        this._debugGraphics.strokeColor = Color.GREEN;
    }

    private _clearDebugGraphics() {
        if (this._debugGraphics) {
            this._debugGraphics.clear();
        }
    }

    private _drawRay(from: Vec3, to: Vec3, color: Color) {
        if (!this._debugGraphics) {
            return;
        }
        const g = this._debugGraphics;
        g.strokeColor = color;
        g.moveTo(from.x, from.y);
        g.lineTo(to.x, to.y);
        g.stroke();
    }
}

コードのポイント解説

  • onLoad
    RigidBody2D を取得し、存在しなければ console.error を出して以降の処理をスキップします。
    ceilingLayerMask が 0(未設定)の場合は、暫定的に全レイヤー対象(0xffffffff)にします。
    debugDraw が true の場合、簡易的な Graphics を子ノードに生成します。
  • update
    enabledCorrectionRigidBody2D の存在チェック。
    ・上向き速度 velocity.yminVerticalVelocity 未満のときは補正しません(落下中や停止時の不自然な補正防止)。
    ・ノードのワールド座標とスケールから、「頭の左右端」のワールド座標 leftHead / rightHead を計算します。
    ・それぞれから上方向へレイを飛ばし、天井との接触を判定。片側だけが当たっている場合に「角に引っかかっている」とみなします。
  • _raycastUp
    PhysicsSystem2D.instance.raycast を使って、指定位置から上方向へのレイキャストを実行します。
    ERaycast2DType.Closest で最も近いヒットだけを取得しています。
  • _trySideCorrection
    ・天井に当たっている側の頭位置から、補正方向(左右)へレイを飛ばして、すぐ横に壁や天井がないか確認します。
    ・横に障害物があれば補正しません。
    ・「もう片方の頭位置」までの差分から、どの程度横に動かせば角から外れそうかを計算し、maxCorrectionDistance 以内に収まるように制限します。
    ・計算した量だけ node.worldPosition.x を直接変更して補正します。
  • デバッグ描画(任意)
    Graphics を使って、レイキャストの線を簡易的に描画します。
    ・UI カメラや座標系との兼ね合いで見えない場合もあるため、「補正ロジックの確認用」のおまけ機能と考えてください。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで任意のフォルダ(例: assets/scripts)を右クリックします。
  2. Create → TypeScript を選択し、ファイル名を CornerCorrection.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の TypeScript コードをそのまま貼り付けて保存します。

2. テスト用プレイヤーノードの準備

  1. Hierarchy パネルで右クリックし、Create → 2D Object → Sprite などでプレイヤー用ノードを作成します。
    (既にプレイヤーノードがある場合はそれを使用して構いません)
  2. プレイヤーノードを選択し、Inspector で次を確認・追加します:
    • 必須: RigidBody2D コンポーネントが追加されていること。
      追加されていない場合は、Add Component → Physics 2D → RigidBody2D を選択して追加します。
    • 必須: Collider2D(例: BoxCollider2D)が追加されていること。
      まだない場合は、Add Component → Physics 2D → BoxCollider2D を追加し、キャラクターの見た目に合わせてサイズを調整します。

RigidBody2D が無いとコンポーネントがエラーを出し、補正が動作しません。

3. CornerCorrection コンポーネントのアタッチ

  1. プレイヤーノードを選択した状態で、Inspector の下部にある Add Component ボタンを押します。
  2. Custom → CornerCorrection を選択して追加します。
  3. 追加された CornerCorrection コンポーネントの各プロパティを設定します。例として:
    • Enabled Correction: true
    • Head Offset Y: 0.9(キャラの高さに応じて調整)
    • Head Half Width: 0.4(キャラの横幅の半分より少し小さめ)
    • Up Check Distance: 0.15
    • Side Check Distance: 0.3
    • Max Correction Distance: 0.25
    • Min Vertical Velocity: 1.0
    • Ceiling Layer Mask: 天井ブロックのレイヤーに応じて設定
      • 特定レイヤーのみなら、対応するビットマスク値を入力
      • とりあえず全部対象にするなら 0 のままでも動作します(onLoad で全ビットONに補正)。
    • Debug Draw: 開発中は true にしてレイの様子を確認しても良いです。

4. 天井ブロック(角)となるオブジェクトの準備

  1. Hierarchy で右クリック → Create → 2D Object → Sprite などで天井用のブロックを作成します。
  2. Inspector で次を設定します:
    • BoxCollider2D などの Collider2D を追加し、ブロックの見た目に合わせてサイズを調整。
    • 必要に応じて RigidBody2D(Static タイプ)を追加するか、Physics2D の設定に応じて「静的コライダー」として扱われるようにします。
    • プレイヤーがぶつかる「天井の角」になるように、位置を調整します。
      例えば、L字型の足場や、天井ブロックを段差状に配置するとテストしやすいです。
    • このブロックのレイヤーを、ceilingLayerMask に含まれるレイヤーに設定します。

5. 実際に動作を確認する

  1. プレイヤーにジャンプ機能がある場合は、天井の角に向かってジャンプしてみます。
  2. 角に頭が当たる瞬間、以下の挙動を確認します:
    • 片側の頭だけが天井に当たっている状況で、キャラクターがほんの少しだけ横にずれて、天井の下をすり抜けられる。
    • 横方向にすぐ壁がある場合は、補正が行われず、その場でぶつかったままになる。
    • 上向きにほとんど動いていないとき(ジャンプし終わって落下中など)は、補正が発生しない。
  3. Debug Drawtrue にしている場合、再生中に Scene ビューでレイの線が描画されるか確認します。
    • 頭の両端から上方向に緑の線(upCheckDistance)。
    • 補正方向に黄色の線(sideCheckDistance)。
    • Console には「角補正を実行: …」のログが出力されます。

もし補正が強すぎる・弱すぎると感じた場合は、

  • Head Offset Y / Head Half Width で「頭の判定範囲」を調整
  • Up Check Distance で「どれくらい近い天井を検出するか」を調整
  • Side Check Distance / Max Correction Distance で「横ずらしの度合い」を調整
  • Min Vertical Velocity で「どのくらい強くジャンプしているときだけ補正するか」を調整

といった形で、ゲームに合うバランスを探ってください。

まとめ

CornerCorrection コンポーネントは、

  • 他のスクリプトやマネージャーに一切依存せず
  • プレイヤーノードにアタッチするだけで
  • 「天井の角に頭が引っかかって前に進めない」問題を自動で軽減

できる汎用的な補正スクリプトです。

パラメータはすべてインスペクタから調整可能なので、

  • キャラクターのサイズが違うゲーム
  • 物理スケール(1 ユニット = 1 メートル or 1 ピクセル)の違うプロジェクト
  • 天井ブロック用レイヤーを分けているプロジェクト

といった様々な環境でも、そのまま再利用できます。
また、「角補正」という一つの責務に限定した独立コンポーネントにしているため、移動・ジャンプ・アニメーションなど他の処理と組み合わせても管理しやすく、ゲーム開発の効率化にもつながります。

必要に応じて、

  • 床側の角での「乗り上げ補正」
  • 壁ジャンプ時の角度補正

なども同様のアプローチで別コンポーネントとして実装していくと、2Dアクションの「ひっかかり感」を減らし、気持ちの良い操作感を実現しやすくなります。