Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。移動処理もカメラもアニメーションもエフェクトも、全部ひとつのスクリプトに詰め込んでしまうと、少し仕様を変えたいだけでもコード全体に影響が出てしまいます。

影の制御も同じで、「プレイヤー制御スクリプトの中でRaycastして、足元に影を置いて…」とやってしまうと、そのキャラ専用のロジックになってしまい、他のキャラやオブジェクトに流用しにくくなります。

そこでこの記事では、「足元の地面の高さに合わせて、楕円形の丸影を自動で追従させる」処理を、ひとつの小さなコンポーネントに切り出します。
キャラ側には「歩くこと」だけを意識させ、影は「ShadowBlob」コンポーネントに任せる、という分業スタイルですね。

【Unity】足元にピタッと張り付く丸影!「ShadowBlob」コンポーネント

以下が、Unity6(C#)で動く「ShadowBlob」コンポーネントのフルコードです。
キャラの足元から下方向にRaycastを飛ばし、ヒットした位置と法線に合わせて丸影(楕円)を配置・回転・スケールします。


using UnityEngine;

/// <summary>
/**
 * 足元に「丸影(Blob Shadow)」を表示するコンポーネント
 * - 対象オブジェクトの足元からRaycastを飛ばし、地面の高さに合わせて影を配置
 * - 地面の法線に沿って影を傾ける(坂道でも自然に見える)
 * - 高さに応じて影の大きさと濃さを変える簡易処理付き
 *
 * 想定する構成:
 * - 親: キャラ本体(プレイヤー、敵など)
 * - 子: 影オブジェクト(丸いQuadやMesh)。このスクリプトをアタッチ
 */
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Transform))]
public class ShadowBlob : MonoBehaviour
{
    [Header("追従対象(通常はキャラのTransform)")]
    [SerializeField]
    private Transform target; // 影を落とす対象(プレイヤーや敵など)

    [Header("Raycast 設定")]
    [SerializeField]
    private LayerMask groundLayer = ~0; // どのレイヤーを「地面」とみなすか。デフォルトは全レイヤー
    [SerializeField]
    private float raycastDistance = 5f;  // どれくらい下までRayを飛ばすか
    [SerializeField]
    private float raycastOffsetY = 0.5f; // 対象の足元から少し上にずらしてRayを飛ばすためのオフセット

    [Header("影の配置オフセット")]
    [SerializeField]
    private float heightOffset = 0.02f;  // 地面から少し浮かせる量(Zファイティング対策)

    [Header("影のスケール設定")]
    [SerializeField]
    private float baseScale = 1.0f;      // 影の基準スケール(1.0でそのまま)
    [SerializeField]
    private float minScale = 0.4f;       // 一番小さいときのスケール係数
    [SerializeField]
    private float maxScale = 1.2f;       // 一番大きいときのスケール係数
    [SerializeField]
    private float maxScaleHeight = 3.0f; // この高さまでをスケール計算の範囲とする

    [Header("影の透明度設定(任意)")]
    [SerializeField]
    private Renderer shadowRenderer;     // 影のマテリアルを持つRenderer(MeshRenderer / SpriteRenderer など)
    [SerializeField]
    private float maxAlpha = 0.8f;       // 一番濃いときのアルファ
    [SerializeField]
    private float minAlpha = 0.2f;       // 一番薄いときのアルファ
    [SerializeField]
    private float maxAlphaHeight = 3.0f; // この高さまでをアルファ計算の範囲とする

    [Header("その他オプション")]
    [SerializeField]
    private bool alignToGroundNormal = true; // 地面の法線に合わせて影を傾けるか
    [SerializeField]
    private bool hideWhenNoGround = true;    // 足元に地面がないとき、影を非表示にするか

    // 内部キャッシュ
    private Transform _transform;
    private MaterialPropertyBlock _mpb;
    private static readonly int ColorId = Shader.PropertyToID("_Color");

    private void Awake()
    {
        _transform = transform;

        // target が未設定なら、親の Transform を自動で使う
        if (target == null && _transform.parent != null)
        {
            target = _transform.parent;
        }

        // Renderer が未設定なら、自分についている Renderer を探す
        if (shadowRenderer == null)
        {
            shadowRenderer = GetComponent();
        }

        if (shadowRenderer != null)
        {
            _mpb = new MaterialPropertyBlock();
        }
    }

    private void LateUpdate()
    {
        if (target == null)
        {
            // 対象がなければ何もしない
            return;
        }

        // Raycast の開始位置を計算(対象の位置から少し上)
        Vector3 rayOrigin = target.position + Vector3.up * raycastOffsetY;
        Vector3 rayDirection = Vector3.down;

        if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hit, raycastDistance, groundLayer, QueryTriggerInteraction.Ignore))
        {
            // 地面にヒットした場合

            // 影の位置を地面のヒット位置 + 少しだけ浮かせた位置に設定
            Vector3 shadowPosition = hit.point + hit.normal * heightOffset;
            _transform.position = shadowPosition;

            // 地面の法線に合わせて影を傾ける
            if (alignToGroundNormal)
            {
                // 法線を「上」として、カメラ方向やtargetのforwardを基準に回転を決める
                // ここでは「上=法線、前=targetのforward」を採用
                Vector3 forward = target.forward;
                // forward が法線とほぼ同じだと回転が不安定になるので、少し調整
                if (Vector3.Dot(forward, hit.normal) > 0.9f)
                {
                    forward = Vector3.ProjectOnPlane(forward, hit.normal).normalized;
                    if (forward.sqrMagnitude < 0.0001f)
                    {
                        forward = Vector3.forward;
                    }
                }

                Quaternion rotation = Quaternion.LookRotation(forward, hit.normal);
                _transform.rotation = rotation;
            }

            // 高さに応じてスケールを調整
            float height = rayOrigin.y - hit.point.y; // 対象の「足元付近」から地面までの距離
            float normalizedHeight = Mathf.Clamp01(height / Mathf.Max(0.0001f, maxScaleHeight));
            float scaleFactor = Mathf.Lerp(maxScale, minScale, normalizedHeight); // 高いほど小さくする
            _transform.localScale = Vector3.one * baseScale * scaleFactor;

            // 透明度の調整(Renderer があれば)
            if (shadowRenderer != null && _mpb != null)
            {
                float alphaNormalized = Mathf.Clamp01(height / Mathf.Max(0.0001f, maxAlphaHeight));
                float alpha = Mathf.Lerp(maxAlpha, minAlpha, alphaNormalized);

                shadowRenderer.GetPropertyBlock(_mpb);
                Color color = Color.white;
                // マテリアルに _Color があれば既存の色を取得
                if (shadowRenderer.sharedMaterial != null && shadowRenderer.sharedMaterial.HasProperty(ColorId))
                {
                    color = shadowRenderer.sharedMaterial.color;
                }
                color.a = alpha;
                _mpb.SetColor(ColorId, color);
                shadowRenderer.SetPropertyBlock(_mpb);
            }

            // 影を表示
            SetShadowVisible(true);
        }
        else
        {
            // 地面にヒットしなかった場合(空中・崖の上など)
            if (hideWhenNoGround)
            {
                SetShadowVisible(false);
            }
        }
    }

    /// <summary>
    /// 影の表示・非表示を切り替える
    /// Renderer がなければ GameObject のアクティブ状態で制御
    /// </summary>
    /// <param name="visible">表示するかどうか</param>
    private void SetShadowVisible(bool visible)
    {
        if (shadowRenderer != null)
        {
            shadowRenderer.enabled = visible;
        }
        else
        {
            // Renderer がない場合はGameObjectごとON/OFF
            if (_transform.gameObject.activeSelf != visible)
            {
                _transform.gameObject.SetActive(visible);
            }
        }
    }

    /// <summary>
    /// 外部から対象を動的に差し替えたいとき用
    /// </summary>
    public void SetTarget(Transform newTarget)
    {
        target = newTarget;
    }
}

使い方の手順

丸影コンポーネントを実際のシーンで使う手順を、プレイヤーキャラを例にまとめます。

  1. ① 影用オブジェクト(プレハブ)を用意する
    • Hierarchy 上で GameObject > 3D Object > Quad を作成します(名前は ShadowBlobObject など)。
    • Quad を真上から見たときに丸く見えるように、丸いテクスチャ+透明部分を持ったマテリアルを割り当てます。
    • マテリアルの Rendering Mode(URPなら Surface Type / Blend Mode)を Transparent に設定し、_Color のアルファを 0.5〜0.8 程度にします。
    • Quad のローカルスケールを (1, 1, 1) か、影の基準サイズに合わせておきます。
    • 作成したオブジェクトに上記の ShadowBlob スクリプトをアタッチし、Prefab 化しておくと便利です。
  2. ② プレイヤー(または敵)に影をぶら下げる
    • シーン内のプレイヤーキャラ(例: Player オブジェクト)を選択します。
    • ShadowBlobObjectPlayer の子オブジェクトとして配置します(ドラッグ&ドロップ)。
    • 子オブジェクトの位置を (0, 0, 0) 付近にし、足元あたりに来るよう微調整します。
    • ShadowBlobTarget が空欄でも、親の Transform を自動で使うのでそのままでOKです。明示的に設定したい場合は Player の Transform をドラッグして割り当てましょう。
  3. ③ 地面レイヤーの設定
    • 地面に使っているオブジェクト(Terrain や Floor など)の Layer を確認します。
    • ShadowBlobGround Layer に、その地面レイヤーをチェックします。
      何も気にしないならデフォルトの「Everything(~0)」のままでも動きますが、
      キャラ同士にRayが当たってしまう場合は、専用の「Ground」レイヤーを作って絞り込むと安定します。
  4. ④ パラメータを調整して見た目を整える
    • Raycast Distance: キャラがどれくらい高く飛ぶかに合わせて調整(例: 5〜10)。
    • Raycast Offset Y: キャラの足元より少し上(例: 0.5〜1.0)。
    • Base Scale / Min Scale / Max Scale: 影の大きさと、高さによる変化量を調整。
    • Max Scale Height / Max Alpha Height: 「どのくらいの高さまでスケール・透明度を変化させるか」を決めます。
    • Align To Ground Normal: 坂道でも影を地面に沿わせたいなら ON、常に水平でよければ OFF。
    • Hide When No Ground: 崖から落ちているときに影を消したいなら ON。
      空中でも足元に影を表示したいなら OFF にして、Rayが届かなかった場合の挙動をカスタムしてもOKです。

同じ手順で、敵キャラや動く足場にも簡単に影を付けられます。
例えば「動く床(Moving Platform)」の子に ShadowBlob を置いておけば、床が上がったり下がったりしても、常に地面に沿った影を表示できます。

メリットと応用

この「ShadowBlob」コンポーネントを使うメリットは、なんと言っても 責務の分離 です。

  • プレイヤーの移動ロジックと、影の見た目ロジックを完全に分離できる。
  • プレイヤー、敵、ギミック(動く床など)すべて同じコンポーネントを再利用できる。
  • プレハブ化しておけば、レベルデザイン時に「ここに影を置きたい」と思ったオブジェクトにドラッグするだけでOK。
  • 影の見た目調整(スケール・透明度・傾き)を、スクリプトを一切触らずインスペクターから行える。

また、「Blob Shadow」はリアルタイムシャドウより軽量なので、モバイルやローエンド環境での最適化にも役立ちます。
ライトの影設定に依存しないので、ライトをベイクしているシーンでも、キャラだけ動的にそれっぽい影を出すことができます。

応用としては、例えば「特定の高さ以上にジャンプしたら、影を一時的に消す」「水面の上では影ではなく波紋を出す」など、状況に応じて見た目を切り替えることもできます。

最後に、簡単な改造案として「影をふわっと拡大・縮小させて、呼吸しているようなアニメーションを付ける」関数例を載せておきます。


/// <summary>
/// 影にゆらぎアニメーションを追加するサンプル
/// LateUpdate の最後あたりから呼び出すと、影が少し脈打つように見える
/// </summary>
private void ApplyBreathingAnimation(float amplitude = 0.05f, float speed = 2.0f)
{
    // 現在のスケールを基準に、正弦波で微妙に拡大縮小
    float t = Time.time * speed;
    float offset = 1.0f + Mathf.Sin(t) * amplitude;

    Vector3 currentScale = _transform.localScale;
    _transform.localScale = currentScale * offset;
}

このように、小さなコンポーネントとして分割しておけば、「影を揺らす」「影の色を時間で変える」「特定のギミックの上だけ別の色にする」など、見た目の遊びもどんどん追加しやすくなります。
巨大な God クラスにせず、「影は影コンポーネントに任せる」というスタイルで、気持ちよく拡張していきましょう。