【Cocos Creator 3.8】ShadowCaster の実装:アタッチするだけで「足元に自動で影を出し、ジャンプ高さで影が縮む」汎用スクリプト

このコンポーネントは、キャラクターやオブジェクトの「足元の影」を自動で生成・制御するための汎用スクリプトです。
ShadowCaster を対象ノードにアタッチするだけで、足元に楕円形の影スプライトを生成し、ノードのジャンプ(Y座標の変化)に応じて影の大きさと濃さを自動調整できます。
別の GameManager や専用のジャンプ制御スクリプトに依存せず、インスペクタで必要なパラメータを設定するだけで動作します。


コンポーネントの設計方針

実現したい機能の整理

  • 対象ノード(キャラクターなど)の足元位置に「楕円形の影」を表示する。
  • 影は対象ノードの子として自動生成される(外部ノードへの依存なし)。
  • 対象ノードがジャンプして Y 座標が高くなるほど、影が小さく・薄くなる。
  • 着地(基準高さ)に近いほど、影が大きく・濃くなる。
  • Inspector で見た目や挙動を調整できるようにする。

外部依存をなくすためのアプローチ

  • 影用ノードは ShadowCaster 自身が自動生成 する。
  • 影のスプライト画像は Inspector から SpriteFrame を指定 する。
  • ジャンプの高さは、親ノードのローカル Y 座標 を参照するだけとし、他スクリプトの状態には依存しない。
  • 基準高さ(着地時の Y 座標)を Inspector で指定し、その高さとの差分を「ジャンプ高さ」として扱う。

Inspector で設定可能なプロパティ設計

ShadowCaster コンポーネントに用意する @property は以下の通りです。

  • shadowSpriteFrame: SpriteFrame | null
    影として使用するスプライト画像。
    楕円形の黒い PNG などを指定します。未設定の場合はエラーログを出し、影は生成しません。
  • baseHeight: number
    「着地しているとみなす Y 座標」の基準値(ローカル座標)。
    例:キャラクターが地面に立っているときの Y 座標が 0 なら 0 のままでOK。
    実行時には currentHeight = node.position.y - baseHeight として「ジャンプ高さ」を計算します。
  • maxJumpHeight: number
    影の縮小・透明化を計算するための「最大ジャンプ高さ」の想定値。
    例:キャラクターが最大で Y=200 くらいまでジャンプするなら 200。
    実際の高さがこれを超える場合は、影サイズ・透明度は最小値にクランプされます。
  • baseScale: number
    基準高さのときの影のスケール(X,Yともにこの値を基準とする)。
    例:1.0 なら、影の元画像サイズそのまま。1.5 なら 1.5 倍。
  • minScale: number
    ジャンプが最大高さに達したときの影の 最小スケール
    例:0.3 なら、maxJumpHeight 以上の高さのときに 0.3 倍まで縮小。
  • baseOpacity: number
    基準高さ(着地時)の影の不透明度(0〜255)。
    例:200 なら、やや透けた影。
  • minOpacity: number
    最大ジャンプ高さのときの影の不透明度(0〜255)。
    例:50 なら、高くジャンプしたときにかなり薄くなる。
  • shadowOffsetY: number
    影の表示位置を親ノードの足元からどれだけ下げるか(ローカル Y オフセット)。
    例:-10 なら、親の中心から 10 下に影を配置。
  • smooth: boolean
    影サイズ・透明度の変化をスムージングするかどうか。
    true の場合、Lerp を使ってなめらかに変化。
  • smoothFactor: number
    スムージング係数(0〜1 推奨)。
    0.1〜0.3 くらいが扱いやすい。値が大きいほど追従が速くなる。
  • debugLog: boolean
    true の場合、基礎的な状態(高さ・スケールなど)をログ出力してデバッグしやすくする。

TypeScriptコードの実装


import { _decorator, Component, Node, Sprite, SpriteFrame, UITransform, Vec3, Color, log, warn, clamp01 } from 'cc';
const { ccclass, property } = _decorator;

/**
 * ShadowCaster
 * 親ノードの足元に楕円形の影スプライトを生成し、
 * 親ノードのジャンプ高さ (Y座標) に応じて縮小・透明化するコンポーネント。
 */
@ccclass('ShadowCaster')
export class ShadowCaster extends Component {

    @property({
        type: SpriteFrame,
        tooltip: '影として使用するスプライト画像(楕円形の黒など)'
    })
    public shadowSpriteFrame: SpriteFrame | null = null;

    @property({
        tooltip: '着地しているとみなすローカルY座標(例: 0)'
    })
    public baseHeight: number = 0;

    @property({
        tooltip: '想定する最大ジャンプ高さ(これ以上は影サイズ・透明度が最小にクランプされる)'
    })
    public maxJumpHeight: number = 200;

    @property({
        tooltip: '着地時(基準高さ)の影のスケール(X,Y)'
    })
    public baseScale: number = 1.0;

    @property({
        tooltip: '最大ジャンプ高さのときの影の最小スケール'
    })
    public minScale: number = 0.3;

    @property({
        tooltip: '着地時(基準高さ)の影の不透明度(0〜255)'
    })
    public baseOpacity: number = 200;

    @property({
        tooltip: '最大ジャンプ高さのときの影の最小不透明度(0〜255)'
    })
    public minOpacity: number = 50;

    @property({
        tooltip: '親ノードの足元からどれだけ下に影を表示するか(ローカルYオフセット)'
    })
    public shadowOffsetY: number = -10;

    @property({
        tooltip: '影の変化をスムージングするかどうか'
    })
    public smooth: boolean = true;

    @property({
        tooltip: 'スムージング係数(0〜1 推奨、値が大きいほど追従が速い)'
    })
    public smoothFactor: number = 0.2;

    @property({
        tooltip: 'デバッグ用ログを出力するかどうか'
    })
    public debugLog: boolean = false;

    // 内部用:生成した影ノードとそのコンポーネント
    private _shadowNode: Node | null = null;
    private _shadowSprite: Sprite | null = null;

    // 内部用:現在のスケールと不透明度(スムージング用)
    private _currentScale: number = 1.0;
    private _currentOpacity: number = 255;

    onLoad() {
        // パラメータの簡易バリデーション
        if (this.maxJumpHeight <= 0) {
            warn('[ShadowCaster] maxJumpHeight が 0 以下です。1 に補正します。');
            this.maxJumpHeight = 1;
        }
        if (this.baseScale <= 0) {
            warn('[ShadowCaster] baseScale が 0 以下です。0.1 に補正します。');
            this.baseScale = 0.1;
        }
        if (this.minScale < 0) {
            warn('[ShadowCaster] minScale が 0 未満です。0 に補正します。');
            this.minScale = 0;
        }
        if (this.baseOpacity < 0) this.baseOpacity = 0;
        if (this.baseOpacity > 255) this.baseOpacity = 255;
        if (this.minOpacity < 0) this.minOpacity = 0;
        if (this.minOpacity > 255) this.minOpacity = 255;

        // 影スプライトが設定されていない場合は警告を出して終了
        if (!this.shadowSpriteFrame) {
            warn('[ShadowCaster] shadowSpriteFrame が設定されていません。影は生成されません。');
            return;
        }

        // 影ノードを生成(既に存在していないかチェック)
        // ノード名で検索して、二重生成を防止
        const existing = this.node.getChildByName('ShadowCaster_Shadow');
        if (existing) {
            this._shadowNode = existing;
            this._shadowSprite = existing.getComponent(Sprite);
            if (!this._shadowSprite) {
                warn('[ShadowCaster] 既存の影ノードに Sprite コンポーネントがありません。自動で追加します。');
                this._shadowSprite = existing.addComponent(Sprite);
            }
        } else {
            this._shadowNode = new Node('ShadowCaster_Shadow');
            this.node.addChild(this._shadowNode);
            this._shadowSprite = this._shadowNode.addComponent(Sprite);
        }

        if (!this._shadowSprite) {
            warn('[ShadowCaster] Sprite コンポーネントの取得・追加に失敗しました。');
            return;
        }

        // Sprite 設定
        this._shadowSprite.spriteFrame = this.shadowSpriteFrame;
        this._shadowSprite.color = new Color(0, 0, 0, this.baseOpacity); // 黒+不透明度

        // UITransform がなければ追加(2D UI/Canvas 上で使う想定)
        let uiTrans = this._shadowNode.getComponent(UITransform);
        if (!uiTrans) {
            uiTrans = this._shadowNode.addComponent(UITransform);
        }

        // 影ノードの初期位置・スケール
        this._shadowNode.setPosition(new Vec3(0, this.shadowOffsetY, 0));
        this._currentScale = this.baseScale;
        this._currentOpacity = this.baseOpacity;
        this._applyScaleAndOpacityInstant();

        if (this.debugLog) {
            log('[ShadowCaster] onLoad 完了。影ノードを生成しました。');
        }
    }

    start() {
        // 特に初期化は onLoad で完了しているが、必要であればここで追加処理
        if (this.debugLog) {
            log('[ShadowCaster] start 呼び出し。');
        }
    }

    update(deltaTime: number) {
        // 影ノードやスプライトが用意できていない場合は何もしない
        if (!this._shadowNode || !this._shadowSprite) {
            return;
        }

        // 親ノードのローカルY座標からジャンプ高さを計算
        const currentY = this.node.position.y;
        const jumpHeight = currentY - this.baseHeight;

        // 0〜1 に正規化した高さ(0: 着地, 1: 最大ジャンプ)
        const tRaw = jumpHeight / this.maxJumpHeight;
        const t = clamp01(tRaw);

        // 高さに応じてスケールと不透明度を線形補間で計算
        const targetScale = this._lerp(this.baseScale, this.minScale, t);
        const targetOpacity = this._lerp(this.baseOpacity, this.minOpacity, t);

        if (this.smooth) {
            // スムージングしながら追従
            const s = clamp01(this.smoothFactor);
            this._currentScale = this._lerp(this._currentScale, targetScale, s);
            this._currentOpacity = this._lerp(this._currentOpacity, targetOpacity, s);
        } else {
            // 即時反映
            this._currentScale = targetScale;
            this._currentOpacity = targetOpacity;
        }

        // 影ノードの位置(X,Zは親に追従、Yはオフセット固定)
        const parentPos = this.node.position;
        this._shadowNode.setPosition(new Vec3(parentPos.x, this.shadowOffsetY, parentPos.z));

        // 計算したスケールと不透明度を適用
        this._applyScaleAndOpacityInstant();

        if (this.debugLog) {
            // あまり毎フレーム大量に出したくない場合は条件を絞るなど調整してください
            // ここでは簡易的に高さとスケールだけをログ
            log(`[ShadowCaster] jumpHeight=${jumpHeight.toFixed(1)}, scale=${this._currentScale.toFixed(2)}, opacity=${Math.round(this._currentOpacity)}`);
        }
    }

    /**
     * スケールと不透明度を現在値で即時適用
     */
    private _applyScaleAndOpacityInstant() {
        if (!this._shadowNode || !this._shadowSprite) {
            return;
        }
        this._shadowNode.setScale(new Vec3(this._currentScale, this._currentScale, 1));
        const alpha = Math.round(this._currentOpacity);
        // 既存の色のRGBは維持しつつAだけ変更
        const c = this._shadowSprite.color;
        this._shadowSprite.color = new Color(c.r, c.g, c.b, alpha);
    }

    /**
     * 線形補間
     */
    private _lerp(a: number, b: number, t: number): number {
        return a + (b - a) * t;
    }
}

コードのポイント解説

  • onLoad
    • Inspector から設定されたパラメータの簡易バリデーション(maxJumpHeight, baseScale など)を行います。
    • shadowSpriteFrame が未設定の場合は警告を出し、影生成をスキップします。
    • 子ノードに ShadowCaster_Shadow という名前の影ノードを自動生成(既にある場合は再利用)し、Sprite と UITransform を付与します。
    • 影の初期位置(shadowOffsetY)とスケール・不透明度を設定します。
  • start
    • 現状ではログ出力のみですが、ゲームによっては start でさらに初期化を追加できます。
  • update
    • 毎フレーム、親ノードのローカル Y 座標から「ジャンプ高さ」を計算します。
    • jumpHeight / maxJumpHeight を 0〜1 にクランプし、これをもとにスケールと不透明度を線形補間します。
    • smooth が true の場合は smoothFactor を使って Lerp でなめらかに変化させます。
    • 影ノードの位置は、X/Z は親に合わせつつ、Y は常に shadowOffsetY に固定することで「足元に貼り付いている」ような見た目にします。
  • _applyScaleAndOpacityInstant
    • 内部で保持している _currentScale_currentOpacity を実際のノード・スプライトに反映します。
    • 不透明度は Color の alpha 値として適用します。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Editor の Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. 作成されたスクリプトの名前を ShadowCaster.ts に変更します。
  4. ダブルクリックしてエディタ(VSCode など)で開き、先ほどのコードを すべて貼り付けて保存 します。

2. 影用スプライト画像の用意

  1. 黒〜グレーの楕円形 PNG 画像を用意します(背景は透過)。
    例:512×256 の黒い楕円など。
  2. その画像を Cocos Creator の Assets パネルにドラッグ&ドロップしてインポートします。
  3. インポートされた画像をクリックし、Inspector で SpriteFrame が生成されている ことを確認します(通常は自動)。

3. テスト用ノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用キャラクターノードを作成します。
  2. 作成された Sprite ノードの名前を Player など分かりやすいものに変更します。
  3. Inspector で Sprite コンポーネントの SpriteFrame に、任意のキャラクター画像を設定します(なければデフォルトでも構いません)。
  4. Player ノードの Position.Y を一旦 0 にしておきます(baseHeight=0 の前提でテストしやすくするため)。

4. ShadowCaster コンポーネントのアタッチ

  1. Hierarchy で Player ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom → ShadowCaster を選択し、コンポーネントを追加します。

5. Inspector でプロパティを設定

Player ノードに追加された ShadowCaster コンポーネントの各プロパティを以下のように設定してみます。

  • Shadow Sprite Frame: 先ほどインポートした楕円形の影画像の SpriteFrame をドラッグ&ドロップ
  • Base Height: 0(Player の Y=0 を着地とみなす)
  • Max Jump Height: 200(Y=200 までジャンプする想定)
  • Base Scale: 1.0(影画像そのままのサイズ)
  • Min Scale: 0.3(最大ジャンプ時に 30% の大きさ)
  • Base Opacity: 200(少し透けた影)
  • Min Opacity: 50(高くジャンプしたときにかなり薄く)
  • Shadow Offset Y: -20(Player の中心から 20 下に影を表示)
  • Smooth: チェックを入れる(true)
  • Smooth Factor: 0.2
  • Debug Log: 最初はオフでOK。挙動確認したいときはオンに。

6. シーンの再生と動作確認

  1. シーンを保存します(Ctrl+S / Cmd+S)。
  2. エディタ上部の Play ボタンを押してゲームを再生します。
  3. 再生中に Hierarchy から Player ノードを選択し、Inspector の Position.Y を手動で変更してみてください。

期待される挙動:

  • Y=0(Base Height と同じ)のとき:影が Base Scale(1.0)で、Base Opacity(200)の濃さで表示される。
  • Y=100(Max Jump Height の半分)のとき:影は Base Scale と Min Scale の中間 の大きさになり、透明度も中間値になる。
  • Y=200(Max Jump Height)以上のとき:影は Min Scale(0.3)まで小さくなり、Min Opacity(50)まで薄くなる。
  • Smooth が true の場合:Y を急に変えても、影は少し遅れてなめらかに追従する。

もし影が表示されない場合は、以下を確認してください。

  • shadowSpriteFrame が正しく設定されているか。
  • Hierarchy で Player ノードの子に ShadowCaster_Shadow ノードが生成されているか。
  • Canvas(2D UI 環境)上に配置しているか(UITransform を使うため)。
  • カメラの表示範囲内に影があるか。

7. 実際のジャンプ処理と組み合わせる

実際のゲームでは、Player ノードに別のジャンプ制御スクリプトを付けて Y 座標を動かすだけで、ShadowCaster が自動的に影の大きさと濃さを調整してくれます。
ShadowCaster は 他のスクリプトに依存せず、単に node.position.y を見るだけなので、どんな移動ロジックとも組み合わせ可能です。


まとめ

  • ShadowCaster は、親ノードの足元に楕円形の影を自動生成し、ジャンプ高さに応じて 影のサイズと透明度を自動調整 する汎用コンポーネントです。
  • 影用ノードの生成から Sprite 設定までをコンポーネント内で完結させているため、他のカスタムスクリプトやノード構成に依存しません
  • Inspector から 影画像・基準高さ・最大ジャンプ高さ・スケール・透明度・スムージング を細かく調整でき、さまざまなキャラクターやゲームに簡単に再利用できます。
  • このコンポーネントをプロジェクトに一つ用意しておけば、新しいキャラクターを追加するたびに ShadowCaster をアタッチして SpriteFrame を指定するだけで、自然な足元の影表現をすぐに導入できます。

このように、単一コンポーネントで完結する汎用スクリプトを積み重ねていくことで、ゲーム開発のスピードと保守性を大きく向上させることができます。ShadowCaster をベースに、影のぼかし具合や色を変えるバリエーションを作るのも簡単ですので、ぜひ自分のプロジェクト向けにカスタマイズしてみてください。