【Cocos Creator】アタッチするだけ!SurfaceFriction (摩擦制御)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】SurfaceFriction の実装:アタッチするだけで「床の上にいる時だけ横滑りを止める」汎用スクリプト

このガイドでは、RigidBody2D を持つキャラクターなどの横方向の滑り(velocity.x)を、床の上にいる時だけ徐々に減速させる汎用コンポーネント SurfaceFriction を実装します。

プレイヤーや敵キャラなどにこのコンポーネントをアタッチするだけで、「床に立っている間だけ摩擦が働き、停止状態に近づいていく」挙動を簡単に付与できます。物理マテリアルを細かくいじらなくても、スクリプト側で摩擦の強さを数値で調整できるのが利点です。


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

要件整理

  • 対象ノードは RigidBody2D を持つことを前提とする。
  • 「床の上にいる時」だけvelocity.x を徐々に 0 に近づける。
  • 床判定は以下のような条件で行う:
    • RigidBody2D に Collider2D があり、下方向に接触しているコライダーがあるとき「床にいる」とみなす。
    • 接触情報は Contact2DType.BEGIN_CONTACT / END_CONTACT で管理する。
    • 「下方向の床」の判定には、接触点の法線ベクトルを利用し、ある程度「上向き」の法線を持つ接触のみを床としてカウントする。
  • 摩擦の強さ・有効速度のしきい値などは インスペクタ上のプロパティから調整可能にする。
  • 他のカスタムスクリプトには一切依存せず、このコンポーネント単体で完結させる。

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

以下のようなプロパティを設計します。

  • enabledOnStart: boolean
    • ツールチップ例: ゲーム開始時に摩擦処理を有効にするかどうか。
    • true の場合、start() 時点で摩擦処理が有効。
    • 一時的に機能を無効化したい場合に便利。
  • frictionStrength: number
    • ツールチップ例: 床の上にいる時に横速度を減速させる強さ。大きいほど素早く止まる。
    • 単位は「1秒あたりの減速率」。
    • 例: 5 なら、1秒でおおよそ 5 程度 velocity.x が減少するイメージ。
  • minSpeedThreshold: number
    • ツールチップ例: この絶対値未満の横速度は 0 とみなして完全停止させる。
    • とても小さな速度で「いつまでも微妙に滑る」状態を防ぐためのデッドゾーン。
    • 例: 0.05 など。
  • applyOnlyWhenGrounded: boolean
    • ツールチップ例: 床に接地している時だけ摩擦を適用するか。false の場合、空中でも横速度を減衰させる。
    • 通常は true 推奨。
  • groundNormalYThreshold: number
    • ツールチップ例: 床とみなす法線ベクトルのY成分のしきい値。1に近いほど「より平らな床」のみを床判定する。
    • 例: 0.5 なら、normal.y >= 0.5 の接触を床とみなす。
    • 坂道を床として認識したい場合は値を下げる、垂直な壁を誤認識したくない場合は上げる。

内部実装のポイント

  • 必須コンポーネントの取得
    • RigidBody2DCollider2DonLoad()getComponent し、見つからない場合は error() ログを出す。
    • どちらか一方でも欠けている場合は、このコンポーネントの機能を無効化する。
  • 床接地判定
    • Collider2Don メソッドで BEGIN_CONTACT / END_CONTACT を購読。
    • BEGIN_CONTACT:
      • 全ての接触点の法線を調べ、normal.y >= groundNormalYThreshold のものがあれば 床カウンタをインクリメント
    • END_CONTACT:
      • 同様に床とみなしていた接触が終わったら 床カウンタをデクリメント
    • 床カウンタ > 0 のとき _isGrounded = true とする。
  • 摩擦処理
    • update(deltaTime) で毎フレーム処理。
    • 条件:
      • _body が存在しない場合は何もしない。
      • applyOnlyWhenGrounded が true の時、_isGrounded が false なら何もしない。
    • 横速度 vx = body.linearVelocity.x を取得。
    • 絶対値が minSpeedThreshold 未満なら vx = 0 にして終了。
    • そうでなければ、符号を保ったまま 0 に近づける処理を行う:
      • 摩擦量 decel = frictionStrength * deltaTime を計算。
      • Math.sign(vx) に応じて vx -= decel または vx += decel
      • 減速しすぎて符号が反転しそうな場合は vx = 0 にクランプ。
    • 計算した vxbody.linearVelocity に再代入。

TypeScriptコードの実装


import { _decorator, Component, Node, RigidBody2D, Collider2D, IPhysics2DContact, Contact2DType, Vec2, error } from 'cc';
const { ccclass, property } = _decorator;

/**
 * SurfaceFriction
 * 
 * 床の上にいる間だけ RigidBody2D の横方向速度 (velocity.x) を
 * 徐々に 0 に近づけて、滑りを止めるための汎用コンポーネント。
 * 
 * このスクリプトをアタッチするノードには、以下のコンポーネントが必要です:
 * - RigidBody2D
 * - Collider2D
 */
@ccclass('SurfaceFriction')
export class SurfaceFriction extends Component {

    @property({
        tooltip: 'ゲーム開始時に摩擦処理を有効にするかどうか。'
    })
    public enabledOnStart: boolean = true;

    @property({
        tooltip: '床の上にいる時に横速度を減速させる強さ。大きいほど素早く止まる(単位: 1秒あたりの速度減少量)。'
    })
    public frictionStrength: number = 5.0;

    @property({
        tooltip: 'この絶対値未満の横速度は 0 とみなして完全停止させるデッドゾーン。'
    })
    public minSpeedThreshold: number = 0.05;

    @property({
        tooltip: '床に接地している時だけ摩擦を適用するか。false の場合、空中でも横速度を減衰させる。'
    })
    public applyOnlyWhenGrounded: boolean = true;

    @property({
        tooltip: '床とみなす法線ベクトルのY成分のしきい値。1に近いほど「より平らな床」のみを床判定する。',
        min: 0.0,
        max: 1.0,
        step: 0.05
    })
    public groundNormalYThreshold: number = 0.5;

    // 内部状態
    private _body: RigidBody2D | null = null;
    private _collider: Collider2D | null = null;
    private _isGrounded: boolean = false;
    private _groundContactCount: number = 0;
    private _isActive: boolean = false;

    onLoad() {
        // 必須コンポーネントの取得
        this._body = this.getComponent(RigidBody2D);
        if (!this._body) {
            error('[SurfaceFriction] RigidBody2D がノードにアタッチされていません。SurfaceFriction を使用するには RigidBody2D が必要です。ノード名:', this.node.name);
        }

        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            error('[SurfaceFriction] Collider2D がノードにアタッチされていません。SurfaceFriction を使用するには Collider2D が必要です。ノード名:', this.node.name);
        }

        // コライダーがある場合のみ接触イベントを購読
        if (this._collider) {
            this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this);
        }
    }

    start() {
        // 開始時の有効状態を設定
        this._isActive = this.enabledOnStart;
    }

    onDestroy() {
        // イベント購読の解除(メモリリーク防止)
        if (this._collider) {
            this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
            this._collider.off(Contact2DType.END_CONTACT, this._onEndContact, this);
        }
    }

    /**
     * 外部から摩擦処理を有効化するための補助メソッド。
     */
    public enableFriction() {
        this._isActive = true;
    }

    /**
     * 外部から摩擦処理を無効化するための補助メソッド。
     */
    public disableFriction() {
        this._isActive = false;
    }

    /**
     * 接触開始イベントハンドラ
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!contact) {
            return;
        }

        // 接触点情報から法線ベクトルを取得し、床かどうかを判定
        const worldManifold = contact.getWorldManifold();
        const normals = worldManifold.normal;

        // Cocos Creator 3.x の getWorldManifold().normal は Vec2 の配列ではなく Vec2 一つの場合が多いので、
        // ここでは単一の Vec2 として扱う。
        // もし将来的なバージョンで仕様が変わった場合は、配列に対応するように修正してください。
        const n = normals as unknown as Vec2;
        if (n && n.y >= this.groundNormalYThreshold) {
            this._groundContactCount++;
            this._isGrounded = this._groundContactCount > 0;
        }
    }

    /**
     * 接触終了イベントハンドラ
     */
    private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (!contact) {
            return;
        }

        const worldManifold = contact.getWorldManifold();
        const normals = worldManifold.normal;
        const n = normals as unknown as Vec2;
        if (n && n.y >= this.groundNormalYThreshold) {
            this._groundContactCount = Math.max(0, this._groundContactCount - 1);
            this._isGrounded = this._groundContactCount > 0;
        }
    }

    update(deltaTime: number) {
        // 必須コンポーネントがない場合は何もしない
        if (!this._body) {
            return;
        }

        // SurfaceFriction が無効状態なら処理しない
        if (!this._isActive) {
            return;
        }

        // 接地時のみ適用する設定の場合、空中では処理しない
        if (this.applyOnlyWhenGrounded && !this._isGrounded) {
            return;
        }

        // 現在の線形速度を取得
        const v = this._body.linearVelocity;
        let vx = v.x;

        // ほぼ停止状態なら完全に 0 にする
        if (Math.abs(vx) < this.minSpeedThreshold) {
            if (vx !== 0) {
                v.x = 0;
                this._body.linearVelocity = v;
            }
            return;
        }

        // 摩擦による減速量を計算
        const decel = this.frictionStrength * deltaTime;

        if (vx > 0) {
            // 右向きに動いている場合、左向きに減速
            vx -= decel;
            if (vx < 0) {
                vx = 0;
            }
        } else if (vx < 0) {
            // 左向きに動いている場合、右向きに減速
            vx += decel;
            if (vx > 0) {
                vx = 0;
            }
        }

        // 計算結果を RigidBody2D に反映
        if (vx !== v.x) {
            v.x = vx;
            this._body.linearVelocity = v;
        }
    }
}

主要な処理の解説

  • onLoad()
    • RigidBody2DCollider2D を取得し、存在しない場合は error() で警告。
    • Collider2D がある場合のみ、接触イベント (BEGIN_CONTACT, END_CONTACT) を購読。
  • start()
    • enabledOnStart の値に応じて、摩擦処理の有効/無効を初期化。
  • _onBeginContact() / _onEndContact()
    • 接触している相手との間の法線ベクトル normal.y をチェックし、groundNormalYThreshold 以上なら「床」とみなす。
    • 床との接触数をカウントし、1 以上なら _isGrounded = true、0 なら false
  • update(deltaTime)
    • コンポーネントが有効かつ、必要に応じて「接地中」であることを確認。
    • RigidBody2D.linearVelocity.x を取得し、minSpeedThreshold 未満なら 0 にスナップ。
    • それ以外は frictionStrength * deltaTime だけ速度を減らし、0 をまたぎ越えないようにクランプ。
    • 更新した速度を RigidBody2D に反映。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、任意のフォルダ(例: assets/scripts)を選択します。
  2. そのフォルダ上で右クリックし、Create → TypeScript を選択します。
  3. 新しく作成されたスクリプトの名前を SurfaceFriction.ts に変更します。
  4. ダブルクリックして開き、既存の中身を全て削除して、前章で示した SurfaceFriction のコードをそのまま貼り付けて保存します。

2. テスト用ノード(キャラクター)の準備

  1. Hierarchy パネルで右クリックし、Create → 2D Object → Sprite などからテスト用のノードを作成します。
    • 名前は PlayerTestBody など、分かりやすいもので構いません。
  2. 作成したノードを選択し、Inspector パネルで以下のコンポーネントを追加します:
    • Add Component → Physics 2D → RigidBody2D
    • Add Component → Physics 2D → BoxCollider2D(または用途に応じた Collider2D)
  3. RigidBody2D の設定例:
    • Type: Dynamic
    • Gravity Scale: 1(通常の重力)
    • 他はデフォルトのままで構いません。

3. 床(Ground)ノードの準備

  1. Hierarchy で右クリック → Create → 2D Object → Sprite などから床用ノードを作成し、Ground と命名します。
  2. Inspector で GroundTransform を調整し、横長の床になるようにスケール・位置を変更します。
  3. Ground に以下のコンポーネントを追加します:
    • Add Component → Physics 2D → RigidBody2D
    • Add Component → Physics 2D → BoxCollider2D
  4. GroundRigidBody2D 設定:
    • Type: Static(床は動かないので Static にします)
  5. BoxCollider2D のサイズを床の見た目に合わせて調整します。

4. SurfaceFriction コンポーネントをアタッチ

  1. Hierarchy で先ほどのキャラクターノード(例: Player)を選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom カテゴリの中から SurfaceFriction を選択してアタッチします。

5. プロパティの設定例

SurfaceFriction のプロパティを次のように設定してみましょう:

  • Enabled On Start: チェック(true)
  • Friction Strength: 510(大きいほど早く止まる)
  • Min Speed Threshold: 0.05
  • Apply Only When Grounded: チェック(true)
  • Ground Normal Y Threshold: 0.5(標準的な床判定)

6. 動作確認

  1. シーン上で PlayerGround の上に落ちてくるように、Player の Y 座標を高めに設定します。
  2. 再生ボタン(▶) を押してゲームを実行します。
  3. 実行中に、以下のいずれかの方法で Player に横方向の速度を与えます:
    • 一時的に PlayerRigidBody2DLinear Velocity を設定しておき、再生直後に動くようにする。
    • または、簡単な一時スクリプトで start() 内から body.linearVelocity = new Vec2(5, 0); のように設定する。
  4. Player が床の上を滑り始めますが、徐々に速度が落ちて最終的に止まることを確認します。
  5. Apply Only When Groundedオフ にして再実行すると、空中にいる間も横速度が減衰する挙動になることも確認できます。

もしコンソールに

[SurfaceFriction] RigidBody2D がノードにアタッチされていません...
[SurfaceFriction] Collider2D がノードにアタッチされていません...

といったエラーが表示される場合は、対象ノードに RigidBody2D と Collider2D の両方が正しく追加されているかを確認してください。


まとめ

この SurfaceFriction コンポーネントを使うことで、

  • プレイヤーや敵キャラに「床の上だけ働く摩擦」を簡単に追加できる。
  • frictionStrengthminSpeedThreshold を調整するだけで、「キビキビ止まる」・「スーッと滑る」などの感触を素早くチューニングできる。
  • 物理マテリアルや他の管理スクリプトに依存せず、このスクリプト単体をアタッチするだけで機能する。

応用例としては:

  • 氷のステージでは frictionStrength を小さくして「よく滑る床」を表現。
  • 泥や砂地などのステージでは frictionStrength を大きくし、minSpeedThreshold もやや大きめにして「ピタッと止まる」感触に。
  • 特定のギミック(油まみれの床など)に乗ったときだけ、一時的に disableFriction() を呼んで滑りやすくする。

このように、SurfaceFriction は単体で完結しつつ、インスペクタから直感的に調整しやすい汎用コンポーネントとして、2D アクションやプラットフォーマー制作の効率を大きく高めてくれます。必要なノードにどんどんアタッチして、ゲーム全体の操作感を統一・チューニングしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!