【Godot 4】IceFloor (氷の床) コンポーネントの作り方

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 4でキャラクターの「ツルツル滑る床」を作ろうとすると、多くの人がまず考えるのは:

  • PhysicsMaterial の friction を変えた専用の TileSet を作る
  • プレイヤーシーンを継承して「氷エリア用プレイヤー」を作る
  • マップ側でプレイヤーのスクリプトに直接アクセスしてパラメータを書き換える

…みたいなやり方だと思います。どれも動くのですが、

  • タイルセットが増える&管理が面倒
  • プレイヤーの派生シーンが増えすぎてカオス
  • マップからプレイヤーを直接いじると依存関係がベタベタになる

という「あとから効いてくるツラさ」がありますよね。

そこで今回は「そのエリアに入っている間だけプレイヤーの摩擦を極端に下げて、慣性を残す」ためのコンポーネント、IceFloor を用意しました。
プレイヤーには「滑りやすさ」を調整できるコンポーネントをアタッチしておき、床の側から「お前、いま氷モードね」と指示を出すだけにします。

継承ではなく 合成(Composition) で、氷エリアのロジックをきれいに分離していきましょう。


【Godot 4】ツルツル氷エリアをコンポーネントで!「IceFloor」コンポーネント

今回の構成は以下の2コンポーネントで考えます:

  • IceFloor:エリアに入ったキャラクターに「氷モード」をオン/オフする
  • FrictionController:キャラクター側にアタッチし、「今は通常摩擦 or 氷摩擦」を切り替える

どちらも 独立したコンポーネント なので、プレイヤーにも敵にも、動く床にも、好きなノードにアタッチして再利用できます。


ソースコード(Full Code)

1. キャラクター側:FrictionController.gd

プレイヤーや敵など「動くもの」にアタッチするコンポーネントです。
外部(今回だと IceFloor)から「氷モードにして」と言われると、摩擦関連パラメータを切り替えます。


extends Node
class_name FrictionController
## キャラクターの「摩擦・慣性」を一元管理するコンポーネント。
## IceFloor などのエリアから状態を切り替えてもらう想定です。

@export var normal_friction: float = 1.0:
	## 通常時の「減速の強さ」を表すパラメータ。
	## 値が大きいほど急ブレーキ、値が小さいほどヌルヌル滑るイメージです。
	set(value):
		normal_friction = max(value, 0.0)

@export var ice_friction: float = 0.1:
	## 氷エリア内での「減速の強さ」。
	## 0 に近づけるほど慣性が強くなり、なかなか止まりません。
	set(value):
		ice_friction = max(value, 0.0)

@export var normal_max_speed: float = 300.0:
	## 通常時の最大速度(あれば)。
	## プレイヤー側で使っていないなら 0 のままでもOKです。
	set(value):
		normal_max_speed = max(value, 0.0)

@export var ice_max_speed: float = 450.0:
	## 氷エリア内での最大速度。
	## 氷の上ではちょっとスピードアップ、のような演出もここで。
	set(value):
		ice_max_speed = max(value, 0.0)

@export var debug_print_state: bool = false
	## true にすると、モード切り替え時にログを出します。

## 現在の摩擦モード
var _is_on_ice: bool = false

## 実際に移動処理を行っているノード(CharacterBody2D など)への参照。
## デフォルトでは親ノードを想定しています。
var _body: Node = null

## 外部から参照しやすいように、現在のパラメータを公開
var current_friction: float:
	get:
		return _is_on_ice ? ice_friction : normal_friction

var current_max_speed: float:
	get:
		return _is_on_ice ? ice_max_speed : normal_max_speed


func _ready() -> void:
	# デフォルトでは親ノードを「動く本体」として扱います。
	_body = get_parent()
	if debug_print_state:
		print("[FrictionController] Ready. body =", _body)


func set_on_ice(enabled: bool) -> void:
	## IceFloor などから呼ばれるエントリポイント。
	## enabled = true で氷モード、false で通常モードに戻します。
	if _is_on_ice == enabled:
		return  # 変化なしなら何もしない

	_is_on_ice = enabled

	if debug_print_state:
		print("[FrictionController] on_ice =", _is_on_ice,
			" friction =", current_friction,
			" max_speed =", current_max_speed)

	# ここでは「今すぐ速度を書き換える」まではしません。
	# 実際の速度制御は、プレイヤーの移動スクリプト側で
	# current_friction / current_max_speed を参照して行う想定です。


func is_on_ice() -> bool:
	## 現在氷モードかどうかを返します。
	return _is_on_ice

プレイヤー側の移動スクリプト例(抜粋)

上記 FrictionController を前提にした、CharacterBody2D 用のシンプルな移動ロジック例です。


extends CharacterBody2D

@export var acceleration: float = 2000.0

var friction_controller: FrictionController


func _ready() -> void:
	# 同じノード階層の子に FrictionController が付いている前提
	friction_controller = $FrictionController


func _physics_process(delta: float) -> void:
	var input_dir := Input.get_axis("ui_left", "ui_right")

	# 加速処理
	if input_dir != 0:
		velocity.x += input_dir * acceleration * delta
	else:
		# 入力が無いときの減速処理。
		# FrictionController の current_friction を使って減速量を決定。
		var friction := friction_controller.current_friction
		if abs(velocity.x) > 0.1:
			var decel := friction * acceleration * delta
			if abs(velocity.x) <= decel:
				velocity.x = 0.0
			else:
				velocity.x -= sign(velocity.x) * decel

	# 最大速度制限(0 の場合は制限なし)
	var max_speed := friction_controller.current_max_speed
	if max_speed > 0.0:
		velocity.x = clamp(velocity.x, -max_speed, max_speed)

	move_and_slide()

2. 氷の床側:IceFloor.gd

こちらが今回の主役コンポーネントです。
Area2D にアタッチして、エリアに入った/出たキャラクターの FrictionController を探し出し、氷モードをオン/オフします。


extends Area2D
class_name IceFloor
## このエリア内にいる間だけ、対象の FrictionController を「氷モード」にするコンポーネント。
## Area2D をベースにしているので、コリジョン形状で氷エリアの形を自由に作れます。

@export var affect_players: bool = true:
	## プレイヤーを氷モード対象にするかどうか。
	## グループ "player" を前提にしています。
	set(value):
		affect_players = value

@export var affect_enemies: bool = false:
	## 敵(グループ "enemy")も氷モード対象にするか。
	set(value):
		affect_enemies = value

@export var auto_disable_when_empty: bool = false:
	## 誰も乗っていないときに自動で無効化したい場合用。
	## (例えば動く氷床などで、パフォーマンスを気にするケース)
	set(value):
		auto_disable_when_empty = value

@export var debug_print: bool = false
	## true でログを出す(デバッグ用)。


## 現在この氷床の上に乗っている FrictionController のリスト
var _controllers: Array[FrictionController] = []


func _ready() -> void:
	# コリジョンレイヤー・マスクはエディタ側で設定しておいてOKですが、
	# 念のため Monitoring は有効化しておきます。
	monitoring = true
	monitorable = true

	body_entered.connect(_on_body_entered)
	body_exited.connect(_on_body_exited)

	if debug_print:
		print("[IceFloor] Ready. affect_players =", affect_players,
			" affect_enemies =", affect_enemies)


func _on_body_entered(body: Node) -> void:
	if not _should_affect_body(body):
		return

	var controller := _find_friction_controller(body)
	if controller == null:
		if debug_print:
			print("[IceFloor] body entered but no FrictionController:", body)
		return

	if controller in _controllers:
		return  # すでに登録済み

	_controllers.append(controller)
	controller.set_on_ice(true)

	if debug_print:
		print("[IceFloor] ENTER: set_on_ice(true) for", body, "controllers:", _controllers.size())


func _on_body_exited(body: Node) -> void:
	if not _should_affect_body(body):
		return

	var controller := _find_friction_controller(body)
	if controller == null:
		return

	if controller in _controllers:
		_controllers.erase(controller)
		controller.set_on_ice(false)

	if debug_print:
		print("[IceFloor] EXIT: set_on_ice(false) for", body, "controllers:", _controllers.size())

	if auto_disable_when_empty and _controllers.is_empty():
		# 誰も乗っていないなら自動で無効化(任意)
		monitoring = false
		if debug_print:
			print("[IceFloor] auto disabled (no bodies on ice).")


func _should_affect_body(body: Node) -> bool:
	## この氷床が、与えられた body を対象にするかどうかを判定する。
	if affect_players and body.is_in_group("player"):
		return true
	if affect_enemies and body.is_in_group("enemy"):
		return true
	return false


func _find_friction_controller(body: Node) -> FrictionController:
	## body 自身、もしくは子孫ノードから FrictionController を探す。
	## ここでは「親が CharacterBody2D / RigidBody2D などで、
	## その子に FrictionController が付いている」構成を想定しています。

	# 1. body 自身が FrictionController を持っているか?
	if body is FrictionController:
		return body

	# 2. 直接の子孫から探す(最初に見つかったものを返す)
	for child in body.get_children():
		if child is FrictionController:
			return child
		# 再帰的に探したい場合は以下を使う(コストは上がる)
		var found := _find_friction_controller_recursive(child)
		if found != null:
			return found

	return null


func _find_friction_controller_recursive(node: Node) -> FrictionController:
	for child in node.get_children():
		if child is FrictionController:
			return child
		var found := _find_friction_controller_recursive(child)
		if found != null:
			return found
	return null

使い方の手順

手順①:プレイヤーに FrictionController をアタッチする

まずはプレイヤーシーンに FrictionController コンポーネントを追加します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FrictionController (Node)  ← このコンポーネントをアタッチ
  • Player のスクリプトは、先ほどの「移動スクリプト例」をベースにして、friction_controller.current_friction を参照するようにします。
  • プレイヤーのルートノードを グループ “player” に追加しておきましょう。

手順②:氷の床シーンを作る

次に、氷エリア用のシーンを作ります。

IceFloor (Area2D)
 ├── CollisionShape2D  ← 氷エリアの形
 └── Sprite2D          ← 氷の見た目(任意)
  • IceFloor ノードに IceFloor.gd をアタッチします。
  • CollisionShape2D でエリアの形を決めます(矩形、ポリゴン、なんでもOK)。
  • コリジョンレイヤー/マスクは「プレイヤーのコリジョン」とぶつかるように設定してください(Area2D と Body の検出用)。

シーンツリー例(マップ側から見た構成):

Level1 (Node2D)
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    ├── CollisionShape2D
 │    └── FrictionController (Node)
 ├── IceFloor_A (Area2D)
 │    ├── CollisionShape2D
 │    └── Sprite2D
 └── IceFloor_B (Area2D)
      ├── CollisionShape2D
      └── Sprite2D

手順③:パラメータを調整する

  • FrictionController(プレイヤー側)
    • normal_friction:通常地面での減速の強さ
    • ice_friction:氷エリアでの減速の強さ(0.05~0.2 くらいがそれっぽいです)
    • normal_max_speed:通常時の最高速度
    • ice_max_speed:氷時の最高速度(少し高めにすると「滑ってる感」が出ます)
  • IceFloor(床側)
    • affect_players:プレイヤーにだけ効かせるなら true
    • affect_enemies:敵にも効かせたいなら true にして、敵を “enemy” グループに
    • auto_disable_when_empty:誰も乗っていないときに監視停止したい場合のみ true
    • debug_print:挙動確認中だけ true にしてログを見ると便利です

手順④:応用例(敵や動く床にも適用)

同じコンポーネントを、敵や動く床にもそのまま使い回せます。

敵キャラ例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── FrictionController (Node)
  • 敵のルートノードをグループ "enemy" に追加。
  • 敵の移動スクリプトでも、プレイヤーと同様に friction_controller.current_friction を参照して減速処理を行う。
  • IceFloor 側の affect_enemies を true にすれば、同じ床でプレイヤーも敵もツルツルにできます。

動く床(ベルトコンベア的なもの)が氷になる、みたいなギミックも:

MovingPlatform (Node2D)
 ├── CollisionShape2D
 ├── Sprite2D
 └── IceFloor (Area2D)  ← ここに IceFloor を付ける

とすれば、「乗っている間だけ摩擦が下がる動く床」が簡単に作れます。


メリットと応用

  • シーン構造がスッキリ:プレイヤーを継承して「氷用プレイヤー」を作る必要がありません。プレイヤーは常に1シーンでOK。
  • レベルデザインが楽:氷エリアを増やしたいときは、IceFloor シーンをマップにポンポン配置するだけ。
  • 再利用性が高い:敵・プレイヤー・動く床など、どんな「動くもの」にも FrictionController を付ければ同じ仕組みが使えます。
  • 依存関係がゆるい:IceFloor は「FrictionController を持っているかどうか」だけを見ており、具体的なプレイヤークラス名などには依存しません。

これぞまさに「継承より合成」ですね。
氷エリアのロジックは IceFloor コンポーネントに閉じ込め、キャラクターの物理パラメータは FrictionController に閉じ込める。
それぞれが役割に集中しているので、後から仕様変更が入っても壊れにくくなります。

改造案:時間経過で「だんだん滑りやすくなる」氷床

例えば、氷の上に乗ってから時間が経つほど摩擦が減っていき、最終的には超ツルツルになるギミックも簡単に作れます。
IceFloor に次のような関数を追加して、_physics_process から呼ぶ構成です。


func _physics_process(delta: float) -> void:
	_update_ice_intensity(delta)


func _update_ice_intensity(delta: float) -> void:
	## 氷の「強さ」を 0.0 ~ 1.0 で管理し、
	## それに応じて FrictionController の ice_friction を書き換える例。

	var intensity_speed := 0.3  # 1.0 になるまでの速さ
	for controller in _controllers:
		# FrictionController 側に「現在の強さ」を保持させてもいいですが、
		# ここでは単純に ice_friction を少しずつ下げていきます。
		var target := 0.02  # 最終的にこのくらいまで下げたい
		var current := controller.ice_friction
		var new_value := lerp(current, target, intensity_speed * delta)
		controller.ice_friction = new_value

このように、床の側のロジックをいじるだけで、プレイヤーや敵のスクリプトを変更せずに挙動を変えられるのが、コンポーネント指向の気持ちよさですね。
ぜひ自分のプロジェクトでも、IceFloor をベースにいろいろな「特殊床コンポーネント」を量産してみてください。

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をコピーしました!