横スクロールアクションを作っていると、One Way Collision(片方向の床)から下にすり抜ける動きって欲しくなりますよね。Godot 4 だと TileMap や StaticBody2D に「One Way Collision」を設定すれば、上から乗るだけなら簡単です。
でも、
- 「↓+ジャンプ」で床をすり抜けたい
- プレイヤーだけ特定の床をすり抜けたい
- 敵はすり抜けないで欲しい
といったちょっとリッチな挙動を実装しようとすると、だんだん面倒になってきます。
- プレイヤーのスクリプトが巨大化して、「床抜け」ロジックが混ざる
- 床側のスクリプトに「誰が抜けるか」ロジックを書き始めてしまう
- 継承ベースで PlayerFloorDropper, EnemyFloorDropper みたいなクラスが増える
こうなると、「床抜け機能」を別キャラにも使い回したいときに、また同じようなコードを書き直すハメになります。まさに「継承地獄」「巨大 Player.gd 問題」ですね。
そこで今回は、どんなキャラにもポン付けできるコンポーネントとして、
「PlatformDropper」コンポーネント
を用意して、「↓+ジャンプで OneWayPlatform を一時的に無効化する」機能を、きれいに分離してしまいましょう。
【Godot 4】↓+ジャンプですり抜け!「PlatformDropper」コンポーネント
このコンポーネントは、CharacterBody2D / CharacterBody3D などの「動く側」にアタッチするだけで動作することを想定しています。
- 「↓+ジャンプ」を検知
- 今立っている One Way Platform を一時的に無効化
- 一定時間後に自動で復帰
という、床抜けに必要な処理を全部まとめています。
フルコード(GDScript / 2D 用)
extends Node
class_name PlatformDropper
"""
PlatformDropper コンポーネント
--------------------------------
「↓+ジャンプ」で One Way Platform を一時的にすり抜けるためのコンポーネント。
想定:
- 親ノードは CharacterBody2D(または同等の「動くキャラ」)
- 親のスクリプト側で通常の移動・ジャンプ処理を行う
- このコンポーネントは「床抜けの制御」だけを担当する
使い方(概要):
1. プレイヤーシーンの直下に、この PlatformDropper ノードを追加
2. InputMap に "move_down" と "jump" を設定
3. 親の CharacterBody2D が One Way Platform の上に立っている状態で
「↓+ジャンプ」を押すと、床をすり抜ける
"""
@export_category("Input")
## 下入力に使うアクション名(InputMap で定義しておく)
@export var down_action: StringName = "move_down"
## ジャンプ入力に使うアクション名
@export var jump_action: StringName = "jump"
@export_category("Drop Settings")
## 「↓+ジャンプ」を判定する許容時間(秒)
## 例: 0.15 なら「ほぼ同時押し」でなくてもOK
@export_range(0.0, 0.5, 0.01)
var combo_time_window: float = 0.15
## 床を無効化しておく時間(秒)
## この時間中は、One Way Platform をすり抜け続ける
@export_range(0.05, 1.0, 0.01)
var drop_duration: float = 0.25
## 対象とする「床」のコリジョンレイヤー
## One Way Platform を特定のレイヤーにまとめておくと管理しやすい
@export_flags_2d_physics var platform_collision_layer: int = 1
@export_category("Debug")
## デバッグ用にログを出すかどうか
@export var debug_log: bool = false
# 内部状態管理用
var _body: CharacterBody2D
var _is_dropping: bool = false
var _drop_timer: float = 0.0
# 「↓」が押された時間
var _last_down_press_time: float = -100.0
# 「ジャンプ」が押された時間
var _last_jump_press_time: float = -100.0
func _ready() -> void:
# 親が CharacterBody2D であることを期待
_body = get_parent() as CharacterBody2D
if _body == null:
push_warning("PlatformDropper: 親ノードが CharacterBody2D ではありません。このコンポーネントは CharacterBody2D 用です。")
# プロパティの初期値チェック
if platform_collision_layer == 0:
push_warning("PlatformDropper: platform_collision_layer が 0 です。One Way Platform のレイヤーを設定してください。")
func _physics_process(delta: float) -> void:
if _body == null:
return
# 入力の監視
_process_input_timestamps()
# 床抜け中のタイマー処理
if _is_dropping:
_drop_timer -= delta
if _drop_timer <= 0.0:
_end_drop_through()
func _process_input_timestamps() -> void:
# 下入力の押下タイミングを記録
if Input.is_action_just_pressed(down_action):
_last_down_press_time = Time.get_ticks_msec() / 1000.0
# ジャンプ入力の押下タイミングを記録
if Input.is_action_just_pressed(jump_action):
_last_jump_press_time = Time.get_ticks_msec() / 1000.0
_try_start_drop_through()
func _try_start_drop_through() -> void:
# すでに床抜け中なら、二重に開始しない
if _is_dropping:
return
# 親が床に接地していないなら、床抜けは開始しない
# (空中で「↓+ジャンプ」しても無視)
if not _body.is_on_floor():
return
# 「↓」と「ジャンプ」が combo_time_window 秒以内に押されているか判定
var now := Time.get_ticks_msec() / 1000.0
var down_diff := absf(now - _last_down_press_time)
var jump_diff := absf(now - _last_jump_press_time)
var is_combo_ok := (down_diff <= combo_time_window) and (jump_diff <= combo_time_window)
if not is_combo_ok:
return
# 実際に床抜けを開始
_start_drop_through()
func _start_drop_through() -> void:
_is_dropping = true
_drop_timer = drop_duration
# コリジョンレイヤーマスクから「床レイヤー」を一時的に外す
# これにより、One Way Platform との衝突が無効化される
var original_mask := _body.collision_mask
var new_mask := original_mask & ~platform_collision_layer
_body.collision_mask = new_mask
if debug_log:
print("PlatformDropper: start drop-through. mask ", original_mask, " -> ", new_mask)
# 親側のジャンプ処理と合わせるため、
# ここで少しだけ下方向に速度を与えることも可能。
# (必要なければコメントアウトのままでOK)
# _body.velocity.y = max(_body.velocity.y, 50.0)
func _end_drop_through() -> void:
_is_dropping = false
# 床レイヤーをマスクに戻す
# 注意:他のスクリプトが collision_mask を書き換えている場合は
# ここで衝突レイヤーを完全に復元したいなら、別途「元の値」を保存しておく必要があります。
_body.collision_mask |= platform_collision_layer
if debug_log:
print("PlatformDropper: end drop-through. mask now ", _body.collision_mask)
このスクリプトは 単独で .gd ファイルとして保存して OK です。
`class_name PlatformDropper` を定義しているので、他のシーンからも簡単にアタッチできます。
使い方の手順
① InputMap を設定する
まず、プロジェクト設定で入力を定義しておきましょう。
- メニューから 「Project > Project Settings… > Input Map」 を開く
- 以下のアクションを追加
move_down…… カーソル下キー / S キー などを割り当てjump…… スペース / Z キー などを割り当て
もし既に別名で使っている場合は、スクリプト側の down_action, jump_action を合わせて変更してください。
② One Way Platform 側の設定
床側は、例えばこんな構成を想定しています。
OneWayPlatform (StaticBody2D or TileMap) └── CollisionShape2D / TileSet Collision
- CollisionShape2D(または TileSet の Collision)に 「One Way Collision」 を有効化
- 床専用の物理レイヤーを 1 つ決めておく(例: Layer 2 を「Platform」とする)
そして、PlatformDropper 側の platform_collision_layer に、そのレイヤーを設定します。
(インスペクタ上で「Layer 2」にチェックを入れる)
③ プレイヤーシーンに PlatformDropper をアタッチする
プレイヤーのノード構成例はこんな感じです:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── PlatformDropper (Node)
- Player シーンを開く
- Player(CharacterBody2D)を選択し、右クリック → Add Child Node…
- 検索バーに「PlatformDropper」と打つと、クラス名からノードが出てくるので追加
- インスペクタから
down_action……move_downjump_action……jumpplatform_collision_layer…… One Way Platform のレイヤーcombo_time_window,drop_duration…… お好みで調整
プレイヤー側のスクリプトは、いつも通りのジャンプ処理を書いておけば OK です。
PlatformDropper は「↓+ジャンプ」が押されたタイミングで、衝突マスクだけをいい感じに切り替えてくれるイメージです。
④ 敵や動く床にも使い回す
このコンポーネントは「親が CharacterBody2D であれば誰でも OK」なので、例えば:
- 敵キャラがプレイヤーを追いかけるときに、床をすり抜けてショートカットさせたい
- 動く床(キャラ扱い)が、特定のタイミングで下に落ちるギミックを作りたい
といったケースでも、そのノードに PlatformDropper をアタッチして、
AI やスクリプト側から Input.action_press() を呼んで擬似的に「↓+ジャンプ」を押させる、という使い方もできます。
敵キャラのシーン構成例:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── PlatformDropper (Node)
メリットと応用
PlatformDropper をコンポーネントとして分離しておくと、こんなメリットがあります。
- Player.gd がスリムになる
床抜けのロジック(タイミング判定、マスク切り替え、タイマー管理など)を全部外出しできるので、
プレイヤーのコードは「移動」「攻撃」「アニメーション」など本質的な処理に集中できます。 - 敵・NPC・別プレイヤーにもそのまま使える
「床抜けできるキャラ」が増えても、PlatformDropper をアタッチして Input を切り替えるだけ。
継承ツリーを増やさずに、合成(Composition)で機能を足していけるのが気持ちいいですね。 - シーン構造がフラットで見通しが良い
深いノード階層に「FloorDropper」「JumpController」などを継承で積み上げる代わりに、
1 キャラ = 1 CharacterBody2D + 複数コンポーネント というシンプルな構成にできます。 - レベルデザイン側の自由度アップ
「このレベルでは床抜け禁止」「この敵だけ床をすり抜ける」など、
ノード単位で PlatformDropper を付け外しするだけで挙動をコントロールできます。
改造案:スクリプトから強制的に床抜けさせる
ゲームの演出で、「このトリガーに入ったら強制的に床をすり抜ける」みたいなギミックを作りたいときは、
PlatformDropper に 外部から呼べる API を 1 つ足しておくと便利です。
func force_drop_through(duration: float = -1.0) -> void:
"""
外部スクリプトから強制的に床抜けを開始させる API。
例:
- トリガーゾーンに入ったときに呼び出す
- 敵 AI が特定条件で床をすり抜けるときに呼び出す
"""
if duration > 0.0:
drop_duration = duration
_start_drop_through()
これで、例えばトリガーゾーン側から:
func _on_body_entered(body: Node2D) -> void:
var dropper := body.get_node_or_null("PlatformDropper") as PlatformDropper
if dropper:
dropper.force_drop_through(0.5)
のように呼び出せば、入力に関係なく床抜けを実行できます。
こうやってコンポーネントに小さな API を足していくと、ゲーム全体の挙動を「組み合わせ」で作れるようになっていきますね。
継承ベースで「PlayerFloorDropper」「EnemyFloorDropper」みたいなクラスを増やしていくよりも、
今回のように PlatformDropper コンポーネントとして切り出しておくと、後からの拡張・再利用が圧倒的にラクになります。
ぜひ、自分のプロジェクトでも「床抜け」機能を合成スタイルで導入してみてください。




