【Cocos Creator 3.8】SquashStretch の実装:アタッチするだけでジャンプ・着地時の「伸び縮み演出」を実現する汎用スクリプト
キャラクターがジャンプする瞬間に「にゅっ」と縦に伸び、着地した瞬間に「ぺたん」と横に潰れる——2Dアニメやリッチなゲームでよく見る「スquash & Stretch(伸縮)」演出を、Cocos Creator 3.8 で簡単に使い回せるコンポーネントとして実装します。
この SquashStretch コンポーネントをノードにアタッチしておけば、あとはスクリプト経由で「ジャンプ開始」「着地」などのトリガーを呼ぶだけで、柔らかい伸縮アニメーションを自動再生できます。外部の GameManager などに一切依存せず、インスペクタで数値を調整するだけでチューニング可能です。
コンポーネントの設計方針
1. 機能要件の整理
- ノードにアタッチして使う 単体完結 のコンポーネント。
- 「ジャンプ開始」「着地」などのタイミングで
scaleを一時的に変化させ、時間経過で元のスケールに戻す。 - アニメーションは 補間(イージング)付き で、カクつかない自然な伸縮にする。
- 演出の強さ・時間・方向などは インスペクタのプロパティ から調整できる。
- 他のカスタムスクリプトやシングルトンに依存しない。
- 防御的実装:必要な前提(初期スケールなど)を
onLoadで安全に確保し、想定外の値にはログやクランプで対処する。
2. 伸縮の基本パターン
このコンポーネントでは、代表的な 2 つのパターンをサポートします。
- ジャンプ開始(Jump):
Y方向に少し伸び、X方向に少し潰れる→ 元のスケールへ戻る。 - 着地(Land):
Y方向に少し潰れ、X方向に少し伸びる→ 元のスケールへ戻る。
これを「プリセット」として持たせ、playJump() / playLand() というメソッドで再生できるようにします。また、任意のターゲットスケールを指定して再生する playCustom() も用意し、汎用性を高めます。
3. インスペクタで設定可能なプロパティ
以下のようなプロパティ群を設計します。
autoPlayOnStart: boolean
– ゲーム開始時に自動で伸縮アニメーションを再生するかどうか。
– デフォルト:falseautoPlayMode: 'Jump' | 'Land'(実装上は enum)
–autoPlayOnStartが有効な場合、どのプリセットを再生するか。
– デフォルト:Jumpduration: number
– 伸縮アニメーションの合計時間(秒)。
– デフォルト:0.18(短めのキビキビした動き)overshoot: number
– 伸縮の「強さ」。
– 例:0.2の場合、1.0 → 1.2 / 0.8 程度に変化。
– デフォルト:0.18useUniformScale: boolean
– true: X, Y を同じ比率で変化させる(丸いキャラなど)。
– false: X, Y を逆方向に変化させる(横に潰れて縦に伸びる等)。
– デフォルト:falsejumpScaleMultiplierX: number,jumpScaleMultiplierY: number
– ジャンプ時の X/Y 方向倍率。
– 例:1.1 / 0.9 なら「少し横に伸びて縦に縮む」。
– デフォルト: X=0.9, Y=1.1landScaleMultiplierX: number,landScaleMultiplierY: number
– 着地時の X/Y 方向倍率。
– 例:1.1 / 0.8 なら「横に潰れて縦に縮む」。
– デフォルト: X=1.1, Y=0.8allowInterrupt: boolean
– 再生中に別の伸縮リクエストが来た場合、
– true: 現在のアニメを中断して新しいものを開始。
– false: 無視して最後まで再生。
– デフォルト:truedebugLog: boolean
– 動作ログをコンソールに出力するか。
– デフォルト:false
4. 外部依存をなくすためのアプローチ
- 伸縮のトリガーは このコンポーネント自身の public メソッド として提供します。
playJump()playLand()playCustom(targetScaleX, targetScaleY)
他スクリプトから「GetComponent してメソッドを呼ぶ」だけで利用できます。
- 内部状態(元のスケール)は
onLoadで保存し、ノードのscaleを直接操作します。 - アニメーションは
update()による時間制御で行い、Action, Tween, Animation など別コンポーネントには依存しません。
TypeScriptコードの実装
import { _decorator, Component, Node, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* SquashStretch
* ノードの scale を時間的に変化させて「伸縮」演出を行う汎用コンポーネント。
* - 他のスクリプトやシングルトンに依存しない。
* - public メソッドを呼び出すだけでジャンプ/着地演出を再生できる。
*/
enum SquashPresetMode {
Jump = 0,
Land = 1,
}
@ccclass('SquashStretch')
export class SquashStretch extends Component {
@property({
tooltip: 'true の場合、シーン開始時 (onStart) に自動で伸縮アニメーションを再生します。',
})
public autoPlayOnStart: boolean = false;
@property({
tooltip: 'autoPlayOnStart が true のとき、どちらのプリセットを再生するかを指定します。',
type: Number,
})
public autoPlayMode: SquashPresetMode = SquashPresetMode.Jump;
@property({
tooltip: '伸縮アニメーション全体の再生時間(秒)。0.05〜1.0 程度を推奨します。',
min: 0.01,
max: 3.0,
step: 0.01,
})
public duration: number = 0.18;
@property({
tooltip: '伸縮の強さを補正する係数。1.0 でプリセット通り、0.0 で変化なし、2.0 で約2倍の強さになります。',
min: 0.0,
max: 3.0,
step: 0.05,
})
public overshoot: number = 1.0;
@property({
tooltip: 'true の場合、X/Y を同じ比率で変化させます。false の場合、プリセットの X/Y 倍率をそのまま使用します。',
})
public useUniformScale: boolean = false;
@property({
tooltip: 'ジャンプ時の X 方向スケール倍率(基準スケールに対する倍率)。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public jumpScaleMultiplierX: number = 0.9;
@property({
tooltip: 'ジャンプ時の Y 方向スケール倍率(基準スケールに対する倍率)。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public jumpScaleMultiplierY: number = 1.1;
@property({
tooltip: '着地時の X 方向スケール倍率(基準スケールに対する倍率)。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public landScaleMultiplierX: number = 1.1;
@property({
tooltip: '着地時の Y 方向スケール倍率(基準スケールに対する倍率)。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public landScaleMultiplierY: number = 0.8;
@property({
tooltip: 'true の場合、再生中でも新しい伸縮リクエストで上書きします。false の場合、再生中は新しいリクエストを無視します。',
})
public allowInterrupt: boolean = true;
@property({
tooltip: 'true にすると、伸縮開始/終了などのデバッグログをコンソールに出力します。',
})
public debugLog: boolean = false;
// ---- 内部状態 ----
/** 再生前の基準スケール(onLoad 時に取得) */
private _baseScale: Vec3 = new Vec3(1, 1, 1);
/** 現在再生中かどうか */
private _isPlaying: boolean = false;
/** 再生開始からの経過時間(秒) */
private _elapsed: number = 0;
/** アニメーション開始時のスケール */
private _startScale: Vec3 = new Vec3(1, 1, 1);
/** アニメーション中の目標スケール */
private _targetScale: Vec3 = new Vec3(1, 1, 1);
onLoad() {
// 基準スケールを保存
this._baseScale = this.node.scale.clone();
// duration の防御的クランプ
if (this.duration <= 0) {
console.warn('[SquashStretch] duration が 0 以下だったため、0.1 に補正しました。');
this.duration = 0.1;
}
if (this.debugLog) {
console.log('[SquashStretch] onLoad baseScale =', this._baseScale);
}
}
start() {
// シーン開始時に自動再生する場合
if (this.autoPlayOnStart) {
if (this.autoPlayMode === SquashPresetMode.Jump) {
this.playJump();
} else {
this.playLand();
}
}
}
update(deltaTime: number) {
if (!this._isPlaying) {
return;
}
this._elapsed += deltaTime;
const t = math.clamp01(this._elapsed / this.duration);
// イージング関数を適用(前半で素早く変化し、後半でゆっくり戻る)
const easedT = this._easeOutBack(t);
// 線形補間でスケールを更新
const sx = math.lerp(this._startScale.x, this._targetScale.x, easedT);
const sy = math.lerp(this._startScale.y, this._targetScale.y, easedT);
const sz = math.lerp(this._startScale.z, this._targetScale.z, easedT);
this.node.setScale(sx, sy, sz);
if (t >= 1.0) {
// アニメーション終了
this._isPlaying = false;
this._elapsed = 0;
// 最終的には必ず基準スケールに戻す
this.node.setScale(this._baseScale);
if (this.debugLog) {
console.log('[SquashStretch] animation finished, reset to baseScale =', this._baseScale);
}
}
}
// ---- パブリック API(他スクリプトから呼び出す想定) ----
/**
* ジャンプ時のプリセット伸縮を再生します。
*/
public playJump(): void {
const target = this._calculatePresetTargetScale(
this.jumpScaleMultiplierX,
this.jumpScaleMultiplierY
);
this._playInternal(target, 'Jump');
}
/**
* 着地時のプリセット伸縮を再生します。
*/
public playLand(): void {
const target = this._calculatePresetTargetScale(
this.landScaleMultiplierX,
this.landScaleMultiplierY
);
this._playInternal(target, 'Land');
}
/**
* 任意のターゲットスケールを指定して伸縮を再生します。
* @param targetScaleX 目標 X スケール
* @param targetScaleY 目標 Y スケール
*/
public playCustom(targetScaleX: number, targetScaleY: number): void {
const target = new Vec3(
this._baseScale.x * targetScaleX,
this._baseScale.y * targetScaleY,
this._baseScale.z
);
this._playInternal(target, 'Custom');
}
/**
* 再生中の伸縮アニメーションを即座に停止し、スケールを基準値に戻します。
*/
public stopAndReset(): void {
this._isPlaying = false;
this._elapsed = 0;
this.node.setScale(this._baseScale);
if (this.debugLog) {
console.log('[SquashStretch] stopAndReset called, reset to baseScale =', this._baseScale);
}
}
// ---- 内部処理 ----
/**
* プリセット倍率と overshoot / useUniformScale を考慮したターゲットスケールを算出。
*/
private _calculatePresetTargetScale(multX: number, multY: number): Vec3 {
let x = multX;
let y = multY;
// overshoot を掛ける(1.0 でそのまま、2.0 で約2倍の変化)
x = 1.0 + (x - 1.0) * this.overshoot;
y = 1.0 + (y - 1.0) * this.overshoot;
if (this.useUniformScale) {
// X/Y の平均を取って同じ値にする
const avg = (x + y) * 0.5;
x = avg;
y = avg;
}
return new Vec3(
this._baseScale.x * x,
this._baseScale.y * y,
this._baseScale.z
);
}
/**
* 伸縮アニメーションの実際の再生処理。
*/
private _playInternal(targetScale: Vec3, label: string): void {
if (!this.allowInterrupt && this._isPlaying) {
if (this.debugLog) {
console.log('[SquashStretch] animation is already playing, new request ignored. label =', label);
}
return;
}
this._isPlaying = true;
this._elapsed = 0;
// 開始スケールは現在のスケール
this._startScale = this.node.scale.clone();
// ターゲットスケールは計算済みの値
this._targetScale = targetScale.clone();
if (this.debugLog) {
console.log('[SquashStretch] play', label, 'from', this._startScale, 'to', this._targetScale);
}
}
/**
* 伸縮用のイージング関数(OutBack 系)。
* t: 0〜1 → 0〜1
*/
private _easeOutBack(t: number): number {
// 一般的な easeOutBack 実装
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}
}
コードの要点解説
onLoad()
– ノードの現在のscaleを_baseScaleとして保存します。
–durationが 0 以下の場合は 0.1 に補正し、想定外の値で止まらないようにしています。start()
–autoPlayOnStartが true のとき、シーン開始直後にplayJump()またはplayLand()を呼び出し、自動で伸縮を再生します。update(deltaTime)
–_isPlayingが true の間だけ動作します。
– 経過時間_elapsedを進め、t = elapsed / durationからイージングをかけたeasedTを計算。
–_startScale→_targetScaleへの線形補間で現在のスケールを更新します。
–t >= 1になったら再生終了とし、スケールを_baseScaleに戻します。playJump()/playLand()
– プリセット倍率(jumpScaleMultiplierX/Y,landScaleMultiplierX/Y)とovershoot,useUniformScaleを考慮して目標スケールを計算し、_playInternal()を呼び出します。playCustom(x, y)
– 任意の倍率を受け取り、_baseScaleに対する相対倍率として目標スケールを構築します。allowInterrupt
– 再生中に新しいリクエストが来た場合の挙動を制御。
– false の場合、すでに_isPlayingが true なら新しいリクエストを無視します。- イージング関数
_easeOutBack()
– 少し「行き過ぎて戻る」感じの動き(OutBack)を再現し、アニメーションらしい弾力を出しています。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create > TypeScript を選択します。
- 新規スクリプトに
SquashStretch.tsという名前を付けます。 - ダブルクリックしてエディタ(VSCode など)で開き、先ほどの TypeScript コードを 丸ごと貼り付けて保存 します。
2. テスト用ノードの作成
ここでは簡単な 2D シーンでテストする例を示します。
- Hierarchy パネルで右クリック → Create > 2D Object > Sprite を選択し、テスト用のスプライトを作成します。
- 作成した Sprite ノードを選択し、Inspector で
SizeやSpriteFrameを適当に設定してキャラクターっぽく見えるようにします。
3. SquashStretch コンポーネントのアタッチ
- テスト用の Sprite ノードを選択した状態で、右側の Inspector を確認します。
- Add Component ボタンをクリックします。
- Custom カテゴリの中から SquashStretch を選択して追加します。
(Custom に見当たらない場合は、プロジェクトを一度保存し、Cocos Creator を再起動してみてください。)
4. プロパティの設定例
Inspector 上で、SquashStretch の各プロパティを次のように設定してみます。
Auto Play On Start: ONAuto Play Mode: JumpDuration: 0.18Overshoot: 1.2(少し強め)Use Uniform Scale: OFFJump Scale Multiplier X: 0.9Jump Scale Multiplier Y: 1.1Land Scale Multiplier X: 1.1Land Scale Multiplier Y: 0.8Allow Interrupt: ONDebug Log: 必要に応じて ON(動作確認時は ON が便利)
この状態で Play ボタンを押すと、シーン開始と同時にスプライトが「ピョン」と伸びるような動きが 1 回再生されるはずです。
5. 他スクリプトからの呼び出し例
実際のゲームでは、「ジャンプボタンを押した瞬間」「地面に着地した瞬間」などで伸縮を再生したいことが多いです。その場合は、同じノードか子ノードにアタッチした別スクリプトから SquashStretch を取得してメソッドを呼び出します。
import { _decorator, Component, Node, input, Input, EventKeyboard, KeyCode } from 'cc';
import { SquashStretch } from './SquashStretch';
const { ccclass, property } = _decorator;
@ccclass('JumpTestController')
export class JumpTestController extends Component {
@property({
tooltip: '伸縮演出を適用したいノード(通常は自分自身)',
type: Node,
})
public targetNode: Node | null = null;
private _squash: SquashStretch | null = null;
onLoad() {
if (!this.targetNode) {
this.targetNode = this.node;
}
if (this.targetNode) {
this._squash = this.targetNode.getComponent(SquashStretch);
if (!this._squash) {
console.error('[JumpTestController] targetNode に SquashStretch がアタッチされていません。');
}
}
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this);
}
private _onKeyDown(event: EventKeyboard) {
if (!this._squash) {
return;
}
if (event.keyCode === KeyCode.SPACE) {
// スペースキーでジャンプ演出
this._squash.playJump();
} else if (event.keyCode === KeyCode.ENTER) {
// Enter キーで着地演出
this._squash.playLand();
}
}
}
上記のように、SquashStretch 自体は外部依存がなく、他スクリプト側で自由にトリガーできます。
6. 動作確認のポイント
- 再生中のスケールが
Inspectorでリアルタイムに変化するか確認する。 - アニメーション終了後、スケールが 必ず元の値 に戻っているか確認する。
Allow Interruptを OFF にした状態で、連打しても 1 回分しか再生されないことを確認する。Use Uniform Scaleを ON にすると、縦横同じ比率で「ふにゃっ」とする動きになることを確認する。
まとめ
SquashStretchは、ノードにアタッチして public メソッドを呼ぶだけで、ジャンプ・着地などの瞬間的な伸縮演出 を実現できる汎用コンポーネントです。- 外部の GameManager や Tween、Animation コンポーネントに依存せず、単体スクリプトとして完結 しているため、どのプロジェクトにも簡単にコピペ導入できます。
- インスペクタから Duration / Overshoot / プリセット倍率 / Uniform 化 などを調整できるため、キャラクターごとに「柔らかさ」を微調整できます。
- 応用例としては:
- ダメージを受けた瞬間の「ビクッ」としたリアクション。
- UI ボタンのクリック時に「ポヨン」と膨らむ演出。
- 敵撃破時に一瞬潰してから消えるエフェクト。
など、あらゆる「動きにメリハリをつけたい場面」で活用できます。
このコンポーネントをプロジェクトの「標準パーツ」として用意しておくと、演出のクオリティを手軽に底上げでき、シーン内のあらゆるノードに「アタッチして呼ぶだけ」で統一感のあるアニメーション表現を追加できます。




