横スクロールアクションを作っていると、プレイヤーが「天井の角」に頭をぶつけて、そこにガチッと引っかかってしまう問題、よくありますよね。
Godot標準の CharacterBody2D でも、move_and_slide() を使うだけだと、こうした「角に引っかかる」挙動は自動では解決してくれません。
よくある対処としては:
- プレイヤーのコリジョン形状を丸くする(でも足場との判定が甘くなる)
- マップ側のタイルコリジョンを細かく調整する(レベルデザインが地獄)
- プレイヤースクリプトに「角補正ロジック」をベタ書きする(継承ツリーが太っていく)
どれも一長一短で、「別キャラを作るたびに同じロジックをコピペ」みたいな事態になりがちです。
そこで今回は、「角補正」だけを独立したコンポーネントとして切り出し、必要なキャラにポン付けできるようにしてみましょう。
【Godot 4】天井の角に引っかからない!「CornerCorrection」コンポーネント
この CornerCorrection コンポーネントは:
- ジャンプ中に天井の角へ頭をぶつけたとき、
- 少しだけ左右にスライドさせて、
- 「スルッ」と通り抜けやすくする
という、プラットフォーマーではおなじみの「角補正」機能を提供します。
プレイヤー本体は CharacterBody2D のまま、移動ロジックも今まで通り。
「角補正したいキャラ」にだけ、このコンポーネントをアタッチしてあげる構成です。
フルコード:CornerCorrection.gd
extends Node2D
class_name CornerCorrection
##
## CornerCorrection (角補正) コンポーネント
##
## ・親ノードに CharacterBody2D / CharacterBody3D を想定
## ・天井に頭をぶつけたときに、左右へ微調整して「角抜け」しやすくする
## ・移動処理そのものは親ノード側で行う前提
##
@export var enabled: bool = true:
set(value):
enabled = value
## どれくらい横にずらすか(ピクセル)
@export_range(0.0, 64.0, 0.5, "or_greater")
var correction_distance: float = 8.0
## 角補正を試す最大回数(左右合計)
## 小さくすると安全だが、引っかかりやすくなる
@export_range(1, 10, 1)
var max_attempts: int = 2
## 角補正を行う高さ範囲(頭から何ピクセル以内の障害物を対象にするか)
@export_range(0.0, 64.0, 0.5, "or_greater")
var vertical_tolerance: float = 6.0
## 補正を行う条件:
## ・親の速度.y がこの値より小さい(上向きにジャンプしている)ときのみ実行
@export var max_vertical_speed_for_correction: float = -10.0
## RayCast2D で天井ヒットを判定するかどうか
## false の場合は、CharacterBody2D の is_on_ceiling() だけを頼りにする
@export var use_raycast_check: bool = true
## RayCast 用:天井方向に飛ばす長さ
@export_range(0.0, 64.0, 0.5, "or_greater")
var ceiling_check_distance: float = 6.0
## RayCast 用:左右にオフセットした位置からもチェックする
@export_range(0.0, 32.0, 0.5, "or_greater")
var side_offset: float = 4.0
## どのレイヤーを「天井」とみなすか(PhysicsLayers)
@export_flags_2d_physics var ceiling_collision_mask: int = 1
## デバッグ描画(エディタ上で補正方向などを可視化)
@export var debug_draw: bool = false:
set(value):
debug_draw = value
queue_redraw()
var _body: CharacterBody2D = null
var _space_state: PhysicsDirectSpaceState2D
func _ready() -> void:
# 親ノードが CharacterBody2D であることを期待
_body = get_parent() as CharacterBody2D
if _body == null:
push_warning("CornerCorrection: 親ノードが CharacterBody2D ではありません。このコンポーネントは無効になります。")
enabled = false
return
_space_state = get_world_2d().direct_space_state
func _physics_process(delta: float) -> void:
if not enabled:
return
if _body == null:
return
# 上向きに動いていないなら、角補正は不要
if _body.velocity.y > max_vertical_speed_for_correction:
return
# すでに天井に接触しているか、RayCast で天井を検出したときのみ補正を試みる
if not _is_hitting_ceiling():
return
# 実際に左右へずらす
_try_corner_correction()
# デバッグ描画更新
if debug_draw:
queue_redraw()
func _is_hitting_ceiling() -> bool:
# CharacterBody2D の is_on_ceiling() をまずチェック
if _body.is_on_ceiling():
return true
if not use_raycast_check:
return false
# RayCast で頭上の障害物を判定
var up_dir := -Vector2.UP
var origin_global := _body.global_position
var ray_from := origin_global
var ray_to := origin_global + up_dir * ceiling_check_distance
var result := _space_state.intersect_ray(
PhysicsRayQueryParameters2D.create(ray_from, ray_to, ceiling_collision_mask, [_body])
)
if result:
return true
# 左右オフセット位置からもチェック
var left_from := origin_global + Vector2(-side_offset, 0.0)
var left_to := left_from + up_dir * ceiling_check_distance
result = _space_state.intersect_ray(
PhysicsRayQueryParameters2D.create(left_from, left_to, ceiling_collision_mask, [_body])
)
if result:
return true
var right_from := origin_global + Vector2(side_offset, 0.0)
var right_to := right_from + up_dir * ceiling_check_distance
result = _space_state.intersect_ray(
PhysicsRayQueryParameters2D.create(right_from, right_to, ceiling_collision_mask, [_body])
)
return bool(result)
func _try_corner_correction() -> void:
# すでに頭がめり込んでいる場合など、無理に動かさないための安全策として
# 少しだけ上方向へオフセットして判定する
var base_pos := _body.global_position
var up_offset := Vector2(0, -vertical_tolerance)
# 左右両方向について、補正を試す
var directions := [Vector2.LEFT, Vector2.RIGHT]
for dir in directions:
for i in range(1, max_attempts + 1):
var offset_amount := correction_distance * float(i) / float(max_attempts)
var offset := dir * offset_amount
var from := base_pos + up_offset
var to := from + offset
var result := _space_state.intersect_ray(
PhysicsRayQueryParameters2D.create(from, to, ceiling_collision_mask, [_body])
)
# Ray が何もヒットしなければ、その方向には障害物がないとみなしてスライド
if not result:
_body.global_position += offset
return
# どの方向にも空きがなかった場合は、何もしない
func _draw() -> void:
if not debug_draw or _body == null:
return
var base_pos := to_local(_body.global_position)
var up_offset := Vector2(0, -vertical_tolerance)
# 上方向ライン
draw_line(base_pos, base_pos + up_offset, Color.YELLOW, 1.0)
# 左右補正方向ライン
var left_offset := Vector2.LEFT * correction_distance
var right_offset := Vector2.RIGHT * correction_distance
draw_line(base_pos + up_offset, base_pos + up_offset + left_offset, Color.AQUA, 1.0)
draw_line(base_pos + up_offset, base_pos + up_offset + right_offset, Color.AQUA, 1.0)
# 原点マーカー
draw_circle(base_pos, 2.0, Color.RED)
使い方の手順
基本的には「プレイヤー(や敵)」の子ノードとして、このコンポーネントをアタッチするだけです。
手順①:スクリプトを用意する
CornerCorrection.gd をプロジェクトに追加し、上記コードをそのまま保存します。
(ファイル名と class_name CornerCorrection が一致していれば、エディタの「ノードを追加」から検索できます)
手順②:プレイヤーシーンにアタッチ
例として、2Dアクション用のプレイヤーシーン構成はこんな感じにします:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CornerCorrection (Node2D)
Playerノードに、いつも通り移動ロジック(move_and_slide()など)を書いたスクリプトを付けておきます。- その子として
CornerCorrectionノードを追加し、CornerCorrection.gdをアタッチします。
プレイヤースクリプト側で特別な呼び出しは不要です。CornerCorrection は自前の _physics_process() 内で、親の CharacterBody2D を監視して補正を行います。
手順③:パラメータを調整する
Inspector からコンポーネントを選択し、以下のパラメータをゲームに合わせて調整します:
correction_distance:どれくらい横にずらすか(8〜16px くらいから試すと良いです)max_attempts:左右それぞれ何段階まで距離を増やして試すかvertical_tolerance:頭のどの範囲を「角」とみなすか。大きくしすぎると不自然にスライドすることがあります。max_vertical_speed_for_correction:どれくらいの上向き速度まで補正を許可するか。あまり高速ジャンプ中に補正すると、ワープ感が出るので注意。ceiling_collision_mask:どのレイヤーのコリジョンを「天井」とみなすか。タイルマップのレイヤーに合わせて設定しましょう。debug_draw:オンにすると、エディタ上で補正方向のラインが見えるので調整に便利です。
手順④:具体的な使用例
例1:プレイヤーキャラ
典型的な 2D プラットフォーマーのプレイヤーに適用する例です。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Camera2D └── CornerCorrection (Node2D)
この構成にしておくと、マップの天井タイルの角に頭をぶつけたときに、自動的に左右へスライドしてくれるので、
「ジャンプしたのに角でガクッと止められてストレス」という状況をかなり減らせます。
例2:天井にぶら下がる敵
天井近くを移動する敵キャラ(コウモリなど)にも同じコンポーネントを使い回せます:
BatEnemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CornerCorrection (Node2D)
この敵が天井付近を左右に移動しているとき、微妙なタイルの角に引っかかるのを避けられます。
プレイヤーとは違う調整がしたければ、correction_distance や max_attempts を別の値にしておけば OK です。
例3:動く床
「プレイヤーが乗れる動く床」が天井付近を通過するとき、天井との角に引っかかって止まってしまうケースにも応用できます。
MovingPlatform (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CornerCorrection (Node2D)
この場合は、max_vertical_speed_for_correction を 0 以上にして、
「上方向に移動しているときだけ角補正をする」といった使い方もできます。
メリットと応用
この CornerCorrection コンポーネントを使う最大のメリットは、「プレイヤーの移動ロジック」と「角補正ロジック」を完全に分離できることです。
- プレイヤーのスクリプトは「入力 → 速度計算 → move_and_slide()」に集中できる
- 角補正の有無は、ノードを付けるかどうかで切り替えられる
- プレイヤー以外のキャラ(敵、動く床、ギミック)にも、そのままポン付けできる
- シーン構造は浅いまま、機能ごとにコンポーネントを足していくスタイルにできる
「継承ツリーにロジックを全部詰め込む」のではなく、「合成(Composition)で機能を組み合わせる」方向に舵を切れるのがいいですね。
Godot はノードベースなので、こうしたコンポーネント指向の設計と相性がとても良いです。
レベルデザインの観点でも:
- マップ側のタイルコリジョンを「完璧な角」にしなくても、ある程度ラフで済む
- プレイヤーの「引っかかり」を減らすことで、操作感がかなり良くなる
- テスト中に「この敵だけ角補正オフにしたい」といった調整も簡単
と、かなり恩恵があります。
改造案:一方向だけに角補正を許可する
たとえば「右方向にだけスライドさせたい」「プレイヤーの向いている方向にだけ補正したい」といったケースもあります。
その場合は、次のようなメソッドを追加すると簡単にカスタマイズできます。
## プレイヤーの「向き」に応じて補正方向を制限する例
@export var use_facing_direction: bool = false
@export var facing_right: bool = true
func set_facing_right(value: bool) -> void:
facing_right = value
func _get_correction_directions() -> Array:
if not use_facing_direction:
# デフォルトは左右両方を試す
return [Vector2.LEFT, Vector2.RIGHT]
# 向きに応じて片側だけを返す
return [Vector2.RIGHT] if facing_right else [Vector2.LEFT]
そして、_try_corner_correction() 内の
var directions := [Vector2.LEFT, Vector2.RIGHT]
を
var directions := _get_correction_directions()
に差し替えれば、プレイヤーの向きに応じた「片側のみ角補正」ができるようになります。
このように、コンポーネントとして独立していると、機能追加や改造もかなりやりやすいですね。
ぜひ、自分のプロジェクト用にパラメータやロジックをちょっとずつ変えながら、「気持ちいいジャンプ」の感触を追求してみてください。




