【Cocos Creator 3.8】WaterBuoyancy の実装:アタッチするだけで「水面でぷかぷか浮く挙動」を実現する汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「水中で浮力が働き、上下にゆらゆらする」挙動を実現できる WaterBuoyancy コンポーネントを実装します。
2D/3D を問わず、「水に浮かぶオブジェクト」「水中に沈みかけるオブジェクト」などを表現したいときに、そのノードにこのスクリプトを付けて設定するだけで利用できるように設計します。
コンポーネントの設計方針
機能要件の整理
- 親ノード(このコンポーネントを付けたノード)が「水エリア」に入っている間だけ、上向きの力(浮力)を加える。
- 水エリアの判定は「Y 座標が水面の高さより下かどうか」で行う(=水面の高さを数値で指定)。
- 水中にいる間は、浮力だけでなく「減衰(抵抗)」を加え、ゆっくり落下・ゆっくり上昇するようにする。
- 軽く上下にゆらゆらする「波」のような動きをオプションで付けられるようにする。
- 2D 物理(
RigidBody2D)と 3D 物理(RigidBody)のどちらにも対応する。 - 物理剛体が付いていない場合は、Transform(位置)を直接動かすモードでも動作する。
- 外部のスクリプトやシングルトンには一切依存しない。すべてインスペクタの設定だけで完結する。
設計アプローチ
WaterBuoyancy は、フレーム毎に以下の処理を行います。
- 現在の Y 座標と、設定された水面高さ
waterLevelYを比較する。 - Y < waterLevelY なら「水中」とみなし、浮力・減衰・波動を計算する。
- 物理剛体がある場合は
applyForceToCenter/applyForceで力として適用。 - 剛体がない場合は、擬似速度を内部で管理し、Y 座標を直接更新する。
- 水上(Y >= waterLevelY)の場合は、オプションで「水から出たときに速度をクリアする」などの制御を行う。
インスペクタで設定可能なプロパティ
以下のプロパティを @property で公開し、インスペクタから調整できるようにします。
waterLevelY: number- 水面の Y 座標(ワールド座標)を指定します。
- この値より下にノードの位置があると「水中」と判定されます。
- 例:2D ゲームで水面が画面中央にあるなら 0 付近など。
buoyancyStrength: number- 浮力の強さ。大きいほど強く上向きに押し上げます。
- 物理モードでは「力の大きさ」、Transform モードでは「加速度」のように扱います。
- 例:2D の軽いオブジェクトなら 5〜20 程度から調整。
damping: number- 水中での減衰率(抵抗)。0〜1 の範囲を想定。
- 1 に近いほど素早く速度が減衰し、0 に近いほど減衰が弱くなります。
- 例:0.1〜0.3 くらいから試すと自然な感じになりやすいです。
maxUpwardSpeed: number- Transform モードでの最大上昇速度の制限値。
- 0 以下なら制限しない。
useWaveMotion: boolean- 水面でのゆらゆら(波)を有効にするかどうか。
- 有効にすると、浮力に加えて小さな上下のサイン波が加算されます。
waveAmplitude: number- 波の振幅(どれくらい上下に揺れるか)。
useWaveMotionが true のときのみ使用。
waveFrequency: number- 波の周波数(揺れる速さ、Hz)。
- 1 なら 1 秒に 1 往復程度のイメージ。
useWorldSpace: boolean- Y 座標の判定と移動を「ワールド座標」で行うか、「ローカル座標」で行うか。
- 通常は true(ワールド座標)で問題ありません。
enable2DPhysics: boolean- 2D 物理(
RigidBody2D)を使用するかどうか。 - 有効にすると、ノードの
RigidBody2Dを取得して力を加えます。
- 2D 物理(
enable3DPhysics: boolean- 3D 物理(
RigidBody)を使用するかどうか。 - 有効にすると、ノードの
RigidBodyを取得して力を加えます。
- 3D 物理(
disableVelocityOnExit: boolean- 水から出た瞬間に、上向きの速度をゼロにするかどうか。
- 水面から飛び出しすぎるのを防ぎたい場合に true にします。
防御的な実装として、
enable2DPhysicsが true だがRigidBody2Dが見つからない場合は警告ログを出し、Transform モードにフォールバック。enable3DPhysicsが true だがRigidBodyが見つからない場合も同様に警告ログ。- 2D/3D 物理が両方とも無効、または見つからない場合は Transform モードで動作。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, RigidBody, RigidBody2D, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* WaterBuoyancy
* 水面より下にある間、上方向の力を加えて浮力を表現するコンポーネント。
* - 2D (RigidBody2D) / 3D (RigidBody) / Transform 直操作の3モード対応
* - 水面高さは Y 座標で指定
* - 減衰・波動(サイン波)も付与可能
*/
@ccclass('WaterBuoyancy')
export class WaterBuoyancy extends Component {
@property({
tooltip: '水面の高さ(Y座標, ワールド基準)。\nこの値より下にあると水中と判定されます。'
})
public waterLevelY: number = 0;
@property({
tooltip: '浮力の強さ。大きいほど強く上向きに押し上げます。\n2D/3D物理では力の大きさ、Transformモードでは加速度として扱われます。'
})
public buoyancyStrength: number = 10;
@property({
tooltip: '水中での減衰率(0〜1推奨)。\n1に近いほど速度が素早く減衰します。'
})
public damping: number = 0.2;
@property({
tooltip: 'Transformモード時の最大上昇速度。0以下なら無制限。'
})
public maxUpwardSpeed: number = 5;
@property({
tooltip: '水面でのゆらゆら(波)を有効にするかどうか。'
})
public useWaveMotion: boolean = true;
@property({
tooltip: '波の振幅。大きいほど上下の揺れが大きくなります。'
})
public waveAmplitude: number = 0.2;
@property({
tooltip: '波の周波数(Hz)。1なら1秒に1往復程度の揺れになります。'
})
public waveFrequency: number = 1.0;
@property({
tooltip: 'ワールド座標でY位置を判定/操作するかどうか。\n通常はtrueのままで問題ありません。'
})
public useWorldSpace: boolean = true;
@property({
tooltip: '2D物理(RigidBody2D)を使用して浮力を加えるかどうか。'
})
public enable2DPhysics: boolean = false;
@property({
tooltip: '3D物理(RigidBody)を使用して浮力を加えるかどうか。'
})
public enable3DPhysics: boolean = false;
@property({
tooltip: '水から出たときに上向き速度をゼロにするかどうか。\n水面から飛び出しすぎるのを防ぎたい場合に有効。'
})
public disableVelocityOnExit: boolean = true;
// 内部状態
private _rb2d: RigidBody2D | null = null;
private _rb3d: RigidBody | null = null;
private _inWater: boolean = false;
private _time: number = 0;
// Transformモード用の擬似速度
private _velocityY: number = 0;
onLoad() {
// 2D/3D物理コンポーネントの取得を試みる
if (this.enable2DPhysics) {
this._rb2d = this.getComponent(RigidBody2D);
if (!this._rb2d) {
console.warn('[WaterBuoyancy] enable2DPhysicsがtrueですが、RigidBody2Dが見つかりません。Transformモードで動作します。', this.node.name);
}
}
if (this.enable3DPhysics) {
this._rb3d = this.getComponent(RigidBody);
if (!this._rb3d) {
console.warn('[WaterBuoyancy] enable3DPhysicsがtrueですが、RigidBodyが見つかりません。Transformモードで動作します。', this.node.name);
}
}
if (!this.enable2DPhysics && !this.enable3DPhysics) {
console.log('[WaterBuoyancy] 物理モードが無効のため、Transformモードで動作します。', this.node.name);
}
}
start() {
// 初期位置から水中かどうかを判定
const y = this._getCurrentY();
this._inWater = y < this.waterLevelY;
}
update(deltaTime: number) {
this._time += deltaTime;
const currentY = this._getCurrentY();
const wasInWater = this._inWater;
const nowInWater = currentY < this.waterLevelY;
this._inWater = nowInWater;
// 水から出た瞬間の処理
if (wasInWater && !nowInWater) {
this._onExitWater();
}
if (!nowInWater) {
// 水上では何もしない
return;
}
// 水中での浮力計算
this._applyBuoyancy(deltaTime, currentY);
}
/**
* 現在のY座標を取得(ワールド or ローカル)
*/
private _getCurrentY(): number {
if (this.useWorldSpace) {
const worldPos = this.node.worldPosition;
return worldPos.y;
} else {
const localPos = this.node.position;
return localPos.y;
}
}
/**
* Y座標を設定(ワールド or ローカル)
*/
private _setCurrentY(y: number) {
if (this.useWorldSpace) {
const wp = this.node.worldPosition.clone();
wp.y = y;
this.node.setWorldPosition(wp);
} else {
const lp = this.node.position.clone();
lp.y = y;
this.node.setPosition(lp);
}
}
/**
* 水から出たときの処理
*/
private _onExitWater() {
if (!this.disableVelocityOnExit) {
return;
}
// 2D物理
if (this._rb2d) {
const v = this._rb2d.linearVelocity;
if (v && v.y > 0) {
v.y = 0;
this._rb2d.linearVelocity = v;
}
}
// 3D物理
if (this._rb3d) {
const v = this._rb3d.linearVelocity;
if (v.y > 0) {
v.y = 0;
this._rb3d.linearVelocity = v;
}
}
// Transformモード
this._velocityY = Math.min(this._velocityY, 0);
}
/**
* 浮力と減衰、波動を適用
*/
private _applyBuoyancy(deltaTime: number, currentY: number) {
// 深さに応じて浮力を変化させる簡易モデル
// 水面からの距離(正の値)を計算
const depth = this.waterLevelY - currentY; // currentY < waterLevelY のため正
// 深さに比例して浮力を強くする(上限は buoyancyStrength)
const depthFactor = math.clamp(depth, 0, 1);
let force = this.buoyancyStrength * depthFactor;
// 波動(サイン波)を加算(オプション)
if (this.useWaveMotion && this.waveAmplitude !== 0 && this.waveFrequency !== 0) {
const wave = Math.sin(this._time * Math.PI * 2 * this.waveFrequency) * this.waveAmplitude;
force += wave;
}
// 減衰の係数
const dampingFactor = math.clamp01(1 - this.damping);
// 2D物理モード
if (this._rb2d) {
const rb = this._rb2d;
const v = rb.linearVelocity;
// 上向きの力を加える
rb.applyForceToCenter(new Vec3(0, force, 0), true);
// 速度に減衰を適用
if (v) {
v.y *= dampingFactor;
rb.linearVelocity = v;
}
return;
}
// 3D物理モード
if (this._rb3d) {
const rb = this._rb3d;
const v = rb.linearVelocity;
// 上向きの力を加える
rb.applyForce(new Vec3(0, force, 0));
// 速度に減衰を適用
v.y *= dampingFactor;
rb.linearVelocity = v;
return;
}
// Transformモード(擬似物理)
// 加速度として扱い、内部の速度を更新
this._velocityY += force * deltaTime;
this._velocityY *= dampingFactor;
// 最大上昇速度の制限
if (this.maxUpwardSpeed > 0 && this._velocityY > this.maxUpwardSpeed) {
this._velocityY = this.maxUpwardSpeed;
}
const newY = currentY + this._velocityY * deltaTime;
this._setCurrentY(newY);
}
}
コードの要点解説
onLoad()enable2DPhysics/enable3DPhysicsに応じてRigidBody2D/RigidBodyを取得します。- 見つからない場合は
console.warnで警告を出し、Transform モードで動作することを明示します。
start()- 初期位置が水中かどうかを判定し、
_inWaterに保存します。
- 初期位置が水中かどうかを判定し、
update(deltaTime)- 現在の Y 座標を取得し、水中かどうかを判定します。
- 水から出た瞬間(
wasInWater && !nowInWater)なら_onExitWater()を呼び、必要に応じて上向き速度をクリアします。 - 水中であれば
_applyBuoyancy()を呼び、浮力・減衰・波動を適用します。
_applyBuoyancy()- 水面からの深さ
depthを使って、浮力を「深いほど強い」ようにスケーリングしています(簡易モデル)。 - オプションでサイン波(
Math.sin)による波動を加え、自然な揺れを表現します。 - 2D/3D 物理モードでは
applyForceToCenter/applyForceで力を加え、linearVelocity.yに減衰をかけています。 - Transform モードでは内部の
_velocityYを更新し、Y 座標を直接変更します。
- 水面からの深さ
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- 新しく作成されたスクリプトに
WaterBuoyancy.tsという名前を付けます。 - ダブルクリックしてエディタ(VS Code など)で開き、先ほどの TypeScript コードを丸ごと貼り付けて保存します。
2D でのテスト手順(RigidBody2D 利用)
2. テスト用シーンとノードの準備
- Scene パネルで、空のシーンまたはテスト用のシーンを開きます。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、浮かせたいオブジェクト(例:
FloatingBox)を作成します。 - 作成した
FloatingBoxノードを選択し、Inspector で以下を設定します。- Add Component → Physics 2D → RigidBody2D を追加。
- 必要に応じて BoxCollider2D などのコライダーも追加(物理挙動を安定させたい場合)。
3. WaterBuoyancy コンポーネントのアタッチ
FloatingBoxノードを選択した状態で、Inspector の下部にある Add Component ボタンをクリックします。- Custom → WaterBuoyancy を選択して追加します。
- Inspector に表示された WaterBuoyancy のプロパティを次のように設定してみます(例):
- waterLevelY:
0(シーン中央を水面とする場合) - buoyancyStrength:
15 - damping:
0.25 - maxUpwardSpeed:
0(制限なし) - useWaveMotion:
true - waveAmplitude:
0.3 - waveFrequency:
0.8 - useWorldSpace:
true - enable2DPhysics:
true - enable3DPhysics:
false - disableVelocityOnExit:
true
- waterLevelY:
FloatingBoxノードの Y 座標を-2など、水面(0)より下に配置しておきます。
4. 再生して動作を確認
- エディタ上部の Play ボタンを押してゲームを実行します。
FloatingBoxが水面の高さ(Y=0)付近まで上昇し、そこでゆらゆら揺れながら浮かぶ挙動になっていれば成功です。- 浮力が弱いと感じたら buoyancyStrength を大きく、動きが速すぎる場合は damping を大きくして調整します。
Transform モードでのテスト(物理コンポーネントなし)
- Hierarchy で右クリック → Create → 2D Object → Sprite を選び、
FloatingIconなどのノードを作成します。 - このノードには
RigidBody2DやRigidBodyを追加しないでください(Transform モードを試すため)。 - Add Component → Custom → WaterBuoyancy を追加します。
- Inspector で次のように設定します:
- waterLevelY:
0 - buoyancyStrength:
5 - damping:
0.2 - maxUpwardSpeed:
3 - useWaveMotion:
true - waveAmplitude:
0.15 - waveFrequency:
1.2 - useWorldSpace:
true - enable2DPhysics:
false - enable3DPhysics:
false
- waterLevelY:
- ノードの Y 座標を
-2などにして水中に置き、再生してみてください。 - 物理挙動はありませんが、ゆっくり水面に向かって上昇し、水面付近で小刻みに揺れる挙動が確認できます。
3D でのテスト(RigidBody 利用)
- Hierarchy で右クリック → Create → 3D Object → Box を作成し、
FloatingCubeを作ります。 FloatingCubeを選択し、Add Component → Physics → RigidBody を追加します。- 必要に応じて BoxCollider も追加します。
- さらに Add Component → Custom → WaterBuoyancy を追加します。
- Inspector で以下のように設定します:
- waterLevelY:
0 - buoyancyStrength:
30(3D では少し大きめに) - damping:
0.3 - useWaveMotion:
true - waveAmplitude:
0.4 - waveFrequency:
0.6 - useWorldSpace:
true - enable2DPhysics:
false - enable3DPhysics:
true
- waterLevelY:
- Y 座標を
-3などにして水中に配置し、再生して挙動を確認します。
まとめ
この WaterBuoyancy コンポーネントは、
- 水面の高さを 1 つの数値で指定するだけで「水中かどうか」を自動判定。
- 2D/3D 物理にも Transform 直操作にも対応し、物理コンポーネントが無くても動作。
- 浮力・減衰・波動をすべてインスペクタから調整可能。
- 外部の GameManager やシングルトンに一切依存しない完全独立コンポーネント。
という特徴を持っています。
例えば、
- 水面に浮かぶ木箱・樽・ボートなどのオブジェクト
- 水中に落ちてゆっくり沈むコインやアイテム
- タイトル画面で水にぷかぷか浮かぶロゴやボタン
といった演出に、そのノードへ WaterBuoyancy をアタッチするだけで簡単に浮力表現を追加できます。
シーンやゲームごとに水面の高さや揺れ方を変えたい場合も、各ノードのインスペクタ設定だけで完結するため、再利用性と保守性に優れた構成になります。
ここからさらに、
- 「水エリアごとに水面Yを変えたい」→ プレハブごとに
waterLevelYを変えて配置 - 「重いオブジェクトは沈みやすくしたい」→
buoyancyStrengthをオブジェクトごとに変える
といった拡張も、同じコンポーネントの設定値だけで対応できます。
プロジェクトの共通ユーティリティとして WaterBuoyancy.ts を 1 つ用意しておくだけで、水の表現を大幅に簡略化できるはずです。




