【Cocos Creator 3.8】TrailRenderer(軌跡)の実装:アタッチするだけで剣や弾に「残像の軌跡」を描画する汎用スクリプト
本記事では、Cocos Creator 3.8.7 + TypeScript で、任意のノードにアタッチするだけで移動経路に滑らかな軌跡を描画する「TrailRenderer」コンポーネントを実装します。
剣の振り、弾丸の軌道、ダッシュ中の残像など、「ノードが動いた線の跡」を簡単に表現したい場面は多いです。ここで実装する TrailRenderer は、Line2D コンポーネントを内部で自動取得して制御し、外部の GameManager やシングルトンに一切依存せず、インスペクタでパラメータを調整するだけで使える汎用スクリプトです。
コンポーネントの設計方針
機能要件の整理
- アタッチしたノードのワールド座標の移動経路に沿って軌跡を描画する。
- 軌跡は一定時間でフェードアウト(透明になって消える)する。
- 動きが止まっているときは、不要に頂点を増やさない(一定以上動いたときだけ頂点を追加)。
- Line2D コンポーネントを利用して、柔らかくつながった線として描画する。
- Line2D がノードに存在しない場合は、エラーログを出して動作を停止し、エディタ上の設定ミスに気づけるようにする。
- TrailRenderer 単体で完結し、他のカスタムスクリプトには一切依存しない。
TrailRenderer の基本動作
- 毎フレーム、ノードのワールド座標を取得する。
- 前回記録した位置から最小移動距離(minDistance)以上動いていたら、新しい「ポイント」を追加する。
- 各ポイントは「位置(Vec3)」と「経過時間(age)」を持つ。
- update() で各ポイントの age を加算し、lifeTime を超えたポイントを削除する。
- 各ポイントの age に応じて、アルファ値(透明度)を計算し、Line2D の頂点カラーとして反映する。
- Line2D の points / colors を更新し、見た目の軌跡を描画する。
インスペクタで設定可能なプロパティ
TrailRenderer でインスペクタから調整できるプロパティと役割を整理します。
- enabledTrail: boolean
軌跡の ON/OFF を切り替えます。
・true: 軌跡を描画する
・false: 軌跡の更新を停止し、既存の軌跡もクリアします - lifeTime: number
1つのポイントが残る時間(秒)。
例: 0.3 ~ 0.8 くらいが剣・弾の軌跡に向いています。
この時間を過ぎたポイントは自動的に削除されます。 - minDistance: number
新しいポイントを追加するために必要な最小移動距離(ワールド座標系の単位)。
小さすぎると頂点が増えすぎて重くなりやすく、大きすぎるとガクガクした線になります。
目安: 0.02 ~ 0.1 - maxPoints: number
軌跡として保持する最大ポイント数。
パフォーマンス保護のため、これを超えたら古いポイントから削除します。
目安: 32 ~ 128 - startWidth: number
軌跡の先頭側の線の太さ。
Line2D の width に反映されます(ここでは全体幅として使用)。 - startColor: Color
軌跡の出始めの色。主に RGB を決める用途。アルファ値は内部で上書きされ、フェードアウトに使われます。 - useWorldSpace: boolean
Line2D が扱う座標系の指定(Line2D の useWorldSpace に合わせる)。
・true: ワールド座標をそのまま使用(通常はこちら)
・false: 親ノードローカル座標として扱う - fadeMode: number (enum)
フェードアウトのカーブを選択します。
・0: Linear(線形に消える)
・1: EaseOut(最初は濃く、最後にスッと消える)
・2: EaseIn(最初にすぐ薄くなり、最後はゆっくり消える)
これらのパラメータをインスペクタから調整することで、剣の鋭い軌跡・魔法の柔らかい残光・弾丸の短いスモークラインなど、さまざまな表現に対応できます。
TypeScriptコードの実装
ここからは、TrailRenderer コンポーネントの完全な TypeScript 実装を示します。
import { _decorator, Component, Node, Vec3, Color, Line2D, math, Enum } from 'cc';
const { ccclass, property } = _decorator;
/**
* 軌跡のフェードモード
*/
enum TrailFadeMode {
LINEAR = 0,
EASE_OUT = 1,
EASE_IN = 2,
}
Enum(TrailFadeMode);
interface TrailPoint {
position: Vec3;
age: number; // 経過時間
}
@ccclass('TrailRenderer')
export class TrailRenderer extends Component {
@property({
tooltip: '軌跡の描画を有効にするかどうか。\nfalse にすると軌跡の更新と描画を停止し、既存の頂点もクリアされます。',
})
public enabledTrail: boolean = true;
@property({
tooltip: '1つのポイントが残る時間(秒)。\nこの時間を過ぎるとポイントは削除され、軌跡が消えていきます。',
min: 0.01,
})
public lifeTime: number = 0.4;
@property({
tooltip: '新しいポイントを追加するために必要な最小移動距離(ワールド座標)。\n小さすぎると頂点が増えすぎて重くなる可能性があります。',
min: 0.001,
})
public minDistance: number = 0.05;
@property({
tooltip: '軌跡として保持する最大ポイント数。\nこれを超えた場合は最も古いポイントから削除されます。',
min: 2,
})
public maxPoints: number = 64;
@property({
tooltip: '軌跡の線の太さ。',
min: 0.001,
})
public startWidth: number = 0.05;
@property({
tooltip: '軌跡の開始色(RGB)。\nアルファ値は内部で制御され、時間経過でフェードアウトします。',
})
public startColor: Color = new Color(255, 255, 255, 255);
@property({
tooltip: 'Line2D の座標系をワールド座標として扱うかどうか。\n通常は true 推奨です。',
})
public useWorldSpace: boolean = true;
@property({
type: TrailFadeMode,
tooltip: 'フェードアウトのカーブを選択します。\nLINEAR: 一定速度で消える\nEASE_OUT: 最後にスッと消える\nEASE_IN: 最初にすぐ薄くなる',
})
public fadeMode: TrailFadeMode = TrailFadeMode.LINEAR;
// 内部状態
private _line2D: Line2D | null = null;
private _points: TrailPoint[] = [];
private _lastPosition: Vec3 | null = null;
private _tempWorldPos: Vec3 = new Vec3();
private _tempLocalPos: Vec3 = new Vec3();
onLoad() {
// Line2D の取得(必須コンポーネント)
this._line2D = this.getComponent(Line2D);
if (!this._line2D) {
console.error(
'[TrailRenderer] このコンポーネントを使用するには、同じノードに Line2D コンポーネントを追加してください。',
this.node.name
);
return;
}
// Line2D の基本設定を初期化
this._line2D.width = this.startWidth;
this._line2D.useWorldSpace = this.useWorldSpace;
this._line2D.points = [];
this._line2D.colors = [];
}
start() {
if (!this._line2D) {
return;
}
// 開始時点の位置を 1 つだけ登録しておく
this._updateCurrentNodeWorldPos();
const startPos = this._getLineSpacePosition(this._tempWorldPos);
this._points = [{
position: startPos.clone(),
age: 0,
}];
this._lastPosition = startPos.clone();
this._applyToLine2D();
}
update(deltaTime: number) {
if (!this._line2D) {
return;
}
if (!this.enabledTrail) {
// 無効化されている場合は軌跡をクリア
if (this._points.length > 0) {
this._points.length = 0;
this._applyToLine2D();
}
return;
}
// 現在位置の取得
this._updateCurrentNodeWorldPos();
const currentPos = this._getLineSpacePosition(this._tempWorldPos);
// 前回位置から十分移動していれば新しいポイントを追加
if (!this._lastPosition || Vec3.distance(this._lastPosition, currentPos) >= this.minDistance) {
this._addPoint(currentPos);
this._lastPosition = currentPos.clone();
}
// ポイントの寿命更新と削除
this._updatePoints(deltaTime);
// Line2D に反映
this._applyToLine2D();
}
/**
* 現在のノードのワールド座標を _tempWorldPos に格納する。
*/
private _updateCurrentNodeWorldPos() {
this.node.getWorldPosition(this._tempWorldPos);
}
/**
* Line2D の座標系に合わせて座標を変換して返す。
* useWorldSpace = true の場合はそのまま、false の場合は親ローカル座標に変換。
*/
private _getLineSpacePosition(worldPos: Vec3): Vec3 {
if (this.useWorldSpace) {
return worldPos.clone();
} else {
const parent = this.node.parent;
if (parent) {
parent.inverseTransformPoint(this._tempLocalPos, worldPos);
return this._tempLocalPos.clone();
} else {
// 親がいない場合はワールド座標とローカル座標が同じとみなす
return worldPos.clone();
}
}
}
/**
* 新しいポイントを追加し、maxPoints を超えている場合は古いものから削除する。
*/
private _addPoint(pos: Vec3) {
this._points.push({
position: pos,
age: 0,
});
if (this._points.length > this.maxPoints) {
const overflow = this._points.length - this.maxPoints;
this._points.splice(0, overflow);
}
}
/**
* ポイントの age を更新し、lifeTime を超えたものを削除する。
*/
private _updatePoints(deltaTime: number) {
if (this._points.length === 0) {
return;
}
for (let i = 0; i < this._points.length; i++) {
this._points[i].age += deltaTime;
}
// lifeTime を超えたポイントを前から削除
while (this._points.length > 0 && this._points[0].age > this.lifeTime) {
this._points.shift();
}
}
/**
* 現在の _points を Line2D の points / colors に反映する。
*/
private _applyToLine2D() {
if (!this._line2D) {
return;
}
const count = this._points.length;
if (count === 0) {
this._line2D.points = [];
this._line2D.colors = [];
return;
}
const points: Vec3[] = new Array(count);
const colors: Color[] = new Array(count);
for (let i = 0; i < count; i++) {
const p = this._points[i];
points[i] = p.position.clone();
const t = math.clamp01(p.age / this.lifeTime);
const alphaFactor = this._evaluateFade(t);
const c = new Color(
this.startColor.r,
this.startColor.g,
this.startColor.b,
Math.floor(255 * alphaFactor)
);
colors[i] = c;
}
this._line2D.points = points;
this._line2D.colors = colors;
this._line2D.width = this.startWidth;
this._line2D.useWorldSpace = this.useWorldSpace;
}
/**
* フェードモードに応じて 0~1 のフェード係数を返す。
* t: 0(生成直後)~1(寿命末期)
*/
private _evaluateFade(t: number): number {
switch (this.fadeMode) {
case TrailFadeMode.EASE_OUT:
// 最後にスッと消える
return 1.0 - t * t;
case TrailFadeMode.EASE_IN:
// 最初にすぐ薄くなる
return 1.0 - math.sqrt(t);
case TrailFadeMode.LINEAR:
default:
return 1.0 - t;
}
}
/**
* 外部から軌跡を即座にクリアしたい場合に呼び出せるヘルパー。
*/
public clearTrail() {
this._points.length = 0;
this._applyToLine2D();
}
}
コードの主要部分の解説
- onLoad()
- 同じノードにアタッチされた
Line2DコンポーネントをgetComponent(Line2D)で取得します。 - 存在しない場合は
console.errorを出し、以降の処理は何もしません(防御的実装)。 - Line2D の
width/useWorldSpace/points/colorsを初期化します。
- 同じノードにアタッチされた
- start()
- 開始時点のノード位置を取得し、最初の TrailPoint として登録します。
_applyToLine2D()を呼び出して、Line2D に 1 点だけの線を描画させます。
- update(deltaTime)
enabledTrailが false の場合は、軌跡のリストをクリアして Line2D も空にします。- 現在のノードのワールド座標を取得し、
useWorldSpace設定に応じて Line2D 用の座標に変換します。 - 前回の位置から
minDistance以上動いていたら新しいポイントを追加します。 - 各ポイントの
ageを更新し、lifeTimeを超えたポイントを削除します。 - 最後に
_applyToLine2D()で Line2D のpoints/colorsを更新します。
- _applyToLine2D()
- 内部で保持している
_points配列を Line2D のpoints/colorsに変換します。 - 各ポイントに対して、経過時間
ageから 0〜1 のtを計算し、_evaluateFade(t)でアルファ係数を求めます。 - このアルファをもとに Color の a 値を決定し、時間とともに透明になっていく軌跡を表現します。
- 内部で保持している
- _evaluateFade(t)
- enum
TrailFadeModeに応じて、線形・EaseOut・EaseIn のフェードカーブを返します。 - これにより、同じ lifeTime でも見た目の消え方を簡単に変えられます。
- enum
- clearTrail()
- 外部から呼び出して、軌跡を即座に消去したいときに使えるユーティリティです。
- 例: 攻撃終了時に一度軌跡をリセットしたい場合など。
使用手順と動作確認
ここからは、エディタ上で TrailRenderer を実際に使うための具体的な手順を説明します。
1. スクリプトファイルの作成
- エディタの Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- ファイル名を TrailRenderer.ts に変更します。
- 作成された
TrailRenderer.tsをダブルクリックして開き、本文で示したコード全体を貼り付けて保存します。
2. テスト用ノードと Line2D の準備
剣や弾の代わりに、まずは簡単なノードで動作確認をします。
- Hierarchy パネルで右クリック → Create → 3D Object → Sprite3D(2Dなら Node でも OK)など、適当なノードを作成します。
ノード名を TrailTest などにしておくと分かりやすいです。 - 作成したノードを選択し、Inspector パネルの Add Component ボタンをクリックします。
- 検索欄に Line2D と入力し、Line2D コンポーネントを追加します。
- Line2D の設定(最初はデフォルトで OK):
- Width: 0.05 ~ 0.1 くらい(後で TrailRenderer 側からも設定されます)。
- Use World Space: とりあえず ON(TrailRenderer の useWorldSpace も ON に合わせます)。
- Points / Colors: 空のままで問題ありません(TrailRenderer が自動で埋めます)。
3. TrailRenderer コンポーネントをアタッチ
- 同じく TrailTest ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
- Custom カテゴリ(または検索欄)から TrailRenderer を選択して追加します。
- Inspector に TrailRenderer の各プロパティが表示されることを確認します。
4. TrailRenderer のプロパティ設定例
まずは剣の軌跡風の設定例を示します。
- Enabled Trail: チェック ON
- Life Time:
0.35 - Min Distance:
0.05 - Max Points:
64 - Start Width:
0.08 - Start Color: やや青みがかった白(例: R=200, G=230, B=255)
- Use World Space: チェック ON
- Fade Mode: EASE_OUT(最後にスッと消える感じ)
これで、TrailTest ノードが動いた経路に、短くてキレのある軌跡が残るようになります。
5. シーン上での動作確認
動かないノードには軌跡が出ないので、何らかの方法でノードを動かします。簡単な確認方法を二つ紹介します。
方法A: エディタの「プレイ中にマウスでノードをドラッグ」して確認
- TrailTest ノードを選択した状態で、シーンビューに表示されていることを確認します。
- 上部の Play ボタンでゲームを再生します。
- ゲーム再生中に、シーンビューで TrailTest ノードをドラッグして動かしてみます。
ノードの移動経路に沿って、白い線の軌跡が残り、少し時間が経つとフェードアウトして消えていくのが確認できるはずです。
方法B: 簡単な自動回転スクリプトを付けて確認
もっとはっきりした動きを見たい場合、TrailTest ノードに簡易的な回転スクリプトを付けて、円を描くように動かしてみます。
- Assets パネルで右クリック → Create → TypeScript で
SimpleRotator.tsを作成します。 - 以下のような簡単なスクリプトを書きます(完全に任意です)。
import { _decorator, Component, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('SimpleRotator')
export class SimpleRotator extends Component {
@property({ tooltip: '回転速度(度/秒)' })
public speed: number = 180;
private _angle: number = 0;
private _radius: number = 1.0;
private _center: Vec3 = new Vec3();
start() {
// 開始位置を中心にする
this.node.getWorldPosition(this._center);
}
update(deltaTime: number) {
this._angle += this.speed * deltaTime;
const rad = this._angle * Math.PI / 180;
const x = this._center.x + Math.cos(rad) * this._radius;
const z = this._center.z + Math.sin(rad) * this._radius;
this.node.setWorldPosition(x, this._center.y, z);
}
}
- TrailTest ノードに SimpleRotator をアタッチし、speed = 360 程度に設定します。
- ゲームを再生すると、TrailTest ノードが円を描きながら動き、その軌跡として円形の残像ラインが描画されます。
このように、TrailRenderer 自体は位置の更新さえあればよく、動かし方は完全に自由です。剣スイングのアニメーションに追従させたり、弾の移動スクリプトと組み合わせたり、任意のノードにそのまま流用できます。
6. 弾丸への適用例(2D)
2D シューティングなどで弾に短い軌跡を付けたい場合の、典型的な設定例です。
- Life Time: 0.15
- Min Distance: 0.02
- Max Points: 16
- Start Width: 0.03
- Start Color: やや黄色(例: R=255, G=240, B=180)
- Fade Mode: LINEAR
弾のノードに Line2D + TrailRenderer をアタッチし、弾を高速で移動させると、短く鋭い光のラインが後ろに残るようになります。
まとめ
本記事では、Cocos Creator 3.8.7 + TypeScript で、Line2D を内部制御して滑らかな軌跡を描画する汎用 TrailRenderer コンポーネントを実装しました。
- 外部の GameManager やシングルトンに依存せず、このスクリプト単体 + Line2D コンポーネントだけで完結する設計。
- インスペクタから lifeTime / minDistance / maxPoints / startWidth / startColor / fadeMode を調整するだけで、剣・弾・ダッシュ・魔法エフェクトなど様々な軌跡表現に対応可能。
- Line2D の存在チェックや、ポイント数制限など、防御的でパフォーマンスを意識した実装。
一度 TrailRenderer をプロジェクトに入れておけば、「軌跡を付けたいノードに Line2D と TrailRenderer をアタッチするだけ」で、視覚的なクオリティを簡単に底上げできます。剣のスイング、ビームの残光、キャラクターのダッシュエフェクトなど、さまざまな場面で再利用してみてください。
