【Cocos Creator】アタッチするだけ!IceFloor (氷の床)の実装方法【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】IceFloor(氷の床)の実装:アタッチするだけで「その上に乗ったキャラがツルツル滑る」物理エリアを実現する汎用スクリプト

このガイドでは、Cocos Creator 3.8.7 + TypeScript で「氷の床」エリアを実現する IceFloor コンポーネントを実装します。
ノードにアタッチしておくだけで、そのエリア内に入った 2D 物理オブジェクトの摩擦(Friction)を一時的に極端に下げ、エリアから出たら元に戻す仕組みです。

プレイヤーや敵キャラが氷の上で止まりにくくなり、慣性で滑り続ける表現を「他のスクリプトに一切依存せず」簡単に導入できます。


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

1. 要件整理

  • このコンポーネントをアタッチしたノードを「氷エリア」として扱う。
  • 2D 物理(Box2D)を前提とし、Collider2D の「トリガー」領域として機能させる。
  • 氷エリア内に入った他ノードの PhysicsMaterial(摩擦など)を一時的に変更し、出たら元に戻す。
  • 外部の GameManager やシングルトンには一切依存せず、このスクリプト単体で完結させる。
  • インスペクタから以下を調整可能にする:
    • 氷エリアで適用する摩擦値(friction)
    • 必要に応じて反発係数(restitution)も変更可能にするかどうか
    • どのグループのオブジェクトに適用するか(groupFilter / レイヤー名)
  • 防御的実装:
    • 自ノードに Collider2D が無い場合はエラーログを出して動作を止める。
    • トリガーになっていない場合は警告を出し、自動で sensor = true にする。
    • 対象ノードに Collider2D が無い場合は何もしない。
    • 元のマテリアル値をノード単位で記録し、エリアから出たときに正しく復元する。

2. 物理挙動と設計のポイント

Cocos Creator 3.8 の 2D 物理では、摩擦は PhysicsMaterial によって管理され、各 Collider2D に設定されます。
氷エリアの中にいる間だけこの摩擦値を小さくし、出たら元に戻せば「ツルツル滑る床」が表現できます。

  • 氷エリアは Collider2D のセンサー(trigger)として設定し、onBeginContact / onEndContact で侵入・離脱を検知します。
  • 接触してきたコライダーの sharedMaterial を取得し、摩擦値(とオプションで反発係数)を書き換えます。
  • 書き換え前の値を Map<Collider2D, { friction: number; restitution: number }> に保存しておき、エリアから出たときに復元します。
  • 対象を限定したい場合のために、インスペクタで「対象グループ名」を指定できるようにします(空ならすべて対象)。

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

  • iceFriction: number
    • 氷エリア内で適用する摩擦値。
    • 0 に近いほどツルツル滑る。0.0 ~ 0.1 くらいが目安。
    • デフォルト: 0.02
  • modifyRestitution: boolean
    • 氷エリア内で反発係数(restitution)も変更するかどうか。
    • チェックを外せば摩擦だけ変更する。
    • デフォルト: false
  • iceRestitution: number
    • modifyRestitution が true の場合に使用される反発係数。
    • 0 に近いほど弾まなくなる。1 に近いほどよく弾む。
    • デフォルト: 0.0(氷の上ではあまり弾まない想定)
  • targetGroupName: string
    • 氷効果を適用する対象のグループ名。
    • 空文字列の場合:すべてのグループに適用。
    • 例: "player" と入力すると、グループ名が player のノードにのみ適用。
  • debugLog: boolean
    • 氷エリアへの侵入・離脱などのログをコンソールに出すかどうか。
    • 動作確認時に便利。リリース時は OFF 推奨。

TypeScriptコードの実装


import { _decorator, Component, Collider2D, IPhysics2DContact, PhysicsMaterial, Node, director } from 'cc';
const { ccclass, property } = _decorator;

/**
 * IceFloor
 * 2D 物理用の「氷の床」コンポーネント。
 * このノードの Collider2D (sensor) の中に入ったオブジェクトの
 * PhysicsMaterial の friction(と任意で restitution)を一時的に変更し、
 * エリアから出たら元に戻します。
 */
@ccclass('IceFloor')
export class IceFloor extends Component {

    @property({
        tooltip: '氷エリア内で適用する摩擦値(0 に近いほどツルツル滑ります)。\n例: 0.0~0.1 程度。',
    })
    public iceFriction: number = 0.02;

    @property({
        tooltip: '氷エリア内で反発係数(restitution)も変更するかどうか。',
    })
    public modifyRestitution: boolean = false;

    @property({
        tooltip: 'modifyRestitution が true の場合に適用する反発係数(0~1)。\n0 に近いほど弾まず、1 に近いほどよく弾みます。',
        visible: function (this: IceFloor) {
            return this.modifyRestitution;
        }
    })
    public iceRestitution: number = 0.0;

    @property({
        tooltip: '氷効果を適用する対象のグループ名。\n空の場合はすべてのグループに適用します。\n例: "player", "enemy" など。',
    })
    public targetGroupName: string = '';

    @property({
        tooltip: '侵入・離脱時のデバッグログを出力するかどうか。',
    })
    public debugLog: boolean = false;

    /** このノードの Collider2D(トリガー) */
    private _collider: Collider2D | null = null;

    /**
     * 接触している Collider ごとの元のマテリアル値を保存するマップ。
     * key: 接触している Collider2D
     * value: { friction, restitution }
     */
    private _originalMaterialMap: Map<Collider2D, { friction: number; restitution: number }> = new Map();

    onLoad() {
        // 自ノードの Collider2D を取得
        this._collider = this.getComponent(Collider2D);
        if (!this._collider) {
            console.error('[IceFloor] このコンポーネントを使用するには、同じノードに Collider2D を追加してください。');
            return;
        }

        // Collider2D をセンサー(トリガー)として扱う
        if (!this._collider.sensor) {
            console.warn('[IceFloor] Collider2D が sensor=false でした。氷エリアとして機能させるため、自動的に sensor=true に変更します。');
            this._collider.sensor = true;
        }

        // 接触イベントの登録
        this._collider.on('onBeginContact', this._onBeginContact, this);
        this._collider.on('onEndContact', this._onEndContact, this);
        this._collider.on('onDestroy', this._onColliderDestroy, this);
    }

    onDestroy() {
        // イベント登録の解除(安全のため)
        if (this._collider) {
            this._collider.off('onBeginContact', this._onBeginContact, this);
            this._collider.off('onEndContact', this._onEndContact, this);
            this._collider.off('onDestroy', this._onColliderDestroy, this);
        }
        // 念のため、残っているコライダーのマテリアルを元に戻す
        this._restoreAllMaterials();
    }

    /**
     * 接触開始時のコールバック
     */
    private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        // 自分のコライダーでなければ無視(通常は不要だが防御的に)
        if (selfCollider !== this._collider) {
            return;
        }

        if (!this._shouldAffectCollider(otherCollider)) {
            return;
        }

        this._applyIceToCollider(otherCollider);
    }

    /**
     * 接触終了時のコールバック
     */
    private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
        if (selfCollider !== this._collider) {
            return;
        }

        if (!this._shouldAffectCollider(otherCollider)) {
            return;
        }

        this._restoreMaterialForCollider(otherCollider);
    }

    /**
     * Collider 自体が破棄されたときのハンドラ。
     * 破棄された Collider に対するマップエントリをクリーンアップする。
     */
    private _onColliderDestroy(collider: Collider2D) {
        if (this._originalMaterialMap.has(collider)) {
            this._originalMaterialMap.delete(collider);
        }
    }

    /**
     * 対象コライダーに氷効果を適用するかどうか判定。
     */
    private _shouldAffectCollider(otherCollider: Collider2D): boolean {
        const otherNode = otherCollider.node;
        if (!otherNode) {
            return false;
        }

        // グループ名によるフィルタリング
        if (this.targetGroupName && otherNode.group !== this.targetGroupName) {
            return false;
        }

        return true;
    }

    /**
     * 指定した Collider2D に氷エリアのマテリアル変更を適用する。
     */
    private _applyIceToCollider(otherCollider: Collider2D) {
        if (!otherCollider) {
            return;
        }

        // すでに記録済みなら二重適用を避ける
        if (this._originalMaterialMap.has(otherCollider)) {
            return;
        }

        // Collider に設定されている PhysicsMaterial を取得
        let mat: PhysicsMaterial | null = otherCollider.sharedMaterial;
        if (!mat) {
            // もし null なら、新しいマテリアルを作って割り当てる
            mat = new PhysicsMaterial();
            otherCollider.sharedMaterial = mat;
        }

        // 元の値を保存
        const original = {
            friction: mat.friction,
            restitution: mat.restitution,
        };
        this._originalMaterialMap.set(otherCollider, original);

        // 氷エリア用の値を適用
        mat.friction = this.iceFriction;
        if (this.modifyRestitution) {
            mat.restitution = this.iceRestitution;
        }

        if (this.debugLog) {
            console.log(`[IceFloor] Apply ice to "${otherCollider.node?.name}" (group: ${otherCollider.node?.group}) - friction: ${original.friction} -> ${mat.friction}` +
                (this.modifyRestitution ? `, restitution: ${original.restitution} -> ${mat.restitution}` : ''));
        }
    }

    /**
     * 氷エリアから出た Collider2D のマテリアルを元に戻す。
     */
    private _restoreMaterialForCollider(otherCollider: Collider2D) {
        const original = this._originalMaterialMap.get(otherCollider);
        if (!original) {
            return;
        }

        const mat = otherCollider.sharedMaterial;
        if (mat) {
            mat.friction = original.friction;
            mat.restitution = original.restitution;
        }

        if (this.debugLog) {
            console.log(`[IceFloor] Restore material for "${otherCollider.node?.name}" - friction: ${mat?.friction}, restitution: ${mat?.restitution}`);
        }

        this._originalMaterialMap.delete(otherCollider);
    }

    /**
     * IceFloor が破棄されるときなどに、まだエリア内にいる可能性のある
     * Collider すべてのマテリアルを復元する。
     */
    private _restoreAllMaterials() {
        this._originalMaterialMap.forEach((original, collider) => {
            const mat = collider.sharedMaterial;
            if (mat) {
                mat.friction = original.friction;
                mat.restitution = original.restitution;
            }
        });
        this._originalMaterialMap.clear();
    }
}

コードのポイント解説

  • onLoad
    • 自ノードの Collider2D を取得し、存在しなければエラーログ。
    • sensor が false(通常の当たり判定)なら、警告を出しつつ true に変更。
    • onBeginContact / onEndContact などのコールバックを登録。
  • _onBeginContact
    • 氷エリアに入ったときに呼ばれます。
    • _shouldAffectCollider でグループ名フィルタを通過したコライダーに対して _applyIceToCollider を呼び出します。
  • _applyIceToCollider
    • 対象 Collider2DsharedMaterial を取得し、なければ新規作成。
    • 現在の friction / restitution_originalMaterialMap に保存。
    • iceFrictioniceRestitution(必要なら)を上書き。
  • _onEndContact
    • 氷エリアから出たときに呼ばれます。
    • _restoreMaterialForCollider で保存しておいた元の値を復元します。
  • onDestroy
    • イベントを解除し、まだエリア内にいる可能性のあるすべてのコライダーのマテリアルを復元してからマップをクリアします。

使用手順と動作確認

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

  1. Assets パネルで右クリックします。
  2. Create > TypeScript を選択します。
  3. ファイル名を IceFloor.ts にします。
  4. 作成された IceFloor.ts をダブルクリックし、エディタで開きます。
  5. 中身をすべて削除し、本記事の「TypeScriptコードの実装」のコードをそのまま貼り付けて保存します。

2. 氷エリア用ノードの作成

  1. Hierarchy パネルで右クリックし、Create > 2D Object > Node を選択して新しいノードを作成します。
  2. ノード名を IceFloorArea など分かりやすい名前に変更します。
  3. このノードに Collider2D を追加します:
    1. IceFloorArea ノードを選択し、Inspector の Add Component ボタンをクリック。
    2. Physics2D > BoxCollider2D(または CircleCollider2D, PolygonCollider2D など)を選択します。
    3. 氷エリアの形状・サイズに合わせて SizeOffset を調整します。
    4. Is Trigger / Sensor にチェックを入れてトリガーにしておきます(スクリプト側でも自動で sensor=true にしますが、見た目上も分かりやすくするため)。

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

  1. IceFloorArea ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
  2. Custom カテゴリ内から IceFloor を選択して追加します。
  3. Inspector に IceFloor コンポーネントのプロパティが表示されます。

4. プロパティの設定例

典型的な「ツルツルな氷床」の設定例:

  • Ice Friction (iceFriction): 0.02
  • Modify Restitution (modifyRestitution): false
  • Ice Restitution (iceRestitution): (modifyRestitution が true の場合のみ)0.0
  • Target Group Name (targetGroupName):
    • 全てのオブジェクトに氷効果を適用したい場合: 空のまま。
    • プレイヤーだけに適用したい場合: ゲーム側でプレイヤーノードのグループを player に設定し、ここに player と入力。
  • Debug Log (debugLog): 動作確認中は true、リリース時は false 推奨。

5. テスト用プレイヤーの準備(簡易例)

すでに 2D 物理で動くプレイヤー(RigidBody2D + Collider2D)がいる場合はそのまま使えます。
ここでは簡単なテスト用オブジェクトの作り方も記載しておきます。

  1. Hierarchy で右クリック → Create > 2D Object > Sprite を選択し、Player ノードを作成します。
  2. Player ノードを選択し、Inspector の Add Component から:
    • Physics2D > RigidBody2D を追加。
    • Physics2D > BoxCollider2D を追加。
  3. RigidBody2DTypeDynamic に設定します。
  4. BoxCollider2D に適当な Size を設定します。
  5. Player ノードのグループを必要に応じて player などに変更します(上で targetGroupName を使う場合)。

6. 実際の動作確認

  1. Scene ビューで IceFloorArea の位置とサイズを調整し、プレイヤーがその上を通るように配置します。
  2. Physics 2D が有効になっていることを確認します:
    • メニューから Project > Project Settings > Feature Cropping > Physics 2D が有効であること。
  3. 再生ボタン(▶)を押してゲームをプレビューします。
  4. プレイヤーに初速を与える(キー入力スクリプトがある場合)か、単純に高い位置から落とすなどして、IceFloorArea 上を通過させます。
  5. 通常の床上ではすぐに止まるのに対し、IceFloorArea 上では摩擦が小さいため、慣性で長く滑り続けるようになっていれば成功です。
  6. debugLog を true にしていれば、コンソールに以下のようなログが出力されます:
    • [IceFloor] Apply ice to "Player" (group: player) - friction: 0.3 -> 0.02
    • [IceFloor] Restore material for "Player" - friction: 0.3, restitution: 0

まとめ

この IceFloor コンポーネントは、

  • 他のカスタムスクリプトに一切依存せず、
  • Collider2D を持つ任意のノードにアタッチするだけで、
  • そのエリア内に入った物理オブジェクトの摩擦を自動で下げ、出たら元に戻す

という、汎用性の高い「氷の床」機能を提供します。

応用例:

  • 氷ステージの床や、油で濡れた床、ローラーコンベアの低摩擦部分など、滑りやすいエリアの表現。
  • targetGroupName を活用して、プレイヤーだけ・敵だけ・特定のギミックだけに影響させる設計。
  • modifyRestitution を ON にし、氷の上では弾みにくくする/逆にスライダーのように弾ませるなど、演出のバリエーション。

このように、物理マテリアルの変更と復元をコンポーネント内で完結させておくことで、シーン内のどの場所にも簡単に「氷エリア」を配置でき、ゲーム全体の開発効率を大きく向上させられます。

あとはシーンに必要な数だけ IceFloor ノードを配置し、サイズとプロパティを調整するだけで、さまざまな滑りやすさのエリアを柔軟に作り分けることができます。

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をコピーしました!