【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の接触を床とみなす。 - 坂道を床として認識したい場合は値を下げる、垂直な壁を誤認識したくない場合は上げる。
- ツールチップ例:
内部実装のポイント
- 必須コンポーネントの取得
RigidBody2DとCollider2DをonLoad()でgetComponentし、見つからない場合はerror()ログを出す。- どちらか一方でも欠けている場合は、このコンポーネントの機能を無効化する。
- 床接地判定
Collider2Dのonメソッドで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にクランプ。
- 摩擦量
- 計算した
vxをbody.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()
RigidBody2DとCollider2Dを取得し、存在しない場合は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. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
assets/scripts)を選択します。 - そのフォルダ上で右クリックし、Create → TypeScript を選択します。
- 新しく作成されたスクリプトの名前を
SurfaceFriction.tsに変更します。 - ダブルクリックして開き、既存の中身を全て削除して、前章で示した
SurfaceFrictionのコードをそのまま貼り付けて保存します。
2. テスト用ノード(キャラクター)の準備
- Hierarchy パネルで右クリックし、Create → 2D Object → Sprite などからテスト用のノードを作成します。
- 名前は
PlayerやTestBodyなど、分かりやすいもので構いません。
- 名前は
- 作成したノードを選択し、Inspector パネルで以下のコンポーネントを追加します:
- Add Component → Physics 2D → RigidBody2D
- Add Component → Physics 2D → BoxCollider2D(または用途に応じた Collider2D)
RigidBody2Dの設定例:- Type: Dynamic
- Gravity Scale: 1(通常の重力)
- 他はデフォルトのままで構いません。
3. 床(Ground)ノードの準備
- Hierarchy で右クリック → Create → 2D Object → Sprite などから床用ノードを作成し、
Groundと命名します。 - Inspector で
Groundの Transform を調整し、横長の床になるようにスケール・位置を変更します。 Groundに以下のコンポーネントを追加します:- Add Component → Physics 2D → RigidBody2D
- Add Component → Physics 2D → BoxCollider2D
GroundのRigidBody2D設定:- Type: Static(床は動かないので Static にします)
BoxCollider2Dのサイズを床の見た目に合わせて調整します。
4. SurfaceFriction コンポーネントをアタッチ
- Hierarchy で先ほどのキャラクターノード(例:
Player)を選択します。 - Inspector の下部にある Add Component ボタンをクリックします。
- Custom カテゴリの中から
SurfaceFrictionを選択してアタッチします。
5. プロパティの設定例
SurfaceFriction のプロパティを次のように設定してみましょう:
- Enabled On Start: チェック(true)
- Friction Strength:
5〜10(大きいほど早く止まる) - Min Speed Threshold:
0.05 - Apply Only When Grounded: チェック(true)
- Ground Normal Y Threshold:
0.5(標準的な床判定)
6. 動作確認
- シーン上で
PlayerがGroundの上に落ちてくるように、Playerの Y 座標を高めに設定します。 - 再生ボタン(▶) を押してゲームを実行します。
- 実行中に、以下のいずれかの方法で
Playerに横方向の速度を与えます:- 一時的に
PlayerのRigidBody2Dに Linear Velocity を設定しておき、再生直後に動くようにする。 - または、簡単な一時スクリプトで
start()内からbody.linearVelocity = new Vec2(5, 0);のように設定する。
- 一時的に
Playerが床の上を滑り始めますが、徐々に速度が落ちて最終的に止まることを確認します。Apply Only When Groundedを オフ にして再実行すると、空中にいる間も横速度が減衰する挙動になることも確認できます。
もしコンソールに
[SurfaceFriction] RigidBody2D がノードにアタッチされていません...
[SurfaceFriction] Collider2D がノードにアタッチされていません...
といったエラーが表示される場合は、対象ノードに RigidBody2D と Collider2D の両方が正しく追加されているかを確認してください。
まとめ
この SurfaceFriction コンポーネントを使うことで、
- プレイヤーや敵キャラに「床の上だけ働く摩擦」を簡単に追加できる。
frictionStrengthとminSpeedThresholdを調整するだけで、「キビキビ止まる」・「スーッと滑る」などの感触を素早くチューニングできる。- 物理マテリアルや他の管理スクリプトに依存せず、このスクリプト単体をアタッチするだけで機能する。
応用例としては:
- 氷のステージでは
frictionStrengthを小さくして「よく滑る床」を表現。 - 泥や砂地などのステージでは
frictionStrengthを大きくし、minSpeedThresholdもやや大きめにして「ピタッと止まる」感触に。 - 特定のギミック(油まみれの床など)に乗ったときだけ、一時的に
disableFriction()を呼んで滑りやすくする。
このように、SurfaceFriction は単体で完結しつつ、インスペクタから直感的に調整しやすい汎用コンポーネントとして、2D アクションやプラットフォーマー制作の効率を大きく高めてくれます。必要なノードにどんどんアタッチして、ゲーム全体の操作感を統一・チューニングしてみてください。




