Godot 4 でキャラクターの「坂道挙動」を作ろうとすると、けっこう面倒ですよね。
CharacterBody2D / CharacterBody3D の move_and_slide() は便利ですが、
- 急な坂だけ滑り落ちてほしい
- でも緩い坂では普通に立ち止まりたい
- キャラごとに「滑りやすさ」を変えたい
…といった細かい調整をしようとすると、
- プレイヤーのスクリプトが「坂判定」「角度計算」「追加重力」などでパンパンになる
- 敵キャラや動く床ごとにコピペしてカスタマイズ → どこを直せばいいか分からなくなる
という「継承地獄」「肥大化スクリプト」コースに入りがちです。
そこで今回は、「急な坂にいるときだけ、重力を強くして自動で滑り落ちる」挙動を、
どのキャラクターにもポン付けできる コンポーネント として切り出してみましょう。
プレイヤーでも敵でも、CharacterBody2D にこのコンポーネントを 1 個アタッチするだけで、
「急斜面でズルズル滑り落ちる」挙動を簡単に再利用できるようにします。
【Godot 4】急斜面でズルズル滑ろう!「SlopeSlider」コンポーネント
今回の SlopeSlider コンポーネントは、ざっくり言うと次のようなことをします。
- 親ノード が
CharacterBody2Dであることを前提にする _physics_process()で「接地&坂の角度」を判定- 一定以上の角度の坂に乗っていたら、「滑りベクトル」を計算して
velocityに加算 - 「滑り出す角度」「滑りの強さ」「Y軸の重力増加量」などを
@exportで調整可能
プレイヤー本体のスクリプトは「入力処理」に集中させて、
坂の物理挙動はすべて SlopeSlider に丸投げする構成ですね。
フルコード: SlopeSlider.gd
extends Node
class_name SlopeSlider
## 急な坂にいるとき、重力を強くして滑り落ちるコンポーネント(2D用)
##
## 想定する親ノード:
## - CharacterBody2D
##
## 使い方:
## 1. 親に CharacterBody2D を置く
## 2. 子としてこの SlopeSlider をアタッチ
## 3. プレイヤー側では「通常の移動と重力」だけ書く
## 4. 坂判定と滑り挙動はこのコンポーネントに任せる
@export_group("基本設定")
## 何度以上の坂を「滑る坂」とみなすか(度数法)
## 例: 30〜45 あたりがゲームらしい値になりやすいです。
@export_range(0.0, 89.0, 0.1)
var slide_angle_threshold_degrees: float = 40.0
## 坂に沿って滑る力の強さ(水平成分)
## 値を大きくすると、急斜面で一気に加速していく感じになります。
@export_range(0.0, 5000.0, 1.0)
var slide_force: float = 800.0
## 滑っているときに追加でかかる重力(垂直成分)
## 「坂から早く落ちてほしい」「吸い付くように張り付いてほしい」などの調整用。
@export_range(0.0, 5000.0, 1.0)
var extra_gravity_while_sliding: float = 400.0
## 滑り中の最大速度(坂に沿った方向)
## 0 以下にすると無制限になります。
@export_range(0.0, 10000.0, 1.0)
var max_slide_speed: float = 1200.0
@export_group("ブレーキ設定")
## 入力による「逆方向移動」がどれくらい滑りを打ち消すかの係数
## 1.0 で「プレイヤー入力をそのまま尊重」
## 0.0 で「滑りを完全に優先(入力では止めづらい)」
@export_range(0.0, 1.0, 0.05)
var input_respect_factor: float = 0.5
## 滑り中に少しだけ摩擦を入れて、無限に加速しないようにする係数
## 0.0 で摩擦なし、1.0 に近づくほどすぐ減速します。
@export_range(0.0, 1.0, 0.01)
var slide_friction: float = 0.05
@export_group("デバッグ")
## デバッグ用: 現在「滑り中」かどうか
@export var debug_is_sliding: bool = false : set = _set_debug_is_sliding
## デバッグ表示用に色を変えたい場合などに利用できます
@export var debug_print: bool = false
# 内部参照: 親の CharacterBody2D
var _body: CharacterBody2D
func _ready() -> void:
# 親が CharacterBody2D かどうかをチェック
_body = get_parent() as CharacterBody2D
if _body == null:
push_warning("SlopeSlider must be a child of CharacterBody2D. Current parent: %s" % [get_parent()])
set_physics_process(false)
return
func _physics_process(delta: float) -> void:
if _body == null:
return
# 接地していないときは何もしない(自由落下中など)
if not _body.is_on_floor():
debug_is_sliding = false
return
# CharacterBody2D の floor_normal を使用して床の傾きを取得
# 通常、上方向は (0, -1) なので、床が水平なら floor_normal ≒ (0, -1) になります。
var floor_normal: Vector2 = _body.get_floor_normal()
# 法線ベクトルから「床の角度」を計算
# 上向きベクトル (0, -1) との角度差をとることで「どれくらい傾いているか」を算出します。
var up: Vector2 = Vector2.UP
var angle_radians := floor_normal.angle_to(-up) # -up = (0, -1)
var angle_degrees := rad_to_deg(angle_radians)
# 閾値未満の坂では滑らない
if angle_degrees < slide_angle_threshold_degrees:
debug_is_sliding = false
return
# ここから「滑り坂」と判断
debug_is_sliding = true
# 床の法線から「坂に沿った接線方向」を計算
# 法線を 90 度回転させることで接線ベクトルを得られます。
var slope_tangent: Vector2 = Vector2(-floor_normal.y, floor_normal.x).normalized()
# 坂の「下り方向」を決める(重力方向に近い方を下りとみなす)
var gravity_dir: Vector2 = Vector2.DOWN
if slope_tangent.dot(gravity_dir) < 0.0:
slope_tangent = -slope_tangent
# 現在の速度を取得
var velocity: Vector2 = _body.velocity
# 坂に沿った現在の速度成分(スカラー)を取得
var current_speed_along_slope: float = velocity.dot(slope_tangent)
# 坂に沿って加速させる(slide_force を delta でスケーリング)
var added_speed: float = slide_force * delta
var new_speed_along_slope: float = current_speed_along_slope + added_speed
# 最大速度制限
if max_slide_speed > 0.0:
new_speed_along_slope = clamp(new_speed_along_slope, -max_slide_speed, max_slide_speed)
# 坂に沿った速度ベクトルを再構築
var slide_velocity: Vector2 = slope_tangent * new_speed_along_slope
# 「もともとの速度」と「滑り速度」をブレンドする
# - 水平方向: 坂に沿った速度を上書きしつつ、プレイヤー入力を少し残す
# - 垂直方向: 追加重力をかけて落ちやすくする
#
# ここでは、
# velocity = velocity - (坂成分) + (slide_velocity * (1.0 - input_respect_factor))
# のようなイメージで、プレイヤー入力を完全には殺さないようにします。
# 現在の速度から「坂に沿った成分」を抜き出す
var current_slope_component: Vector2 = slope_tangent * current_speed_along_slope
var velocity_without_slope: Vector2 = velocity - current_slope_component
# 入力をどれくらい尊重するかでブレンド
var blended_slope_velocity: Vector2 = lerp(
current_slope_component,
slide_velocity,
1.0 - input_respect_factor
)
velocity = velocity_without_slope + blended_slope_velocity
# 垂直方向(Y軸)に追加重力を加える
velocity.y += extra_gravity_while_sliding * delta
# 摩擦による減速(坂に沿った方向)
if slide_friction > 0.0:
var friction_factor := 1.0 - slide_friction
# 坂成分にだけ摩擦を適用
var slope_only: Vector2 = slope_tangent * velocity.dot(slope_tangent)
var non_slope: Vector2 = velocity - slope_only
slope_only *= friction_factor
velocity = non_slope + slope_only
# 計算した速度を親の CharacterBody2D に反映
_body.velocity = velocity
if debug_print:
print("SlopeSlider: angle=%.2f, sliding=%s, vel=%s" % [angle_degrees, str(debug_is_sliding), str(_body.velocity)])
func _set_debug_is_sliding(value: bool) -> void:
debug_is_sliding = value
# ここで Sprite の色を変えたり、エフェクトを出したりしても良い
# 例:
# var sprite := _body.get_node_or_null("Sprite2D") as Sprite2D
# if sprite:
# sprite.modulate = value ? Color(1, 0.8, 0.8) : Color(1, 1, 1)
使い方の手順
-
スクリプトをプロジェクトに追加
上記コードをres://components/SlopeSlider.gdなどのパスで保存します。
class_name SlopeSliderを定義しているので、エディタの「ノード追加」から直接追加できます。 -
プレイヤーシーンにアタッチ
例として、2D 横スクロールのプレイヤーを考えます。Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SlopeSlider (Node)Playerは通常どおりCharacterBody2Dとして作成- 子ノードとして
SlopeSlider(Nodeベース)を追加 SlopeSliderのスクリプトに、先ほどのSlopeSlider.gdを指定
-
プレイヤー側の移動スクリプトを書く(シンプルでOK)
プレイヤーのスクリプトは「入力と基礎重力」だけに集中させます。# Player.gd extends CharacterBody2D @export var move_speed: float = 220.0 @export var base_gravity: float = 900.0 @export var jump_speed: float = -360.0 func _physics_process(delta: float) -> void: var input_dir := Input.get_axis("ui_left", "ui_right") # 水平移動 velocity.x = move_speed * input_dir # 基本の重力 if not is_on_floor(): velocity.y += base_gravity * delta # ジャンプ if Input.is_action_just_pressed("ui_accept") and is_on_floor(): velocity.y = jump_speed # 実際の移動 move_and_slide()坂の角度判定・滑り処理はすべて
SlopeSliderが裏でvelocityをいじってくれるので、
プレイヤー側のコードはかなりスッキリします。 -
パラメータを調整して好みの挙動にする
エディタでSlopeSliderノードを選ぶと、インスペクタに以下の項目が出ます。slide_angle_threshold_degrees… この角度以上の坂で滑る(例: 35〜45)slide_force… 坂に沿った滑りの強さ(例: 600〜1000)extra_gravity_while_sliding… 滑り中の追加重力(例: 300〜600)max_slide_speed… 滑りの最高速度input_respect_factor… プレイヤー入力をどれだけ尊重するかslide_friction… 滑り中の摩擦(ブレーキ)
レベル内の坂を実際に歩かせながら、
「ここはギリギリ登れる」「ここからはズルズル落ちる」といった気持ちよさを探っていきましょう。
別の使用例: 敵キャラや動く床にも
同じコンポーネントを、敵キャラや特殊なオブジェクトにもそのまま付けられます。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SlopeSlider (Node)
この場合、敵の移動ロジック(パトロール・プレイヤー追跡など)は Enemy.gd に書き、
坂で滑る挙動はすべて SlopeSlider に任せる、という分担になります。
「動く氷床」のようなものを作る場合も、CharacterBody2D ベースで坂に沿って動かしつつ、
このコンポーネントを付けて「自重でずれていく床」なども表現できます。
メリットと応用
この SlopeSlider コンポーネントを使うメリットを整理すると:
- プレイヤー/敵スクリプトから「坂ロジック」を完全に追い出せる
→ 移動コードがシンプルになり、デバッグしやすいです。 - シーン構造は浅いまま、「挙動」をコンポーネントで足していける
→ Godot でありがちな「ノード継承ツリーが深くなりすぎる」問題を避けられます。 - パラメータだけでキャラごとの個性を出せる
→ 同じSlopeSliderを使いながら、「重い敵は滑りにくい」「氷の精霊はめちゃ滑る」といった差別化が簡単です。 - レベルデザインの調整がやりやすい
→ 「このステージは全体的に滑りやすくしたい」なら、シーン内のSlopeSliderのパラメータを一括調整するだけでOKです。
「継承で PlayerSlopeWalker, EnemySlopeWalker… を作りまくる」のではなく、
「共通の SlopeSlider をコンポーネントとしてアタッチする」構成にすることで、
プロジェクト全体の見通しがかなり良くなりますね。
改造案: 「滑り中だけアニメーションを切り替える」
例えば、「滑っているときだけアニメーションを変えたい」場合は、
SlopeSlider にコールバックを 1 個足すだけで簡単に対応できます。
# SlopeSlider.gd 内に追加(例)
signal sliding_state_changed(is_sliding: bool)
var _prev_sliding: bool = false
func _physics_process(delta: float) -> void:
# ...(中略)坂判定と debug_is_sliding の更新まで終わった後
if debug_is_sliding != _prev_sliding:
_prev_sliding = debug_is_sliding
emit_signal("sliding_state_changed", debug_is_sliding)
# 以降、velocity 計算などの処理...
そしてプレイヤー側では:
# Player.gd
func _ready() -> void:
var slider := get_node("SlopeSlider") as SlopeSlider
slider.sliding_state_changed.connect(_on_sliding_state_changed)
func _on_sliding_state_changed(is_sliding: bool) -> void:
var anim := $AnimatedSprite2D
if is_sliding:
anim.play("slide")
else:
anim.play("idle")
こんな感じで、「坂滑り」という 1 つの振る舞いをコンポーネントとして独立させておくと、
アニメーションやエフェクトとの連携もシグナルベースで気持ちよく組めるようになります。
継承より合成、どんどん進めていきましょう。




