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風の横スクロールプレイヤーを例に、足元の影を付ける手順を説明します。
もちろん、敵キャラや動く足場などにも同じ要領で使えます。
手順①:影用のスプライトを用意する
- 任意の画像編集ソフトで、黒い楕円形(中心が透過、周囲がぼかされたものだとそれっぽい)を作成し、PNGで保存します。
- Unityにインポートし、Sprite(2D and UI)として設定します。
- 必要に応じて「Filter Mode: Point/Bilinear」「Compression: None」などを調整してください。
手順②:影オブジェクトを作成する
- Hierarchyで、プレイヤーオブジェクト(例:
Player)を選択します。 Playerを右クリック →Create Emptyで子オブジェクトを作成し、名前をShadowにします。ShadowにSpriteRendererコンポーネントを追加し、
先ほど用意した楕円形の影スプライトを設定します。- 影が足元に来るように、
Shadowの Position(Y)を少し下げておきます(後でShadowCasterが上書きするので、目安程度でOK)。
手順③:ShadowCasterコンポーネントを追加・設定する
Shadowオブジェクトに、上のコードのShadowCaster.csをアタッチします。- 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にしてレイキャストを使うと便利です。
- 2D横スクロールで地面が常にY=0なら
- Ground Layer Mask(レイキャスト使用時):地面レイヤー(例: Ground)を指定。
- Raycast Offset:キャラの中心から足元へ少し下げる(例: Y = 0.5〜1.0)と、足元からレイが飛ぶようになります。
- Shadow Renderer:自動で
手順④:プレイヤーや敵、動く床に適用してみる
- プレイヤー:
- ジャンプアニメーションや物理挙動とは無関係に、
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つの機能に対して、専用のコンポーネントを用意しておくと、
見た目の調整や表現の拡張がとてもやりやすくなります。
ぜひ自分のプロジェクトに合わせて、少しずつカスタマイズしてみてください。
