【Cocos Creator 3.8】WaterBuoyancy の実装:アタッチするだけで「水中の物体をふわっと浮かせる」汎用スクリプト
このガイドでは、2D/3D問わず「水面エリア」にアタッチするだけで、その中に入った物体を上方向に押し上げる 浮力コンポーネント WaterBuoyancy を実装します。
水面ノードにこのスクリプトを付けておけば、プレイヤーや箱などが水に入ったときに自動で浮き上がる挙動を再現できます。
外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結し、必要な調整はすべてインスペクタのプロパティから行えるように設計します。
コンポーネントの設計方針
1. 想定する利用シーン
- 水面を表すノード(2D なら BoxCollider2D 付きの四角、水たまりなど)にアタッチ。
- その水エリア内に入った RigidBody2D(プレイヤー、箱、敵など)に対して、上方向の力を加えて疑似的な浮力を表現。
- オプションで「水の抵抗(減速)」や「重さによる浮きやすさの変化」も扱えるようにする。
2. 実装の基本方針
- WaterBuoyancy は「水エリア」に付けるコンポーネントとする。
- 水エリアのコライダーに isTrigger(Sensor) を設定し、
onBeginContact / onEndContactで水中に入った/出た剛体を管理。 update()で水中にいる各 RigidBody2D に対して、上向きの力を毎フレーム加える。- 外部スクリプトには一切依存せず、必要なコンポーネント(Collider2D など)は自前で取得&チェックして防御的に実装。
3. インスペクタで設定可能なプロパティ設計
WaterBuoyancy コンポーネントでインスペクタから調整できるプロパティと役割は次の通りです。
- buoyancyForce (number)
・水中にいる剛体に対して、毎フレーム上向きに加える力の強さ。
・値が大きいほど強く浮き上がる。
・2D 物理の場合、だいたい50 ~ 500程度から調整開始するのが目安。 - useMassFactor (boolean)
・剛体の質量に応じて浮力をスケーリングするかどうか。
・ON の場合:実際に加える力 = buoyancyForce × body.mass
・OFF の場合:すべての剛体に同じbuoyancyForceを適用。 - dragInWater (number)
・水中にいる剛体の 速度を減衰させる係数。
・0なら減衰なし、0.1 ~ 5.0くらいの範囲で調整。
・値が大きいほど水の抵抗が強くなり、動きがねっとりと遅くなる。 - affectVerticalOnly (boolean)
・水の抵抗(dragInWater)を 縦方向の速度だけに適用するかどうか。
・ON の場合:上下方向の速度のみ減衰し、左右の動きはそのまま。
・OFF の場合:速度ベクトル全体を減衰。 - maxUpwardSpeed (number)
・水中での 最大上昇速度の上限。
・これを超えて上向きに加速しないようにして、不自然な高速浮上を防ぐ。
・0以下にすると、この制限は無効。 - debugLog (boolean)
・ON にすると、水中に入った/出た剛体や、コンポーネント取得失敗などをログ出力。
・動作確認やデバッグ時に有効にし、最終的なビルドでは OFF 推奨。
また、コンポーネントが正しく動作するために、以下の標準コンポーネントが必要です。
- Collider2D(BoxCollider2D など)
・WaterBuoyancy をアタッチしたノードに追加し、isTrigger = true に設定。
・このコライダーの範囲が「水エリア」として扱われる。
これらはコード内で getComponent を使って取得し、存在しない場合は error ログで警告します。
TypeScriptコードの実装
以下が完成した WaterBuoyancy コンポーネントの全コードです。
import { _decorator, Component, Node, Collider2D, IPhysics2DContact, RigidBody2D, Vec2, ERigidBody2DType, log, error } from 'cc';
const { ccclass, property } = _decorator;
/**
* WaterBuoyancy
* 水エリアに入った RigidBody2D に対して、上向きの力と水の抵抗を与える汎用コンポーネント。
* このコンポーネントは「水面(または水エリア)」のノードにアタッチして使用します。
*/
@ccclass('WaterBuoyancy')
export class WaterBuoyancy extends Component {
@property({
tooltip: '水中にいる剛体に毎フレーム加える上向きの力の基本値。\n値が大きいほど強く浮き上がります。'
})
public buoyancyForce: number = 200;
@property({
tooltip: '剛体の質量に応じて浮力をスケーリングするかどうか。\nON の場合: 実際の力 = buoyancyForce × 質量。'
})
public useMassFactor: boolean = true;
@property({
tooltip: '水の抵抗の強さ。0 で無効。\n値が大きいほど水中での速度が早く減衰します。'
})
public dragInWater: number = 1.0;
@property({
tooltip: '水の抵抗を上下方向の速度のみに適用するかどうか。\nON: 縦方向のみ減衰 / OFF: 速度ベクトル全体を減衰。'
})
public affectVerticalOnly: boolean = true;
@property({
tooltip: '水中での最大上昇速度の上限(m/s)。\n0 以下の場合、この制限は無効になります。'
})
public maxUpwardSpeed: number = 5.0;
@property({
tooltip: 'デバッグログを有効にするかどうか。\nON にすると、水中への出入りやエラー情報をコンソールに表示します。'
})
public debugLog: boolean = false;
// 水エリアに現在浸かっている剛体のリスト
private _bodiesInWater: Set<RigidBody2D> = new Set<RigidBody2D>();
// 自身の Collider2D 参照
private _collider: Collider2D | null = null;
onLoad() {
// 自身の Collider2D を取得
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
error('[WaterBuoyancy] このノードには Collider2D (BoxCollider2D など) が必要です。追加してから使用してください。 Node:', this.node.name);
return;
}
// Trigger(Sensor)として動作させる
this._collider.sensor = true;
// 衝突コールバックの登録
this._collider.on('onBeginContact', this._onBeginContact, this);
this._collider.on('onEndContact', this._onEndContact, this);
if (this.debugLog) {
log('[WaterBuoyancy] onLoad 完了。Node:', this.node.name);
}
}
onDestroy() {
// イベント登録解除
if (this._collider) {
this._collider.off('onBeginContact', this._onBeginContact, this);
this._collider.off('onEndContact', this._onEndContact, this);
}
this._bodiesInWater.clear();
}
/**
* あるコライダーが水エリアに入ったときに呼ばれる。
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const body = otherCollider.getComponent(RigidBody2D);
if (!body) {
// 剛体を持たないオブジェクトは無視
return;
}
// 静的ボディなどは通常浮力の対象外にする
if (body.type === ERigidBody2DType.Static) {
return;
}
this._bodiesInWater.add(body);
if (this.debugLog) {
log('[WaterBuoyancy] 物体が水中に入りました:', body.node.name);
}
}
/**
* あるコライダーが水エリアから出たときに呼ばれる。
*/
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const body = otherCollider.getComponent(RigidBody2D);
if (!body) {
return;
}
if (this._bodiesInWater.has(body)) {
this._bodiesInWater.delete(body);
if (this.debugLog) {
log('[WaterBuoyancy] 物体が水中から出ました:', body.node.name);
}
}
}
/**
* 毎フレーム呼ばれ、水中にいる剛体に浮力と水の抵抗を与える。
* @param dt フレームの経過時間(秒)
*/
update(dt: number) {
if (this._bodiesInWater.size === 0) {
return;
}
// 各剛体に対して処理
this._bodiesInWater.forEach((body) => {
// 剛体が破棄されている可能性もあるのでチェック
if (!body.node || !body.enabledInHierarchy) {
return;
}
// 現在の速度
const velocity = body.linearVelocity;
// --- 水の抵抗(減速) ---
if (this.dragInWater > 0) {
if (this.affectVerticalOnly) {
// 縦方向のみ減衰
const vy = velocity.y;
const dragAmount = this.dragInWater * dt;
// 線形補間でゆっくり 0 に近づけるイメージ
const newVy = vy - vy * dragAmount;
velocity.y = newVy;
} else {
// ベクトル全体を減衰
const dragAmount = this.dragInWater * dt;
velocity.x = velocity.x - velocity.x * dragAmount;
velocity.y = velocity.y - velocity.y * dragAmount;
}
}
// --- 浮力(上向きの力) ---
let forceMagnitude = this.buoyancyForce;
if (this.useMassFactor) {
forceMagnitude *= body.mass;
}
// 上向きの力を加える
const force = new Vec2(0, forceMagnitude);
body.applyForceToCenter(force, true);
// --- 最大上昇速度の制限 ---
if (this.maxUpwardSpeed > 0) {
if (velocity.y > this.maxUpwardSpeed) {
velocity.y = this.maxUpwardSpeed;
}
}
// 変更した速度を剛体に反映
body.linearVelocity = velocity;
});
}
}
コードのポイント解説
- onLoad()
・WaterBuoyancy がアタッチされたノードからCollider2Dを取得。
・取得できなければerrorログを出して終了(防御的実装)。
・コライダーを sensor = true に設定し、onBeginContactとonEndContactを登録します。 - _onBeginContact / _onEndContact()
・水エリアに入った/出たコライダーからRigidBody2Dを取得。
・静的剛体(Static)は浮力対象外として無視。
・対象のRigidBody2DをSet<RigidBody2D>で管理します。 - update(dt)
・現在水中にいるすべての剛体に対して処理。
・まず dragInWater に基づき速度を減衰。
・次に buoyancyForce(必要に応じて質量でスケーリング)を上向きに加える。
・最後に maxUpwardSpeed で上昇速度の上限を制限し、不自然な高速浮上を防ぎます。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで任意のフォルダを右クリックします。
- Create → TypeScript を選択し、ファイル名を
WaterBuoyancy.tsにします。 - 自動生成されたスクリプトをダブルクリックで開き、中身をすべて削除して、前述のコードをそのまま貼り付けて保存します。
2. 水エリア用ノードの作成(2Dの例)
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を
WaterAreaなどに変更します。 - Inspector で
Spriteの SpriteFrame に水っぽい画像を設定するか、単色で青にして水を表現します。 - 同じく Inspector で Add Component → Physics 2D → BoxCollider2D を追加します。
Sizeを調整し、水エリアの大きさに合わせます。- Sensor チェックボックスを ON にして、「当たり判定のみ」のトリガーにします。
※ スクリプト側でもsensor = trueを設定しますが、エディタ上で明示しておくと分かりやすいです。
3. WaterBuoyancy コンポーネントのアタッチ
- WaterArea ノードを選択します。
- Inspector の下部で Add Component → Custom → WaterBuoyancy を選択します。
- 追加された WaterBuoyancy の各プロパティを設定します。
- Buoyancy Force:
200(まずは標準値でOK) - Use Mass Factor: ON(質量によって浮きやすさを変えたい場合)
- Drag In Water:
1.0(水の抵抗を適度に) - Affect Vertical Only: ON(上下方向のみ抵抗をかける)
- Max Upward Speed:
5.0(上昇速度の上限) - Debug Log: 動作確認中は ON、本番では OFF 推奨
- Buoyancy Force:
4. 浮かぶオブジェクト(剛体)の作成
次に、水に浮かせたいオブジェクトを用意します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を
FloatingBoxにします。 - Sprite の見た目を箱やキャラクターなど、好きな画像に設定します。
- Add Component → Physics 2D → RigidBody2D を追加します。
Type: Dynamic に設定(動く剛体)。Gravity Scale: 1.0(標準の重力)でまずは試す。- 必要に応じて
MassやLinear Dampingを調整。
- Add Component → Physics 2D → BoxCollider2D を追加し、Sprite に合わせてサイズを調整します。
- FloatingBox ノードを、WaterArea の少し上に配置しておきます(ゲームビューで水の上に見える位置)。
5. 再生して動作確認
- エディタ上部の Play(▶) ボタンを押してゲームを再生します。
- FloatingBox が重力で落下し、水エリアに入った瞬間から
- 落下速度が弱まり、
- やがて上方向に押し上げられて、ふわっと浮き上がる
挙動が確認できれば成功です。
- Debug Log を ON にしている場合は、Console に
物体が水中に入りました: FloatingBox物体が水中から出ました: FloatingBox
といったログが出力されることも確認できます。
6. 調整のコツ
- 浮力が弱すぎる/強すぎる
・Buoyancy Forceを上下させて調整します。
・質量の大きいオブジェクトが沈みすぎる場合は、Use Mass Factorを OFF にして一律の力にするのも手です。 - 水中での動きが速すぎる/遅すぎる
・Drag In Waterを上げると水の抵抗が強くなり、動きが重くなります。
・Affect Vertical Onlyを OFF にすると、左右の動きも水の抵抗を受けてゆっくりになります。 - 上に吹き飛びすぎる
・Max Upward Speedを小さめ(例: 2.0 ~ 3.0)に設定することで、浮上速度を抑えられます。
まとめ
この WaterBuoyancy コンポーネントは、
- 水エリアのノードにアタッチするだけで、
- そこに入った任意の RigidBody2D を自動で「浮かせる」
という、汎用性の高い浮力システムを実現します。
特徴として:
- 完全に独立したコンポーネントであり、外部の GameManager やシングルトンに依存しない。
- 必要な設定はすべてインスペクタから行えるため、デザイナーやレベルエディタでも簡単に調整可能。
- 浮力の強さ・水の抵抗・最大上昇速度などを細かく調整でき、プールのような弱い浮力から、強い噴水のような押し上げまで幅広く表現できる。
応用例としては、
- マップ上に複数の水エリアを置き、それぞれ違う浮力パラメータを設定して「浅瀬」「深い池」「激流」などを表現する。
- 毒の沼エリアなどで、浮力は弱く、代わりに別スクリプトでダメージを与えるなど、他のギミックと組み合わせる。
- 一部だけ
Buoyancy Forceを負の値にして「沈むエリア」「強いダウンフォース」を作る。
このように、WaterBuoyancy を一度プロジェクトに組み込んでおけば、水関係のギミックを素早く量産できる再利用可能コンポーネントとして、ゲーム開発の効率を大きく向上させられます。




