【Godot 4】TentacleLimb (多関節触手) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

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)   ← このコンポーネントをアタッチ

手順①:スクリプトを用意する

  1. 上記の TentacleLimb.gd をプロジェクト内(例: res://components/TentacleLimb.gd)に保存します。
  2. Godotエディタを再読み込みすると、ノード追加ダイアログで TentacleLimb が選べるようになります。

手順②:プレイヤーにコンポーネントをアタッチ

  1. プレイヤーシーンを開きます。
  2. ルートが CharacterBody2D になっていることを確認します。
  3. 右クリック → 「子ノードを追加」 → 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_countsegment_massjoint_softness を調整しましょう。

別の使用例:ボスの多関節触手

同じコンポーネントを、そのままボスの触手にも使えます。

Boss (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── TentacleLimb_Left (Node2D)
 └── TentacleLimb_Right (Node2D)
  • TentacleLimb_Left.direction = Vector2.LEFT
  • TentacleLimb_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_countsegment_length を変えるだけで、長さの違う触手を量産できます。
    テクスチャを差し替えれば、同じ物理挙動で「しっぽ」「鎖」「ロープ」などを使い分け可能です。
  • 継承地獄からの脱出
    「TentacleEnemy」「TentacleBoss」「TentacleTrap」などを継承で増やすのではなく、
    それぞれの敵に TentacleLimb コンポーネントをアタッチするだけで済みます。
    合成(Composition)で機能を足すスタイルですね。
  • レベルデザインが楽になる
    レベルエディタで TentacleLimb をポンポン置いて、
    segment_countdirection をシーンごとに変えるだけで、
    「天井から垂れ下がる触手」「床から突き出すトゲロープ」などを簡単に配置できます。

応用: 先端をターゲット方向に引っ張る「簡易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つで「うねる触手」を量産できる仕組みの方が、あとからの拡張や調整が圧倒的に楽になります。ぜひ自分のプロジェクト用にカスタマイズしてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!