Godotで「触手」や「しっぽ」「ロープ」みたいな多関節ものを作ろうとすると、ありがちなパターンはこんな感じですよね。
- シーンを深い階層で組む(Segment1 > Segment2 > Segment3…)
- それぞれのセグメントにRigidBody2DとJoint2Dを手作業で配置
- 長さを変えたくなるたびに「セグメントの複製 → Jointの再設定」という苦行
一度作ってしまえば動くけど、「長さ違いの触手が10本欲しい」「敵ごとに見た目を変えたい」となった瞬間、継承ベースや手作業のノード構成は一気にメンテナンス地獄になります。
そこで今回は、コンポーネント1つをアタッチするだけで「多関節触手」を物理ベースで自動生成してくれる、TentacleLimb コンポーネントを用意しました。親ノードにペタッと貼ってパラメータをいじるだけで、長さ・節の数・見た目を変えられるようにしていきましょう。
【Godot 4】物理でうねる多関節触手を量産!「TentacleLimb」コンポーネント
このコンポーネントは、親ノードの下に複数のRigidBody2D + Joint2D + Sprite2Dを自動生成し、物理演算でうねる触手を表現します。
コンポーネント指向なので、敵の腕、ボスの触手、動くロープ、鎖など、好きなノードにアタッチして再利用できます。
フルコード:TentacleLimb.gd
extends Node2D
class_name TentacleLimb
## 多関節の触手(ロープ・しっぽ等)を物理ベースで自動生成するコンポーネント。
##
## 親ノードのローカル座標を基準に、複数のセグメント(RigidBody2D)と
## PinJoint2D を並べて「うねる触手」を作ります。
##
## 想定する親ノード:
## - Node2D / CharacterBody2D / RigidBody2D など2D系ノード
##
## 使い方:
## 1. 親ノードの子として TentacleLimb を追加
## 2. エディタでパラメータ(@export)を調整
## 3. 再生すると、子ノードとしてセグメントとジョイントが自動生成される
@tool
## 触手の総セグメント数(最初のルートは除く、純粋な「関節の数」)
@export_range(1, 64, 1)
var segment_count: int = 8:
set(value):
segment_count = max(1, value)
if Engine.is_editor_hint():
_regenerate_tentacle()
## 1セグメントあたりの長さ(ピクセル)
@export_range(4.0, 256.0, 1.0)
var segment_length: float = 32.0:
set(value):
segment_length = max(4.0, value)
if Engine.is_editor_hint():
_regenerate_tentacle()
## セグメントの幅(見た目とコリジョンの幅)
@export_range(2.0, 256.0, 1.0)
var segment_width: float = 16.0:
set(value):
segment_width = max(2.0, value)
if Engine.is_editor_hint():
_regenerate_tentacle()
## セグメントのSpriteに使うテクスチャ
@export
var segment_texture: Texture2D:
set(value):
segment_texture = value
if Engine.is_editor_hint():
_update_segment_textures()
## セグメントSpriteのテクスチャを縦方向に伸ばすかどうか
@export
var stretch_texture_along_length: bool = true:
set(value):
stretch_texture_along_length = value
if Engine.is_editor_hint():
_update_segment_textures()
## セグメントの質量(大きいほど重くなる)
@export_range(0.01, 1000.0, 0.01)
var segment_mass: float = 1.0:
set(value):
segment_mass = max(0.01, value)
if Engine.is_editor_hint():
_update_segment_bodies()
## 各ジョイントの柔らかさ(バネのような効果)
## 0 に近いほど硬く、1 に近いほど柔らかく揺れるイメージ。
@export_range(0.0, 1.0, 0.01)
var joint_softness: float = 0.2:
set(value):
joint_softness = clamp(value, 0.0, 1.0)
if Engine.is_editor_hint():
_update_joints()
## 触手の基部をどのノードに固定するか
## 空なら「このコンポーネントの親ノード」に自動で固定します。
@export
var base_node_path: NodePath
## 触手の基部と最初のセグメントをどれくらい離すか
@export_range(0.0, 512.0, 1.0)
var base_offset: float = 0.0:
set(value):
base_offset = max(0.0, value)
if Engine.is_editor_hint():
_regenerate_tentacle()
## セグメント生成方向(ローカル座標系での方向)
## 例: Vector2.DOWN で「下向きの触手」になる
@export
var direction: Vector2 = Vector2.DOWN:
set(value):
direction = value.normalized() if value.length() > 0.0 else Vector2.DOWN
if Engine.is_editor_hint():
_regenerate_tentacle()
## セグメントを一列に並べるときの初期ゆらぎ角度(ラジアン)
## 0 ならまっすぐ、値を増やすと少しランダムに曲がった初期形状になる
@export_range(0.0, PI, 0.01)
var initial_angle_jitter: float = 0.2:
set(value):
initial_angle_jitter = clamp(value, 0.0, PI)
if Engine.is_editor_hint():
_regenerate_tentacle()
## デバッグ表示: セグメント位置・ジョイント位置を描画するか
@export
var debug_draw: bool = false:
set(value):
debug_draw = value
queue_redraw()
## 生成されたセグメントを格納するグループ名
const SEGMENT_GROUP := "tentacle_segment"
## 内部管理: 生成済みセグメント(順番に並んだ RigidBody2D の配列)
var _segments: Array[RigidBody2D] = []
## 実行時にのみ再生成したいとき用のフラグ
var _needs_runtime_regen: bool = false
func _ready() -> void:
if Engine.is_editor_hint():
# エディタ上ではすでに _regenerate_tentacle() が呼ばれている可能性がある
return
# 実行環境では起動時に一度だけ再生成
_regenerate_tentacle()
func _physics_process(_delta: float) -> void:
if _needs_runtime_regen and not Engine.is_editor_hint():
_regenerate_tentacle()
_needs_runtime_regen = false
func _notification(what: int) -> void:
if what == NOTIFICATION_EDITOR_PROPERTY_CHANGED:
# エディタでパラメータが変わったときに再描画
queue_redraw()
func _draw() -> void:
if not debug_draw:
return
# デバッグ用に、セグメントとジョイントの位置を簡易表示
for i in _segments.size():
var seg := _segments[i]
if not is_instance_valid(seg):
continue
var local_pos := to_local(seg.global_position)
draw_circle(local_pos, 3.0, Color.AQUA)
if i > 0:
var prev := _segments[i - 1]
if is_instance_valid(prev):
var prev_local := to_local(prev.global_position)
draw_line(prev_local, local_pos, Color.LIGHT_GREEN, 1.0)
# === パブリックAPI =========================================================
## 触手の先端ノード(RigidBody2D)を取得する。
## 先端に攻撃判定を付けたいときなどに利用できます。
func get_tip_body() -> RigidBody2D:
if _segments.is_empty():
return null
return _segments.back()
## 触手の全セグメントを配列で取得する。
func get_segments() -> Array[RigidBody2D]:
return _segments.duplicate()
## 実行時に「もう一度セグメントを作り直したい」ときに呼ぶ。
## 例: ボスフェーズ移行時に長さを変えるなど。
func regenerate_runtime() -> void:
_needs_runtime_regen = true
# === 内部実装 =============================================================
func _regenerate_tentacle() -> void:
_clear_existing_segments()
if segment_count <= 0:
return
_segments.clear()
# 基部ノード(触手の根本)を決定
var base_node := _get_base_node()
if base_node == null:
push_warning("TentacleLimb: base_node が見つかりません。親ノードにアタッチするか、base_node_path を設定してください。")
return
var base_global_pos := base_node.global_position
var dir := direction.normalized()
var base_to_first := dir * (base_offset + segment_length * 0.5)
var previous_body: PhysicsBody2D = null
for i in segment_count:
# 各セグメントのグローバル位置を決定
var offset_along_dir := dir * (segment_length * (i as float) + base_offset + segment_length * 0.5)
var seg_global_pos := base_global_pos + offset_along_dir
# セグメントRigidBody2Dを生成
var body := RigidBody2D.new()
body.name = "TentacleSegment_%d" % i
body.mass = segment_mass
body.gravity_scale = 1.0
body.linear_damp = 0.1
body.angular_damp = 0.1
body.freeze_mode = RigidBody2D.FREEZE_MODE_STATIC
body.freeze = false
# 少しだけ初期角度にゆらぎを与える
var angle := dir.angle()
if initial_angle_jitter > 0.0:
angle += randf_range(-initial_angle_jitter, initial_angle_jitter)
body.rotation = angle
# 見た目用Sprite2D
var sprite := Sprite2D.new()
sprite.name = "Sprite2D"
sprite.centered = true
sprite.texture = segment_texture
if stretch_texture_along_length and segment_texture:
# 簡易的なスケーリングで「細長い板」にする
var tex_size := segment_texture.get_size()
if tex_size.y != 0:
sprite.scale = Vector2(segment_width / tex_size.x, segment_length / tex_size.y)
else:
sprite.scale = Vector2.ONE
# コリジョン(長方形)
var collision := CollisionShape2D.new()
collision.name = "CollisionShape2D"
var rect_shape := RectangleShape2D.new()
rect_shape.size = Vector2(segment_width, segment_length)
collision.shape = rect_shape
# セグメントのローカル座標(親: TentacleLimb)
body.global_position = seg_global_pos
add_child(body)
body.owner = get_tree().edited_scene_root if Engine.is_editor_hint() else get_tree().current_scene
body.add_child(sprite)
sprite.owner = body.owner
body.add_child(collision)
collision.owner = body.owner
body.add_to_group(SEGMENT_GROUP)
# ジョイントを作成して前のセグメント or 基部につなぐ
if i == 0:
# 最初のセグメントは「基部」と繋ぐ
var joint := PinJoint2D.new()
joint.name = "BaseJoint"
# NodeA: 基部, NodeB: 最初のセグメント
joint.node_a = base_node.get_path()
joint.node_b = body.get_path()
# 基部と最初のセグメントの間のアンカー
joint.global_position = base_global_pos + dir * base_offset
_configure_joint(joint)
add_child(joint)
joint.owner = body.owner
else:
# それ以降は「前のセグメント」と繋ぐ
var joint := PinJoint2D.new()
joint.name = "Joint_%d" % i
joint.node_a = previous_body.get_path()
joint.node_b = body.get_path()
# 2つのセグメントの中点あたりにジョイントを置く
joint.global_position = (previous_body.global_position + body.global_position) * 0.5
_configure_joint(joint)
add_child(joint)
joint.owner = body.owner
previous_body = body
_segments.append(body)
queue_redraw()
func _clear_existing_segments() -> void:
# 既存のセグメントとジョイントを全削除
for child in get_children():
if child is RigidBody2D and child.is_in_group(SEGMENT_GROUP):
child.queue_free()
elif child is Joint2D and (str(child.name).begins_with("Joint_") or child.name == "BaseJoint"):
child.queue_free()
_segments.clear()
func _get_base_node() -> Node2D:
# 明示的に指定されていればそれを使う
if base_node_path != NodePath():
var node := get_node_or_null(base_node_path)
if node and node is Node2D:
return node
# そうでなければ「親ノード」を基部とみなす
if get_parent() and get_parent() is Node2D:
return get_parent()
return null
func _configure_joint(joint: PinJoint2D) -> void:
# Godot 4 の PinJoint2D には damping, softness 等のパラメータがあります。
# ここでは joint_softness を「柔らかさ」として簡単に反映します。
# 数値は好みに応じて調整してください。
joint.bias = 0.8 # 1 に近いほど制約を強く守る
joint.damping = lerp(0.0, 1.0, joint_softness)
joint.softness = joint_softness
func _update_segment_textures() -> void:
# 既存セグメントのSprite2Dにテクスチャやスケールを反映
for body in _segments:
if not is_instance_valid(body):
continue
var sprite := body.get_node_or_null("Sprite2D")
if sprite and sprite is Sprite2D:
sprite.texture = segment_texture
if stretch_texture_along_length and segment_texture:
var tex_size := segment_texture.get_size()
if tex_size.y != 0:
sprite.scale = Vector2(segment_width / tex_size.x, segment_length / tex_size.y)
else:
sprite.scale = Vector2.ONE
queue_redraw()
func _update_segment_bodies() -> void:
# 質量などの物理パラメータを一括更新
for body in _segments:
if not is_instance_valid(body):
continue
body.mass = segment_mass
func _update_joints() -> void:
# 既存のジョイントに joint_softness を反映
for child in get_children():
if child is PinJoint2D:
_configure_joint(child)
使い方の手順
ここからは、プレイヤーに「物理で揺れるしっぽ」を付ける例で説明します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── TentacleLimb (Node2D) ← このコンポーネントをアタッチ
手順①:スクリプトを用意する
- 上記の
TentacleLimb.gdをプロジェクト内(例:res://components/TentacleLimb.gd)に保存します。 - Godotエディタを再読み込みすると、ノード追加ダイアログで
TentacleLimbが選べるようになります。
手順②:プレイヤーにコンポーネントをアタッチ
- プレイヤーシーンを開きます。
- ルートが
CharacterBody2Dになっていることを確認します。 - 右クリック → 「子ノードを追加」 →
TentacleLimbを検索して追加。
シーン構成はこんな感じになります:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── TentacleLimb (Node2D)
手順③:パラメータを設定する
TentacleLimb を選択すると、インスペクタに以下のような設定項目が出てきます。
segment_count: しっぽの節の数(例: 10)segment_length: 1節の長さ(例: 24)segment_width: しっぽの太さ(例: 12)segment_texture: しっぽ用のテクスチャ(細長い画像 or 小さい正方形)stretch_texture_along_length: テクスチャを縦に伸ばすかどうかsegment_mass: 1節の重さ(例: 0.5~2.0)joint_softness: 柔らかさ(0.0: ガチガチ / 0.8: かなりフニャフニャ)base_node_path: 基部ノード(未設定なら親ノード=Playerに固定)base_offset: Playerからどれくらい離してしっぽを生やすかdirection: しっぽが伸びる向き(例:Vector2.DOWN)initial_angle_jitter: 初期状態の「くねり」具合debug_draw: デバッグ用ライン描画のON/OFF
エディタ上でパラメータを変えると、その場でセグメントが再生成されるので、見た目を確認しながら微調整できます。
手順④:実行して確認する
- ゲームを実行すると、Player にぶら下がるしっぽが出現し、移動やジャンプに合わせて物理的に揺れます。
- もし「長すぎる」「重すぎてブランブランしすぎる」などがあれば、
segment_countやsegment_mass、joint_softnessを調整しましょう。
別の使用例:ボスの多関節触手
同じコンポーネントを、そのままボスの触手にも使えます。
Boss (Node2D) ├── Sprite2D ├── CollisionShape2D ├── TentacleLimb_Left (Node2D) └── TentacleLimb_Right (Node2D)
TentacleLimb_Left.direction = Vector2.LEFTTentacleLimb_Right.direction = Vector2.RIGHT- それぞれ
segment_textureを変えて、左右で見た目を変えることも可能
さらに、先端だけ別スクリプトで制御したい場合は、get_tip_body() を使って「先端の RigidBody2D」にアクセスし、エフェクトや攻撃判定を付けることもできます。
# Boss.gd の一部例
@onready var left_tentacle: TentacleLimb = $TentacleLimb_Left
@onready var right_tentacle: TentacleLimb = $TentacleLimb_Right
func _ready() -> void:
# 左右の触手の先端にパーティクルを付けるなど
var left_tip := left_tentacle.get_tip_body()
var right_tip := right_tentacle.get_tip_body()
if left_tip:
var fx := preload("res://fx/GlowParticles2D.tscn").instantiate()
left_tip.add_child(fx)
if right_tip:
var fx2 := preload("res://fx/GlowParticles2D.tscn").instantiate()
right_tip.add_child(fx2)
メリットと応用
TentacleLimb コンポーネントを使うメリットは、ざっくり言うと以下の通りです。
- シーン構造がスッキリ
触手のために「Segment1 > Segment2 > Segment3…」と深い階層を作る必要がありません。
親ノードの下にTentacleLimbを1つ置くだけでOKです。 - 長さや見た目のバリエーションが簡単
segment_countやsegment_lengthを変えるだけで、長さの違う触手を量産できます。
テクスチャを差し替えれば、同じ物理挙動で「しっぽ」「鎖」「ロープ」などを使い分け可能です。 - 継承地獄からの脱出
「TentacleEnemy」「TentacleBoss」「TentacleTrap」などを継承で増やすのではなく、
それぞれの敵にTentacleLimbコンポーネントをアタッチするだけで済みます。
合成(Composition)で機能を足すスタイルですね。 - レベルデザインが楽になる
レベルエディタでTentacleLimbをポンポン置いて、
segment_countやdirectionをシーンごとに変えるだけで、
「天井から垂れ下がる触手」「床から突き出すトゲロープ」などを簡単に配置できます。
応用: 先端をターゲット方向に引っ張る「簡易AI」
最後に、改造案として「触手の先端を特定のターゲット方向に少し引っ張る」関数の例を載せておきます。
ボスの触手がプレイヤーの方向にじわっと向きを合わせる、みたいな表現に使えます。
## 触手の先端を target_global_pos 方向に少しだけ引っ張る
## strength: どれくらい強く引っ張るか(0.0~1.0 目安)
func pull_tip_towards(target_global_pos: Vector2, strength: float = 0.1) -> void:
var tip := get_tip_body()
if not tip:
return
strength = clamp(strength, 0.0, 1.0)
# 先端からターゲットへのベクトル
var dir := (target_global_pos - tip.global_position)
if dir.length() <= 0.1:
return
# 方向に応じて小さな力を加える
var force := dir.normalized() * segment_mass * 100.0 * strength
tip.apply_central_impulse(force)
この関数を TentacleLimb に追加しておき、ボス側のスクリプトから
func _physics_process(_delta: float) -> void:
var player := get_tree().get_first_node_in_group("player")
if player:
$TentacleLimb_Left.pull_tip_towards(player.global_position, 0.05)
$TentacleLimb_Right.pull_tip_towards(player.global_position, 0.05)
のように呼べば、「物理で揺れつつ、ゆっくりプレイヤーの方向を向く触手」が簡単に実現できます。
継承ベースでガチガチに固めたクラスより、こういう コンポーネント1つで「うねる触手」を量産できる仕組みの方が、あとからの拡張や調整が圧倒的に楽になります。ぜひ自分のプロジェクト用にカスタマイズしてみてください。




