Unityを触り始めた頃によくやってしまうのが、「Update にプレイヤーの移動・ジャンプ・カメラ制御・アニメーション・当たり判定…全部まとめて書いてしまう」パターンですね。
動き始めたときは嬉しいのですが、あとから「坂道で滑らせたい」「氷の床でツルツルさせたい」といった要件が出てくると、巨大なスクリプトの中から関連コードを探して条件分岐を追加するハメになり、あっという間にカオス化します。
そこでこの記事では、「坂道でだけ滑り落ちる挙動」を専用コンポーネントとして切り出した 「SlopeSlider」 を紹介します。
プレイヤーや敵キャラのベース移動ロジックには手を入れず、「急な坂道にいるときだけ重力を強くして滑り落ちる」処理を独立したコンポーネントとして追加することで、責務の分離と再利用性を両立させましょう。
【Unity】急斜面では容赦なく滑る!「SlopeSlider」コンポーネント
以下は、Rigidbody ベースのキャラクターに「急な坂道で滑り落ちる挙動」を追加するための、完全な C# スクリプトです。
傾斜角度のしきい値・滑る力の強さ・接地判定の方法などをインスペクターから調整できるようにしています。
using UnityEngine;
/// <summary>
/// 急な坂道にいるとき、重力方向に追加のスライド力を与えて
/// 「坂を滑り落ちる」挙動を付与するコンポーネント。
///
/// ・Rigidbody を利用した物理ベースのキャラクター向け
/// ・接地判定は足元への Raycast で行う
/// ・法線ベクトルから傾斜角を計算し、しきい値を超えたらスライド
///
/// 責務:
/// 「坂道で滑る力だけ」を担当し、移動入力やジャンプ処理などとは切り離す。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class SlopeSlider : MonoBehaviour
{
[Header("基本設定")]
[SerializeField]
private LayerMask groundLayer = ~0; // どのレイヤーを「地面」とみなすか
[Tooltip("接地判定に使う Ray の長さ(キャラクターの足元から下方向)")]
[SerializeField]
private float groundCheckDistance = 0.6f;
[Tooltip("どの角度以上を「急な坂」とみなすか(度数法)")]
[SerializeField, Range(0f, 89f)]
private float steepSlopeAngle = 35f;
[Header("スライド挙動")]
[Tooltip("急斜面にいるときに追加でかけるスライド力の強さ")]
[SerializeField]
private float slideForce = 20f;
[Tooltip("水平面方向の速度がこの値以下なら、滑り始めるのを少し抑える(0で無効)")]
[SerializeField]
private float minHorizontalSpeedToSlide = 0f;
[Tooltip("急斜面にいるとき、垂直方向の速度をこの値以下に抑える(0で無効)")]
[SerializeField]
private float maxFallSpeedOnSlope = 0f;
[Header("デバッグ表示")]
[SerializeField]
private bool drawDebugGizmos = true;
// 内部参照
private Rigidbody _rb;
// 足元のヒット情報をキャッシュ(必要に応じて外部から参照してもOK)
private bool _isGrounded;
private RaycastHit _groundHit;
private float _currentSlopeAngle;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
// Rigidbody の設定の一例(プロジェクトに応じて調整)
// スライド挙動を自然に見せるため、回転は物理で転がらないように固定することが多いです。
_rb.freezeRotation = true;
}
private void FixedUpdate()
{
// 物理挙動は FixedUpdate で行う
CheckGround();
ApplySlopeSliding();
}
/// <summary>
/// 足元への Raycast で接地判定と地面の法線・傾斜角を取得する
/// </summary>
private void CheckGround()
{
Vector3 origin = transform.position;
Vector3 direction = Vector3.down;
_isGrounded = Physics.Raycast(
origin,
direction,
out _groundHit,
groundCheckDistance,
groundLayer,
QueryTriggerInteraction.Ignore
);
if (_isGrounded)
{
// 地面の法線ベクトルと「上方向ベクトル(Vector3.up)」の角度から傾斜角を算出
_currentSlopeAngle = Vector3.Angle(_groundHit.normal, Vector3.up);
}
else
{
_currentSlopeAngle = 0f;
}
}
/// <summary>
/// 急な坂にいる場合、重力方向に沿って滑り落ちる力を加える
/// </summary>
private void ApplySlopeSliding()
{
if (!_isGrounded)
{
// 空中にいるときは何もしない(別コンポーネントに任せる)
return;
}
// 傾斜が急でなければスライドしない
if (_currentSlopeAngle < steepSlopeAngle)
{
return;
}
// 地面の法線ベクトルから「坂に沿った滑り方向」を計算する
// 1. 法線ベクトルを上方向に投影して「坂の上方向ベクトル」を求める
// 2. その逆方向が「坂を下る方向」になる
Vector3 slopeNormal = _groundHit.normal;
// 地面の法線と重力方向から、坂に沿った滑りベクトルを求める
// Vector3.ProjectOnPlane を使うと「ある平面上に投影されたベクトル」が得られる
// 今回は「地面の法線を法線とする平面」に重力ベクトルを投影 → その逆方向が滑り方向
Vector3 gravity = Physics.gravity;
Vector3 gravityAlongSlope = Vector3.ProjectOnPlane(gravity, slopeNormal);
if (gravityAlongSlope.sqrMagnitude < 0.0001f)
{
// ほぼ水平 or 数値的に不安定な場合は処理しない
return;
}
Vector3 slideDirection = gravityAlongSlope.normalized;
// オプション:水平速度が小さいときはスライドを弱める(立ち止まりやすくする)
if (minHorizontalSpeedToSlide > 0f)
{
Vector3 horizontalVelocity = new Vector3(_rb.velocity.x, 0f, _rb.velocity.z);
if (horizontalVelocity.magnitude < minHorizontalSpeedToSlide)
{
// 速度が小さいときはスライド力を半減させるなど、調整も可能
slideDirection *= 0.5f;
}
}
// 実際に力を加える
// ForceMode.Acceleration を使うと「質量に関係なく一定の加速度」として扱える
_rb.AddForce(slideDirection * slideForce, ForceMode.Acceleration);
// オプション:急斜面での落下速度を抑える(真下に落ちすぎると不自然に見える場合)
if (maxFallSpeedOnSlope > 0f)
{
Vector3 v = _rb.velocity;
if (v.y < -maxFallSpeedOnSlope)
{
v.y = -maxFallSpeedOnSlope;
_rb.velocity = v;
}
}
}
#region デバッグ用 Gizmo 表示
private void OnDrawGizmosSelected()
{
if (!drawDebugGizmos)
{
return;
}
// 足元の Raycast をシーンビューに可視化
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, transform.position + Vector3.down * groundCheckDistance);
// 実行中のみ、地面の法線と滑り方向を表示
if (Application.isPlaying && _isGrounded)
{
// 法線ベクトル
Gizmos.color = Color.green;
Gizmos.DrawLine(_groundHit.point, _groundHit.point + _groundHit.normal);
// 滑り方向(計算を再利用)
Vector3 gravity = Physics.gravity;
Vector3 gravityAlongSlope = Vector3.ProjectOnPlane(gravity, _groundHit.normal);
if (gravityAlongSlope.sqrMagnitude > 0.0001f)
{
Vector3 slideDir = gravityAlongSlope.normalized;
Gizmos.color = Color.red;
Gizmos.DrawLine(_groundHit.point, _groundHit.point + slideDir);
}
}
}
#endregion
}
使い方の手順
ここでは、プレイヤーキャラクターに坂道滑り挙動を追加する例を中心に、敵キャラや動く床などへの応用も交えて解説します。
-
① Rigidbody ベースのキャラクターを用意する
すでに Rigidbody を使ったプレイヤーや敵キャラがある場合は、そのオブジェクトを使いましょう。
まだない場合は、以下のような最低限のセットアップをします。- 空の GameObject を作成し、「Player」などの名前をつける
Rigidbodyコンポーネントを追加する- Use Gravity: ON
- Freeze Rotation: X, Y, Z をチェック(転がらないように)
- Capsule Collider など、キャラクターの当たり判定を追加する
-
② 「SlopeSlider.cs」を作成してアタッチする
- プロジェクトビューで
Scriptsフォルダなどを作成し、右クリック > Create > C# Script - 名前を SlopeSlider に変更
- スクリプトを開き、この記事のコードを丸ごとコピペして保存
- Player オブジェクトに
SlopeSliderコンポーネントをアタッチするRequireComponent(typeof(Rigidbody))により、自動で Rigidbody が要求されます
- プロジェクトビューで
-
③ 地形(坂道)を用意する
- Plane や ProBuilder、Terrain などを使って坂道を作成する
- 急な坂にしたい箇所は、steepSlopeAngle より大きな角度になるように傾ける
- 例:
steepSlopeAngle = 35にしたい場合、45度の坂は「急斜面」とみなされ滑り落ちます
- 例:
- 地形のオブジェクトは「地面レイヤー(Default でも可)」に設定しておく
groundLayerをEverything(デフォルト)にしておけば、特にレイヤー設定を変えなくても動きます。
「地面だけに反応させたい」場合は、専用レイヤー(例: Ground)を作成してgroundLayerに指定しましょう。 -
④ パラメータを調整して挙動を確認する
Steep Slope Angle(急斜面とみなす角度)- 30〜40 度あたりが扱いやすいです
- 値を小さくすると、緩い坂でも滑りやすくなります
Slide Force(滑り落ちる力の強さ)- 10〜30 あたりから試すと良いです
- 大きくしすぎると、ロケットのように加速するので注意
Min Horizontal Speed To Slide- 0 のままなら、停止していても急斜面に立っているだけで滑ります
- 1〜2 などにすると、「少し動き出したときにだけ滑る」ような感覚になります
Max Fall Speed On Slope- 0 のままなら制限なし(物理挙動に任せる)
- 5〜10 などにすると、急斜面でも落下速度を抑えられます
シーンビューでオブジェクトを選択すると、足元の Ray や 地面の法線、滑り方向 が Gizmo で表示されるので、
「どの角度で急斜面になっているか」「どちら向きに滑ろうとしているか」を視覚的に確認できます。
敵キャラへの応用例:
敵にも同じ SlopeSlider をアタッチしておけば、プレイヤーと同じルールで坂道を滑るようになります。
移動 AI は別コンポーネント(例: EnemyPatrol)に分けておき、「坂で滑るかどうか」は SlopeSlider に完全に任せられるのがポイントです。
動く床への応用例:
動く床自体に SlopeSlider を付けるのではなく、「プレイヤー側にだけ」付けておくことで、
エレベーターや動く傾斜ステージでも、プレイヤーが自然に滑り落ちるように制御できます。
床は単に Rigidbody 付きのオブジェクトとして動かしておけば OK です。
メリットと応用
責務を分けることのメリット
- プレイヤー移動スクリプトを汚さない
「入力に応じた移動」「ジャンプ」「ダッシュ」などはPlayerMovementなどのコンポーネントに任せ、
「急斜面で滑る」という物理的な補正はSlopeSliderに閉じ込めることで、コードの見通しが良くなります。 - プレハブ単位で挙動を切り替えられる
同じ移動ロジックを使っていても、SlopeSliderを付けるかどうかで「坂で踏ん張れるキャラ」「すぐ滑るキャラ」を簡単に作り分けられます。
例えば:- プレイヤー:
SlopeSliderあり(坂で滑る) - ボス:
SlopeSliderなし(どんな坂でもどっしり立つ)
- プレイヤー:
- レベルデザインがやりやすい
「この坂はギリギリ登れる」「ここから先は滑り落ちる」など、角度とパラメータだけでゲーム性をコントロールできます。
スクリプトを書き換えずに、シーン上で角度を変えるだけで難易度調整できるのは大きなメリットですね。
応用アイデア
- 氷のステージ専用のスライダー
groundLayerを「Ice」レイヤーに限定したIceSlopeSliderを派生させれば、
氷の上だけ極端に滑るようなギミックも簡単に作れます。 - 坂の種類ごとに滑りやすさを変える
地形側に「摩擦係数」的なスクリプトを付けておき、SlopeSliderがそれを読み取ってslideForceを変える、といった発展も可能です。
簡単な改造案:
「このフレームで実際にスライドしているかどうか」を外部から知りたい場合、以下のようなメソッドを SlopeSlider に追加すると便利です。
/// <summary>
/// 現在、急な坂の上でスライドしているかどうかを返す
/// (他コンポーネントからの参照用)
/// </summary>
public bool IsSliding()
{
// 接地していて、傾斜角がしきい値を超えていれば「スライド中」とみなす
return _isGrounded && _currentSlopeAngle >= steepSlopeAngle;
}
これを使えば、Animator のパラメータを切り替えて「滑りモーション」を再生したり、
スライド中だけ足音を変える、といった演出コンポーネントと連携しやすくなります。
このように、小さな責務ごとにコンポーネントを分けておくと、後からの拡張が本当に楽になりますね。
