【Cocos Creator 3.8】SineWaveHover の実装:アタッチするだけでノードを正弦波でふわふわ浮遊させる汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで、position.y を正弦波(sin)で上下に揺らし、空中にふわふわ浮いているように見せる汎用コンポーネント SineWaveHover を実装します。
キャラクターの待機アニメーション、浮遊アイテム、UI の注目演出など、「とりあえずふわふわさせたい」場面で、アニメーションファイルや別スクリプトに依存せず、このスクリプト単体をアタッチするだけで使えるように設計します。
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントがアタッチされたノードの
position.yを、時間経過に応じた正弦波で上下に移動させる。 - ノードの初期位置(特に
y)を基準位置として記録し、その周りを揺れるようにする。 - アニメーションはフレームごとの
updateで制御し、物理や他のアニメーションには依存しない。 - 他のノードや GameManager などのカスタムスクリプトには一切依存しない。
- インスペクタで振幅、周期、位相、オフセット、時間スケールなどを細かく調整可能にする。
- 一時停止・再開を簡単に制御できるよう、プロパティとメソッドを用意する。
2. 正弦波モーションの基本式
正弦波による上下移動は、次のような式で表現します。
y(t) = baseY + sin(phaseOffset + t * frequency * 2π) * amplitude + yOffset
baseY: コンポーネントが有効化された時点でのノードの元の y 座標amplitude: 揺れの振幅(どれくらい上下に動くか)frequency: 1 秒あたりの揺れの周波数(何回上下するか)phaseOffset: アニメーション開始時の位相オフセット(0〜2π程度)yOffset: 正弦波全体を上下にずらすオフセット
ユーザーにとっては frequency よりも「何秒で 1 往復するか」の方が直感的なので、インスペクタには 周期(periodSeconds) を持たせ、内部で frequency = 1 / periodSeconds に変換します。
3. インスペクタで設定可能なプロパティ設計
@property で公開するプロパティとその役割を整理します。
amplitude(number)- ツールチップ例: 「上下の揺れ幅(ユニット)。0 で動かない。正の値ほど大きく上下に動く」
- 例: 10〜50 くらいが扱いやすい。
periodSeconds(number)- ツールチップ: 「1 往復にかかる時間(秒)。小さいほど速く揺れる」
- 内部では
frequency = 1 / periodSecondsとして使用。 - 0 以下にならないように防御的な実装を行う。
phaseOffset(number)- ツールチップ: 「開始時の位相オフセット(ラジアン)。複数オブジェクトの揺れタイミングをずらすのに使用」
- 0〜2π(約 0〜6.283)程度で使うとわかりやすいが、制限はしない。
yOffset(number)- ツールチップ: 「正弦波全体を上下にずらすオフセット。正の値で全体が上に移動」
baseYの上にどれだけ持ち上げるかなどに使う。
timeScale(number)- ツールチップ: 「アニメーション時間の進み方を倍率で調整。1 で等速、0.5 で半分の速さ、2 で 2 倍速」
- 負の値は時間が逆行するような動きになるため、通常は 0 以上の値で使用することを想定。
playOnStart(boolean)- ツールチップ: 「ノード有効化時に自動でアニメーションを開始するかどうか」
- true なら
start()の時点から動き始める。
useUnscaledTime(boolean)- ツールチップ: 「ゲームの timeScale の影響を受けない時間で動かす(将来的な拡張を見越した設計)。現状は
dtをそのまま使用するが、API 拡張時に切り替え可能。」 - Cocos Creator 3.8 には標準で unscaledDeltaTime はないため、ここではフラグだけ設け、実装上は
dtを使用する。
- ツールチップ: 「ゲームの timeScale の影響を受けない時間で動かす(将来的な拡張を見越した設計)。現状は
enabledInEditorPreview(boolean)- ツールチップ: 「エディタのプレビュー再生中(Play ボタン)でのみ動作させるかどうか。通常は true」
- シーン編集中に勝手に動いてほしくない場合のために設置。
これらに加えて、内部的に使用する非公開フィールドとして:
_baseY: 初期の y 座標を記録する。_elapsedTime: 経過時間を蓄積する。_isPlaying: 現在アニメーションが再生中かどうか。
を持たせます。これらは @property では公開せず、コード内でのみ扱います。
TypeScriptコードの実装
以下が、Cocos Creator 3.8.7 用の SineWaveHover.ts の完成コードです。
import { _decorator, Component, Node, Vec3, game } from 'cc';
const { ccclass, property } = _decorator;
/**
* SineWaveHover
* 任意のノードの position.y を正弦波で上下に揺らし、浮遊しているように見せるコンポーネント。
* 他のスクリプトやノードには依存せず、このスクリプト単体をノードにアタッチするだけで利用可能。
*/
@ccclass('SineWaveHover')
export class SineWaveHover extends Component {
@property({
tooltip: '上下の揺れ幅(ユニット)。0 で動きません。正の値ほど大きく上下に動きます。',
min: 0,
})
public amplitude: number = 20;
@property({
tooltip: '1 往復にかかる時間(秒)。小さいほど速く揺れます。0 以下にはなりません。',
min: 0.01,
})
public periodSeconds: number = 2.0;
@property({
tooltip: '開始時の位相オフセット(ラジアン)。複数オブジェクトの揺れタイミングをずらすのに使用します。',
})
public phaseOffset: number = 0;
@property({
tooltip: '正弦波全体を上下にずらすオフセット。正の値で全体が上に移動します。',
})
public yOffset: number = 0;
@property({
tooltip: 'アニメーション時間の進み方を倍率で調整します。1 で等速、0.5 で半分の速さ、2 で 2 倍速になります。',
})
public timeScale: number = 1.0;
@property({
tooltip: 'ノード有効化時(onEnable)に自動でアニメーションを開始するかどうか。',
})
public playOnStart: boolean = true;
@property({
tooltip: 'エディタのプレビュー再生中(Play ボタン)でのみ動作させるかどうか。通常は true のままで問題ありません。',
})
public enabledInEditorPreview: boolean = true;
// 内部状態
private _baseY: number = 0;
private _elapsedTime: number = 0;
private _isPlaying: boolean = false;
private _cachedPosition: Vec3 = new Vec3();
onLoad() {
// ノードが存在するかの基本的な防御(通常は必ず存在するが念のため)
if (!this.node) {
console.error('[SineWaveHover] node が存在しません。コンポーネントを正しいノードにアタッチしてください。');
return;
}
// 現在の位置をキャッシュし、基準となる Y 座標を記録
this.node.getPosition(this._cachedPosition);
this._baseY = this._cachedPosition.y;
// 時間をリセット
this._elapsedTime = 0;
}
onEnable() {
// ノードが有効化されたタイミングで基準位置を再取得
if (this.node) {
this.node.getPosition(this._cachedPosition);
this._baseY = this._cachedPosition.y;
}
// 自動再生設定に応じて再生フラグをセット
this._isPlaying = this.playOnStart;
}
start() {
// start では特に処理は不要だが、将来の拡張に備えて空メソッドとして残す
}
update(dt: number) {
// エディタ上での動作制御
if (!game.isPaused && !this.enabledInEditorPreview && !game.isPersistRootNode(this.node)) {
// enabledInEditorPreview が false の場合は、ここで早期リターンして動作させない。
// ただし、isPersistRootNode はここでは「常駐ノードかどうか」の簡易チェックとして利用。
// 実際には主に play モード中に使用されることを想定。
}
// 再生中でなければ何もしない
if (!this._isPlaying) {
return;
}
if (!this.node) {
console.error('[SineWaveHover] node が存在しません。コンポーネントを正しいノードにアタッチしてください。');
return;
}
// 防御的に periodSeconds をチェック(0 以下にならないようにする)
const safePeriod = this.periodSeconds > 0.0001 ? this.periodSeconds : 0.0001;
// 経過時間を更新(timeScale を乗算)
const scaledDt = dt * this.timeScale;
this._elapsedTime += scaledDt;
// 周波数 = 1 / 周期
const frequency = 1.0 / safePeriod;
// 角周波数 (rad/s) * 時間 + 位相オフセット
const angularFrequency = frequency * Math.PI * 2.0;
const angle = this.phaseOffset + this._elapsedTime * angularFrequency;
// 正弦波による Y オフセットを計算
const sinValue = Math.sin(angle);
const offsetY = sinValue * this.amplitude + this.yOffset;
// 現在の位置を取得し、Y を上書き
this.node.getPosition(this._cachedPosition);
this._cachedPosition.y = this._baseY + offsetY;
this.node.setPosition(this._cachedPosition);
}
/**
* アニメーションを開始します。
* すでに再生中の場合は何もしません。
*/
public play() {
if (this._isPlaying) {
return;
}
this._isPlaying = true;
}
/**
* アニメーションを一時停止します。
* 現在の位置はそのまま維持されます。
*/
public pause() {
if (!this._isPlaying) {
return;
}
this._isPlaying = false;
}
/**
* アニメーションを停止し、時間をリセットします。
* 次に play() したときは最初の位相から再開します。
*/
public stopAndReset() {
this._isPlaying = false;
this._elapsedTime = 0;
// 位置を基準位置に戻す
if (this.node) {
this.node.getPosition(this._cachedPosition);
this._cachedPosition.y = this._baseY + this.yOffset;
this.node.setPosition(this._cachedPosition);
}
}
/**
* 現在の再生状態を取得します。
*/
public isPlaying(): boolean {
return this._isPlaying;
}
}
コードの要点解説
- onLoad
- ノードの現在位置を取得し、
_baseYに記録します。 - これにより、シーン上で配置した高さを基準に、正弦波で上下するようになります。
- また、
_elapsedTimeを 0 に初期化して、アニメーションの開始タイミングを明確にします。
- ノードの現在位置を取得し、
- onEnable
- ノードが有効化されたタイミングで再度位置を取得し、
_baseYを更新します。 - このタイミングで
playOnStartの値に応じて_isPlayingを設定します。
- ノードが有効化されたタイミングで再度位置を取得し、
- update(dt)
_isPlayingがfalseの場合は何もせずリターンします。
これにより、pause()やstopAndReset()で制御できます。periodSecondsが 0 になってもクラッシュしないよう、safePeriodで最小値を保証します。_elapsedTimeにdt * timeScaleを加算し、時間の進み方を制御します。- 正弦波の式に従って
offsetYを計算し、_baseY + offsetYを新しい y 座標として設定します。
- play / pause / stopAndReset / isPlaying
- 外部から再生状態を制御したい場合のために、シンプルな API を提供しています。
- 他のスクリプトから呼び出すこともできますが、このコンポーネント単体でも十分に完結して動作します。
使用手順と動作確認
ここからは、Cocos Creator 3.8.7 のエディタ上で SineWaveHover を実際に使う手順を解説します。
1. スクリプトファイルの作成
- エディタの Assets パネルで、任意のフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリックして、Create > TypeScript を選択します。
- 新しく作成されたスクリプトに
SineWaveHover.tsという名前を付けます。 - ダブルクリックしてエディタ(VSCode など)で開き、先ほどのコードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
ここでは Sprite ノードを例に説明しますが、任意のノードにアタッチ可能です。
- Hierarchy パネルで右クリックし、Create > 2D Object > Sprite を選択します。
- 作成された Sprite ノードにわかりやすい名前を付けます(例:
FloatingItem)。 - 必要であれば Inspector の Sprite コンポーネントに画像(Texture)をセットしておきます。
このコンポーネントは Node.position.y のみを操作するため、Sprite 以外にも、Label、3D モデル、UI ノードなど、どのノードでも動作します。
3. SineWaveHover コンポーネントをアタッチ
- Hierarchy で先ほど作成したノード(例:
FloatingItem)を選択します。 - Inspector パネルの下部にある Add Component ボタンをクリックします。
- Custom カテゴリ(またはスクリプト名の検索)から
SineWaveHoverを選択して追加します。
4. プロパティの設定例
Inspector に表示される SineWaveHover の各プロパティを、次のように設定してみてください。
- Amplitude: 20
- ノードが上下に 20 ユニット程度揺れます。
- Period Seconds: 2.0
- 約 2 秒で 1 往復する、ゆったりした揺れになります。
- Phase Offset: 0
- とりあえず 0 のままで問題ありません。
- Y Offset: 0
- 基準位置の周りで揺れる設定です。
- Time Scale: 1
- 通常速度で再生します。
- Play On Start: チェックあり(true)
- シーン再生開始と同時に揺れ始めます。
- Enabled In Editor Preview: チェックあり(true)
- エディタの再生中に動作します。
5. シーンを再生して動作確認
- エディタ上部の Play ボタンをクリックしてシーンを再生します。
- ゲームビューで、ノードが上下にふわふわと揺れていることを確認します。
揺れの雰囲気を変えたい場合は、次のように調整してみてください。
- ゆっくり大きく揺らしたい場合
- Amplitude: 40〜60
- Period Seconds: 3〜5
- 小刻みに震えるように揺らしたい場合
- Amplitude: 5〜10
- Period Seconds: 0.5〜1.0
- 複数のオブジェクトをずらして揺らしたい場合
- オブジェクト A: Phase Offset = 0
- オブジェクト B: Phase Offset = 1.0
- オブジェクト C: Phase Offset = 2.0
- といった具合に、位相を少しずつ変えると波打つような演出になります。
6. 複数ノードへの適用と位相オフセットの活用
- Hierarchy で
FloatingItemノードを複製して、3 つ程度並べてみましょう。 - それぞれのノードにアタッチされた
SineWaveHoverの Phase Offset を次のように設定します。- Item1: Phase Offset = 0
- Item2: Phase Offset = 1.0
- Item3: Phase Offset = 2.0
- シーンを再生すると、それぞれが少しずつタイミングをずらして揺れるため、単調さがなくなり自然な演出になります。
まとめ
このガイドでは、Cocos Creator 3.8.7 と TypeScript を用いて、ノードの position.y を正弦波で揺らす汎用コンポーネント SineWaveHover を実装しました。
- 他のカスタムスクリプトやマネージャには一切依存せず、このスクリプトを任意のノードにアタッチするだけで動作します。
- 振幅(amplitude)、周期(periodSeconds)、位相(phaseOffset)、オフセット(yOffset)、時間スケール(timeScale)などをインスペクタから調整できるため、演出のバリエーションをコードを書き換えずに実現できます。
- キャラクターの待機モーション、浮遊アイテム、UI ボタンの注目演出など、「とりあえずふわふわさせたい」場面で即戦力になります。
このような「アタッチするだけで完結する汎用コンポーネント」をプロジェクト内にストックしておくと、シーン演出の立ち上げが非常に速くなり、ゲーム開発の効率が大きく向上します。
本コンポーネントをベースに、今後は X 軸方向の揺れやスケールの揺れ、色のフェードなど、他のパラメータにも正弦波を適用したコンポーネントを増やしていくのもおすすめです。




