Unityを触り始めると、つい何でもかんでも Update() に書いてしまいがちですよね。
プレイヤーの移動、アニメーション、当たり判定、エフェクト、UI更新…全部が1つのスクリプトに詰め込まれた「Godクラス」になってしまうと、次のような問題が起きやすくなります。

  • ちょっとした修正でも巨大なスクリプトを延々スクロールする必要がある
  • 責務が混ざり合っていて、バグの原因箇所を特定しづらい
  • 別のプロジェクトや別のキャラクターに流用しづらい

足元の影も、そうなりがちな代表例です。
「ジャンプ処理のついでに影も動かすか…」と1つのスクリプトに押し込むと、影のロジックとジャンプのロジックが絡み合ってしまいます。

そこでこの記事では、「足元の影だけ」を担当する小さなコンポーネントとして、
親の足元に楕円形の影スプライトを表示し、ジャンプ高さに応じて縮小するための ShadowCaster を実装していきます。

【Unity】ジャンプ高さで変形する足元の影!「ShadowCaster」コンポーネント

ここでは、次のような要件を満たすコンポーネントを作ります。

  • 親オブジェクト(プレイヤーや敵)の足元に、楕円形の影スプライトを表示する
  • 親がジャンプして高さが変わると、高さに応じて影のスケールが変化する
  • 親が高く飛ぶほど影は小さくなり、着地すると元の大きさに戻る
  • 足元の「地面の高さ」は任意のY座標か、レイキャストで自動検出できる

影の挙動だけを ShadowCaster に閉じ込めることで、
ジャンプや移動ロジックとは独立して管理できるようにします。


フルコード:ShadowCaster.cs


using UnityEngine;

/// <summary>
/// 親オブジェクトの足元に楕円形の影を表示し、
/// 親の高さに応じて影のスケールを変化させるコンポーネント。
/// 
/// ・SpriteRenderer を使ったシンプルな影表現
/// ・ジャンプの高さに応じて影が縮小
/// ・地面との距離は「固定Y」または「レイキャスト」で取得可能
/// 
/// このコンポーネントは「影オブジェクト」にアタッチし、
/// その Transform.parent に「影を落とす対象(例: プレイヤー)」を設定して使います。
/// </summary>
[RequireComponent(typeof(SpriteRenderer))]
public class ShadowCaster : MonoBehaviour
{
    [Header("影の見た目設定")]
    [SerializeField] private SpriteRenderer shadowRenderer; 
    [SerializeField] private Color shadowColor = new Color(0f, 0f, 0f, 0.5f);
    [SerializeField] private Vector2 baseShadowScale = new Vector2(1.0f, 0.5f);

    [Header("高さによるスケール変化設定")]
    [Tooltip("この高さまでは影のサイズが変化する(それ以上は最小サイズで頭打ち)")]
    [SerializeField] private float maxHeight = 3.0f;

    [Tooltip("高さ0(地面)での影のスケール倍率(1.0でbaseShadowScaleそのまま)")]
    [SerializeField] private float scaleAtGround = 1.0f;

    [Tooltip("maxHeight以上の高さでの影のスケール倍率")]
    [SerializeField] private float scaleAtMaxHeight = 0.3f;

    [Header("地面との位置関係")]
    [Tooltip("影を落とす対象のTransform。通常はこのオブジェクトの親を指定")]
    [SerializeField] private Transform target;

    [Tooltip("地面のY座標を固定値で扱うかどうか")]
    [SerializeField] private bool useFixedGroundY = true;

    [Tooltip("useFixedGroundYがtrueのときに使う、地面のY座標")]
    [SerializeField] private float fixedGroundY = 0f;

    [Tooltip("レイキャストで地面を検出する場合のレイヤーマスク")]
    [SerializeField] private LayerMask groundLayerMask;

    [Tooltip("レイキャストで地面を検出する場合の最大距離")]
    [SerializeField] private float raycastMaxDistance = 10f;

    [Tooltip("レイキャスト開始位置のオフセット(targetの足元が中心になるように調整)")]
    [SerializeField] private Vector3 raycastOffset = Vector3.zero;

    [Header("動きの滑らかさ")]
    [Tooltip("影の位置補間の速さ(0で即時追従、値が大きいほど素早く追従)")]
    [SerializeField] private float positionLerpSpeed = 20f;

    [Tooltip("影のスケール補間の速さ")]
    [SerializeField] private float scaleLerpSpeed = 15f;

    // 実際に使っている現在の地面Y座標(デバッグ用)
    private float currentGroundY;

    // 内部計算用:現在のスケール倍率
    private float currentScaleFactor = 1.0f;

    private void Reset()
    {
        // コンポーネント追加時に、自動でSpriteRendererを参照しておく
        shadowRenderer = GetComponent<SpriteRenderer>();
        shadowRenderer.sortingOrder = -10; // 足元に来やすいように小さめのOrderを推奨

        // デフォルトの影色を設定
        shadowColor = new Color(0f, 0f, 0f, 0.5f);
        shadowRenderer.color = shadowColor;

        // target に親を自動設定(親がいれば)
        if (transform.parent != null)
        {
            target = transform.parent;
        }
    }

    private void Awake()
    {
        // Inspectorで未設定なら自動取得
        if (shadowRenderer == null)
        {
            shadowRenderer = GetComponent<SpriteRenderer>();
        }

        if (target == null && transform.parent != null)
        {
            target = transform.parent;
        }

        // 初期色反映
        shadowRenderer.color = shadowColor;

        // 初期スケール設定
        transform.localScale = new Vector3(baseShadowScale.x, baseShadowScale.y, 1f);
        currentScaleFactor = scaleAtGround;
    }

    private void LateUpdate()
    {
        if (target == null)
        {
            // 影を落とす対象がない場合は何もしない
            return;
        }

        // 1. 地面のY座標を取得
        float groundY = useFixedGroundY
            ? fixedGroundY
            : DetectGroundYByRaycast();

        currentGroundY = groundY;

        // 2. 対象オブジェクトの高さ(地面からの距離)を計算
        float targetHeight = Mathf.Max(0f, target.position.y - groundY);

        // 3. 高さに応じてスケール倍率を計算
        float targetScaleFactor = CalculateScaleFactor(targetHeight);

        // 4. 影の位置を更新(XとZはtargetに追従、Yは地面に固定)
        Vector3 targetPosition = transform.position;

        // X,Zは対象に追従
        targetPosition.x = target.position.x;
        targetPosition.z = target.position.z;

        // Yは地面の高さ + 少しだけ浮かせる(Z-fighting防止)
        targetPosition.y = groundY + 0.01f;

        // 補間して滑らかに追従
        if (positionLerpSpeed > 0f)
        {
            transform.position = Vector3.Lerp(
                transform.position,
                targetPosition,
                positionLerpSpeed * Time.deltaTime
            );
        }
        else
        {
            // Lerp速度0なら即時追従
            transform.position = targetPosition;
        }

        // 5. 影のスケールを更新
        currentScaleFactor = Mathf.Lerp(
            currentScaleFactor,
            targetScaleFactor,
            scaleLerpSpeed * Time.deltaTime
        );

        Vector3 baseScale3 = new Vector3(baseShadowScale.x, baseShadowScale.y, 1f);
        transform.localScale = baseScale3 * currentScaleFactor;
    }

    /// <summary>
    /// 高さに応じてスケール倍率を計算する。
    /// height = 0 のとき scaleAtGround、
    /// height = maxHeight 以上のとき scaleAtMaxHeight。
    /// その間は線形補間します。
    /// </summary>
    private float CalculateScaleFactor(float height)
    {
        if (maxHeight <= 0f)
        {
            // maxHeightが0以下なら常に地面のスケールを使う
            return scaleAtGround;
        }

        float t = Mathf.Clamp01(height / maxHeight);
        // t=0 → ground、t=1 → maxHeight
        return Mathf.Lerp(scaleAtGround, scaleAtMaxHeight, t);
    }

    /// <summary>
    /// レイキャストを使って地面のY座標を検出する。
    /// 対象の足元から真下に向けてレイを飛ばす想定。
    /// 何もヒットしなかった場合は、targetの現在位置を地面とみなす。
    /// </summary>
    private float DetectGroundYByRaycast()
    {
        if (target == null)
        {
            return transform.position.y;
        }

        Vector3 origin = target.position + raycastOffset;
        Vector3 direction = Vector3.down;

        if (Physics.Raycast(origin, direction, out RaycastHit hit, raycastMaxDistance, groundLayerMask))
        {
            // ヒットしたコライダーの表面が地面
            return hit.point.y;
        }

        // ヒットしなければ、targetのYをそのまま地面とみなす(落下中など)
        return target.position.y;
    }

#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        if (!useFixedGroundY && target != null)
        {
            // レイキャストの可視化(Sceneビューのみ)
            Gizmos.color = Color.yellow;
            Vector3 origin = target.position + raycastOffset;
            Gizmos.DrawLine(origin, origin + Vector3.down * raycastMaxDistance);
        }

        // 現在の地面Yをラインで表示(エディタ再生中のみ)
        if (Application.isPlaying)
        {
            Gizmos.color = Color.green;
            Gizmos.DrawLine(
                new Vector3(target.position.x - 0.5f, currentGroundY, target.position.z),
                new Vector3(target.position.x + 0.5f, currentGroundY, target.position.z)
            );
        }
    }
#endif
}

使い方の手順

ここでは、2D風の横スクロールプレイヤーを例に、足元の影を付ける手順を説明します。
もちろん、敵キャラや動く足場などにも同じ要領で使えます。

手順①:影用のスプライトを用意する

  1. 任意の画像編集ソフトで、黒い楕円形(中心が透過、周囲がぼかされたものだとそれっぽい)を作成し、PNGで保存します。
  2. Unityにインポートし、Sprite(2D and UI)として設定します。
  3. 必要に応じて「Filter Mode: Point/Bilinear」「Compression: None」などを調整してください。

手順②:影オブジェクトを作成する

  1. Hierarchyで、プレイヤーオブジェクト(例: Player)を選択します。
  2. Player を右クリック → Create Empty で子オブジェクトを作成し、名前を Shadow にします。
  3. ShadowSpriteRenderer コンポーネントを追加し、
    先ほど用意した楕円形の影スプライトを設定します。
  4. 影が足元に来るように、Shadow の Position(Y)を少し下げておきます(後でShadowCasterが上書きするので、目安程度でOK)。

手順③:ShadowCasterコンポーネントを追加・設定する

  1. Shadow オブジェクトに、上のコードの ShadowCaster.cs をアタッチします。
  2. Inspector で以下を確認・調整します。
    • Shadow Renderer:自動で SpriteRenderer が入っていればOK。
    • Shadow Color:影の色と透明度を調整(デフォルトは半透明の黒)。
    • Base Shadow Scale:影の基本サイズ。横長(例: X=1.2, Y=0.4)にするとそれっぽいです。
    • Max Height:プレイヤーがこの高さまでジャンプすると、影は最小サイズになります。
    • Scale At Ground / Scale At Max Height
      • 地面にいるときのスケール倍率(例: 1.0)
      • 最大高さのときのスケール倍率(例: 0.2〜0.4)
    • Target:通常は自動で親の Player が入ります。入っていなければ手動でドラッグ。
    • Use Fixed Ground Y
      • 2D横スクロールで地面が常にY=0なら true にして Fixed Ground Y = 0
      • 3Dや起伏のある地形なら false にしてレイキャストを使うと便利です。
    • Ground Layer Mask(レイキャスト使用時):地面レイヤー(例: Ground)を指定。
    • Raycast Offset:キャラの中心から足元へ少し下げる(例: Y = 0.5〜1.0)と、足元からレイが飛ぶようになります。

手順④:プレイヤーや敵、動く床に適用してみる

  • プレイヤー
    • ジャンプアニメーションや物理挙動とは無関係に、ShadowCaster が自動で影の大きさを調整してくれます。
    • プレイヤーのスクリプト側では「影のことは何も考えない」構造になるので、責務が分離されてスッキリします。
  • 敵キャラ
    • ジャンプする敵や、ホッピングするスライムなどにも、そのまま Shadow プレハブを子として置くだけでOKです。
    • 敵のAIロジックを触らなくても、影が自然に追従します。
  • 動く床(リフト)
    • リフトオブジェクトの子として Shadow を置き、ShadowCaster を付ければ、
      リフトが上下するたびに影のサイズが変わります。
    • 「影は常に地面側にある」ように見せたい場合は、Use Fixed Ground Y をオンにして、床より下の地面をY=0などに揃えておくと扱いやすいです。

メリットと応用

ShadowCaster をコンポーネントとして分離しておくと、次のようなメリットがあります。

  • プレハブ化しやすい
    • Shadow オブジェクトをプレハブにしておけば、どのキャラにもドラッグ&ドロップで簡単に影を付けられます。
    • 影の色やサイズの調整も、プレハブを1箇所いじるだけで全キャラに反映できます。
  • レベルデザインが楽になる
    • 影のつき方が統一されるので、高低差のあるステージでも「どのくらいの高さにいるのか」が直感的に伝わります。
    • 地面のレイヤーや高さを変えても、レイキャスト設定さえ合っていれば自動で影が追従します。
  • 責務の分離
    • ジャンプ処理やアニメーション処理から「影の計算ロジック」を完全に切り離せます。
    • 結果として、プレイヤーや敵のスクリプトはシンプルになり、テストしやすくなります。

改造案:高さに応じて影の濃さも変える

応用として、ジャンプが高くなるほど影を薄くする処理を追加してみましょう。
以下は ShadowCaster に追加できる、簡単な改造用メソッドの例です。


/// <summary>
/// 高さに応じて影のアルファ(濃さ)を変化させる例。
/// height = 0 で alphaAtGround、
/// height = maxHeight 以上で alphaAtMaxHeight になります。
/// </summary>
private void UpdateShadowAlphaByHeight(float height, float alphaAtGround = 0.6f, float alphaAtMaxHeight = 0.2f)
{
    if (shadowRenderer == null) return;

    if (maxHeight <= 0f)
    {
        // 高さの概念がない場合は常に地面のアルファ
        Color c = shadowRenderer.color;
        c.a = alphaAtGround;
        shadowRenderer.color = c;
        return;
    }

    float t = Mathf.Clamp01(height / maxHeight);
    float alpha = Mathf.Lerp(alphaAtGround, alphaAtMaxHeight, t);

    Color color = shadowRenderer.color;
    color.a = alpha;
    shadowRenderer.color = color;
}

このメソッドを使う場合は、LateUpdate() 内で targetHeight を計算した直後あたりに、


UpdateShadowAlphaByHeight(targetHeight);

の1行を追加すればOKです。
これで「高くジャンプするほど影が小さく、かつ薄く」なり、より立体感のある表現になります。

このように、足元の影という1つの機能に対して、専用のコンポーネントを用意しておくと、
見た目の調整や表現の拡張がとてもやりやすくなります。
ぜひ自分のプロジェクトに合わせて、少しずつカスタマイズしてみてください。