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;
}
}
使い方の手順
丸影コンポーネントを実際のシーンで使う手順を、プレイヤーキャラを例にまとめます。
-
① 影用オブジェクト(プレハブ)を用意する
- 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 化しておくと便利です。
- Hierarchy 上で
-
② プレイヤー(または敵)に影をぶら下げる
- シーン内のプレイヤーキャラ(例:
Playerオブジェクト)を選択します。 ShadowBlobObjectをPlayerの子オブジェクトとして配置します(ドラッグ&ドロップ)。- 子オブジェクトの位置を (0, 0, 0) 付近にし、足元あたりに来るよう微調整します。
ShadowBlobの Target が空欄でも、親の Transform を自動で使うのでそのままでOKです。明示的に設定したい場合はPlayerの Transform をドラッグして割り当てましょう。
- シーン内のプレイヤーキャラ(例:
-
③ 地面レイヤーの設定
- 地面に使っているオブジェクト(Terrain や Floor など)の Layer を確認します。
ShadowBlobの Ground Layer に、その地面レイヤーをチェックします。
何も気にしないならデフォルトの「Everything(~0)」のままでも動きますが、
キャラ同士にRayが当たってしまう場合は、専用の「Ground」レイヤーを作って絞り込むと安定します。
-
④ パラメータを調整して見た目を整える
- 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 クラスにせず、「影は影コンポーネントに任せる」というスタイルで、気持ちよく拡張していきましょう。
