Godot 4 で「水の中に入ったらフワッと浮く」挙動を作ろうとすると、けっこう面倒ですよね。典型的なのは:
- 水用の
Area2D/Area3Dを作って、 - 各オブジェクト側で
body_entered/body_exitedを拾って、 - 「水の中にいるフラグ」を自前で持って、
- さらに
_physics_process()で重力と足し引きしながら浮力を計算する
…といった感じの実装になりがちです。
しかも「プレイヤー」「木箱」「敵キャラ」など、水に浮いてほしいノードごとに似たようなコードをコピペし始めると、あっという間にメンテ地獄ですね。
そこで今回は「継承ではなくコンポーネントをアタッチするだけ」で水の浮力を付与できる 「WaterBuoyancy」コンポーネント を用意しました。
RigidBody 系でも CharacterBody 系でも、「水エリア」に入っている間だけ上向きの力・速度補正をかけることができます。
【Godot 4】水に入ったらフワッと浮く!「WaterBuoyancy」コンポーネント
このコンポーネントは Area2D をベースにした「水エリア」用ノードです。
シーンにポンと置いてコリジョンを張れば、そのエリア内にいる物体に自動で浮力を加えてくれます。
- 水エリア側:
WaterBuoyancyをアタッチしたシーンを配置するだけ - 物体側:通常の
RigidBody2DやCharacterBody2Dのままで OK(特別な継承不要)
つまり、「水の挙動」は水エリアのコンポーネントに閉じ込めておき、プレイヤーや箱には余計な責務を持たせない、というコンポーネント指向の構成になっています。
GDScript フルコード(2D 版)
extends Area2D
class_name WaterBuoyancy
## 水エリア内の物体に「浮力」を与えるコンポーネント(2D 用)
##
## - このノード自体を「水エリア」として使います
## - コリジョン形状を持たせておき、物体が入っている間だけ上向きの力/速度補正を加えます
## - RigidBody2D / CharacterBody2D 両方をサポートします
## --- 基本設定 ---
@export_category("Buoyancy Settings")
@export var enabled: bool = true:
set(value):
enabled = value
monitoring = value
monitorable = value
## 浮力の強さ(単位: px/s^2 相当)
## RigidBody2D: 重力に対抗する「加速度」として力を追加
## CharacterBody2D: velocity.y をこの値に近づけるように補正
@export var buoyancy_acceleration: float = 600.0
## 水の抵抗(減速係数)。大きいほど動きが重くなる
@export_range(0.0, 1.0, 0.01)
var damping_factor: float = 0.2
## CharacterBody2D の縦速度をどこまで抑えるか(上方向の最大速度)
@export var max_upward_speed: float = -200.0
## CharacterBody2D の落下速度(下方向)をどこまで抑えるか
@export var max_downward_speed: float = 150.0
## RigidBody2D にインパルスではなく「連続的な力」を与えるかどうか
## (true の方が自然に見えやすい)
@export var use_constant_force: bool = true
## デバッグ用: 対象を色付きでハイライトする
@export_category("Debug")
@export var debug_draw: bool = false
@export var debug_color: Color = Color(0.2, 0.6, 1.0, 0.4)
## --- 内部状態 ---
## 現在この水エリア内にいる物体一覧
var _bodies: Array[Node] = []
func _ready() -> void:
## Area2D としての監視設定
monitoring = enabled
monitorable = enabled
## 既にシーンにいるボディを拾う(エディタ上で重なっている場合など)
for body in get_overlapping_bodies():
_on_body_entered(body)
## シグナル接続(エディタ上で繋がなくてOK)
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
func _physics_process(delta: float) -> void:
if not enabled:
return
for body in _bodies:
if not is_instance_valid(body):
continue
if body is RigidBody2D:
_apply_buoyancy_to_rigidbody(body as RigidBody2D, delta)
elif body is CharacterBody2D:
_apply_buoyancy_to_character(body as CharacterBody2D, delta)
func _on_body_entered(body: Node) -> void:
if body in _bodies:
return
_bodies.append(body)
func _on_body_exited(body: Node) -> void:
_bodies.erase(body)
## --- RigidBody2D への適用ロジック ---
func _apply_buoyancy_to_rigidbody(rb: RigidBody2D, delta: float) -> void:
## 物理演算が無効なら何もしない
if rb.freeze:
return
## 上向きの力(加速度)を与える
var force := Vector2(0, -buoyancy_acceleration * rb.mass)
if use_constant_force:
## add_constant_force は Godot 4.2 時点では 2D に存在しないため、
## 連続的に add_force を呼ぶ形で近似します。
rb.apply_force(force * delta)
else:
## 単発インパルス寄りの効果(少し荒っぽい)
rb.apply_central_impulse(force * delta)
## 水の抵抗として減速
rb.linear_velocity.x *= (1.0 - damping_factor * delta * 5.0)
rb.linear_velocity.y *= (1.0 - damping_factor * delta * 5.0)
## --- CharacterBody2D への適用ロジック ---
func _apply_buoyancy_to_character(ch: CharacterBody2D, delta: float) -> void:
var v := ch.velocity
## 縦方向の速度を「浮力方向」に引き寄せる
## v.y は下向きが正なので、上向きにしたい場合は負方向へ寄せる
var target_upward_speed := max_upward_speed
## 線形補間で「ふわっと」寄せる
v.y = lerp(v.y, target_upward_speed, buoyancy_acceleration * delta / 1000.0)
## 最大落下速度を制限(沈みすぎ防止)
if v.y > max_downward_speed:
v.y = max_downward_speed
## 水の抵抗として横方向も減速
v.x = lerp(v.x, 0.0, damping_factor * delta * 5.0)
ch.velocity = v
## --- デバッグ描画(任意) ---
func _draw() -> void:
if not debug_draw:
return
## CollisionShape2D があれば、その AABB を塗る簡易デバッグ
for child in get_children():
if child is CollisionShape2D and child.shape:
var aabb := child.shape.get_rect()
draw_rect(aabb, debug_color, true)
func _process(_delta: float) -> void:
if debug_draw:
queue_redraw()
使い方の手順
① WaterBuoyancy.gd をプロジェクトに追加
res://components/WaterBuoyancy.gdなど好きな場所に、上記コードを保存します。- エディタを再読み込みすると、ノード追加ダイアログの「スクリプトクラス」に WaterBuoyancy が出てくるはずです。
② 水エリア用シーンを作る
例として、2D の水たまりを作ってみます。
WaterArea (Area2D) ├── CollisionShape2D └── Sprite2D(任意、水の見た目)
- WaterArea (Area2D) に
WaterBuoyancy.gdをアタッチします。 - CollisionShape2D に、矩形やポリゴンなど「水の範囲」を表す形状を設定します。
- Sprite2D で水のテクスチャを貼れば、見た目も分かりやすくなります。
この WaterArea シーンを、ステージシーンの好きな場所にインスタンスして配置するだけで「水エリア」が完成します。
③ プレイヤーや箱を普通に配置する
たとえばこんなシーン構成を想定します:
MainLevel (Node2D)
├── Player (CharacterBody2D)
│ ├── Sprite2D
│ ├── CollisionShape2D
│ └── Camera2D
├── Box (RigidBody2D)
│ ├── Sprite2D
│ └── CollisionShape2D
└── WaterArea (Area2D)
├── CollisionShape2D
└── Sprite2D
- Player は通常の横スクロール用 CharacterBody2D スクリプトで OK(重力や移動処理は従来通り)
- Box は単純な RigidBody2D(モード: Rigid)
- WaterArea だけが
WaterBuoyancyを持っています
この構成なら、プレイヤーや箱のスクリプトは「水のこと」を一切知らなくてよく、
水エリアに入った瞬間から自動的に浮力の影響を受けるようになります。
④ パラメータ調整で「好みの水」を作る
WaterArea を選択すると、インスペクタに Buoyancy Settings が出てきます。
buoyancy_acceleration:大きくすると強く浮く(重力より大きいとガンガン浮かぶ)damping_factor:大きくすると水の抵抗が強くなり、動きが重くなるmax_upward_speed:CharacterBody2D がどれだけ速く上に登っていいか(負の値)max_downward_speed:CharacterBody2D がどれだけ速く沈んでいいかdebug_draw:オンにすると水エリアのコリジョンを塗りつぶして確認できます
ステージごとに WaterArea を複数置いて、浅い水・深い水・粘度の高い水などを作り分けるのも簡単ですね。
メリットと応用
1. シーン構造がシンプルになる
水の挙動はすべて WaterBuoyancy に閉じ込めているので、プレイヤーや敵・オブジェクト側のスクリプトは一切汚れません。
「水に浮く処理を追加したいからプレイヤーのスクリプトを継承し直して…」といった継承地獄を回避できます。
2. どんなオブジェクトにも後付けで対応できる
水エリアは「Area2D と CollisionShape2D だけ」なので、ステージ上にある既存の RigidBody2D / CharacterBody2D はそのまま対応可能です。
特定の敵だけ浮かせたくない場合は、collision_layer / collision_mask を使ってレイヤー分けすれば OK です。
3. レベルデザインが楽になる
レベルデザイナは「水エリアのプレハブ」をペタペタ置くだけで、水たまり・池・川などを構築できます。
浮力の強さもインスペクタから即座に調整できるので、ゲームバランスの微調整も素早く行えます。
4. 応用例
- 溶岩エリア:浮力は弱め、ダメージ処理だけ追加する
- 毒の沼:強い減速(damping_factor 高め)+徐々に HP を削る
- 上昇気流:水ではないが、「上向きのエリア」として流用する
改造案:ダメージ付きの毒沼にする
例えば、WaterBuoyancy を継承した「PoisonSwampBuoyancy」を作り、
水エリア内のキャラクターにダメージを与える処理を足すと、簡単に毒沼が作れます。
extends WaterBuoyancy
class_name PoisonSwampBuoyancy
@export var damage_per_second: float = 5.0
func _physics_process(delta: float) -> void:
## まず通常の浮力処理を実行
super._physics_process(delta)
## 追加でダメージ処理
for body in _bodies:
if not is_instance_valid(body):
continue
## ここでは「take_damage(amount: float)」を持つノードを想定
if body.has_method("take_damage"):
body.take_damage(damage_per_second * delta)
こうして「水の浮力」はそのままに、「毒沼」という新しいギミックを実現できます。
継承を使うにしても、ゲームロジックごとにコンポーネントを分けておくと、責務がハッキリして気持ちいいですね。
この WaterBuoyancy コンポーネントをベースに、独自の水物理やギミックをどんどん合成していきましょう。
