【Cocos Creator 3.8】NumberTicker(数値ドラム)の実装:アタッチするだけでスコア表示を「パラパラ」とカウントアップさせる汎用スクリプト
スコアやコイン枚数などの数値を更新するとき、いきなり値を切り替えるのではなく、「000 → 137」のようにパラパラとカウントアップしていく演出は、ゲームの気持ちよさを大きく高めます。この記事では、Label が付いたノードにアタッチするだけで、数値がなめらかにカウントアップ/ダウンする汎用コンポーネント NumberTicker を実装します。
外部の GameManager やシングルトンには一切依存せず、すべての設定はインスペクタのプロパティから行えるように設計します。
コンポーネントの設計方針
機能要件の整理
- Label コンポーネントが付いたノードにアタッチして使う。
- スクリプト単体で完結し、他のカスタムスクリプトには依存しない。
- インスペクタから設定した 目標値(targetValue)に向かって数値をアニメーション表示する。
- 更新時間ベース(duration)でゆっくりカウントアップ/ダウンできる。
- 変化量が大きいときでも、最大ステップ数(maxStepCount)を指定して「パラパラ感」を保つ。
- 整数表示を基本としつつ、小数表示もサポート(小数桁数を指定)。
- 先頭ゼロのパディング(例:
000123)をオプションでサポート。 - 目標値の更新を インスペクタからも、コードからも行えるようにする。
- 必要な Label が見つからない場合は エラーログを出して安全に処理を止める。
インスペクタで設定可能なプロパティ
以下のようなプロパティを用意します。
startValue: number- 初期表示値。
- シーン開始時に Label に表示される値。
targetValue: number- 到達させたい最終値。
- インスペクタで変更して Play 中に「Apply Target」ボタンを押すと、その値に向かってアニメーションする。
duration: number- カウントアニメーションの所要時間(秒)。
- 例: 0.5 なら 0.5 秒かけて startValue → targetValue に変化。
maxStepCount: number- カウント中に更新する最大ステップ数。
- 大きな値差(例: 0 → 100000)でも、ステップ数を制限して処理を軽くしつつパラパラ感を維持。
useEasing: boolean- 値の変化にイージングをかけるかどうか。
- オンの場合、序盤ゆっくり・中盤速く・終盤ゆっくりのような自然な動きになる。
decimalPlaces: number- 小数点以下の桁数。
- 0 なら整数表示(例: 123)、2 なら 123.45 のように表示。
useLeadingZeros: boolean- 先頭ゼロで桁数をそろえるかどうか。
- 例: 6 桁指定で
000123のように表示。
minDigits: numberuseLeadingZerosが true の場合に有効。- 表示する最小桁数(例: 6)。
autoStartOnLoad: boolean- シーン開始時に
startValue → targetValueのアニメーションを自動で開始するか。
- シーン開始時に
playOnTargetChangeInInspector: boolean- エディタ上で
targetValueを変更した際に、「Apply Target」ボタンで即アニメーションするかどうか。 - (実行時専用の挙動であり、エディタ編集中は動かない。)
- エディタ上で
さらに、エディタ上から操作しやすいように、ボタンクリック用のダミープロパティを用意します。
applyTargetButton: void(実際には getter を使ってボタン風に見せる)
TypeScriptコードの実装
import { _decorator, Component, Label, log, error, CCFloat, CCInteger } from 'cc';
const { ccclass, property } = _decorator;
/**
* NumberTicker
* Label に表示される数値を、指定時間をかけてパラパラとカウントアップ/ダウンさせるコンポーネント。
*
* 使用条件:
* - このスクリプトをアタッチするノードに Label コンポーネントが付いていること。
*/
@ccclass('NumberTicker')
export class NumberTicker extends Component {
// ====== 基本設定 ======
@property({
tooltip: '初期表示値。シーン開始時にこの値が Label に表示されます。'
})
public startValue: number = 0;
@property({
tooltip: '到達させたい最終値。再生中に値を変更し、「Apply Target」ボタンで適用するとアニメーションします。'
})
public targetValue: number = 0;
@property({
type: CCFloat,
tooltip: 'カウントアニメーションにかける時間(秒)。0 の場合は即座に値が切り替わります。'
})
public duration: number = 0.5;
@property({
type: CCInteger,
tooltip: 'カウント中の最大ステップ数。差分が大きいときもここで指定した回数以内で更新します(0 以下で無制限)。'
})
public maxStepCount: number = 30;
@property({
tooltip: 'true の場合、イージング(イーズアウト)をかけて自然な速度変化にします。'
})
public useEasing: boolean = true;
// ====== 表示設定 ======
@property({
type: CCInteger,
tooltip: '小数点以下の桁数。0 なら整数表示、2 なら 123.45 のように表示します。'
})
public decimalPlaces: number = 0;
@property({
tooltip: 'true の場合、先頭を 0 で埋めて桁数をそろえます(例: 000123)。'
})
public useLeadingZeros: boolean = false;
@property({
type: CCInteger,
tooltip: '先頭ゼロでそろえる最小桁数(useLeadingZeros が true のときのみ有効)。'
})
public minDigits: number = 6;
// ====== 自動再生設定 ======
@property({
tooltip: 'true の場合、シーン開始時に startValue → targetValue のアニメーションを自動で開始します。'
})
public autoStartOnLoad: boolean = false;
@property({
tooltip: 'true の場合、実行中に targetValue を変更し「Apply Target」ボタンを押すと、その値へアニメーションします。'
})
public playOnTargetChangeInInspector: boolean = true;
// ====== 内部状態 ======
private _label: Label | null = null;
private _currentValue: number = 0;
private _startAnimValue: number = 0;
private _endAnimValue: number = 0;
private _elapsed: number = 0;
private _isPlaying: boolean = false;
// ====== エディタから叩ける「ボタン風」プロパティ ======
@property({
tooltip: '(実行中のみ有効)現在の targetValue に向かってカウントアニメーションを開始します。',
visible: true
})
get Apply_Target() {
// getter は値を返さないが、インスペクタ上でボタン風に見せるために使用
return '';
}
set Apply_Target(value: string) {
// インスペクタで値を変更(クリック)されたときに呼ばれる
if (!this.playOnTargetChangeInInspector) {
return;
}
// 実行時のみ動作させる
if (this.node.scene && (this.node.scene as any)._isOnLoadCalled) {
this.playToTarget(this.targetValue);
}
}
// ====== ライフサイクル ======
onLoad() {
this._label = this.getComponent(Label);
if (!this._label) {
error('[NumberTicker] Label コンポーネントが見つかりません。このスクリプトを使用するノードに Label を追加してください。');
return;
}
// 内部の現在値を初期化
this._currentValue = this.startValue;
this._updateLabelText(this._currentValue);
}
start() {
if (!this._label) {
// onLoad でエラー済み
return;
}
if (this.autoStartOnLoad) {
this.playToTarget(this.targetValue);
}
}
update(deltaTime: number) {
if (!this._isPlaying) {
return;
}
if (!this._label) {
return;
}
// duration が 0 以下なら即時反映
if (this.duration <= 0) {
this._currentValue = this._endAnimValue;
this._updateLabelText(this._currentValue);
this._isPlaying = false;
return;
}
this._elapsed += deltaTime;
let t = this._elapsed / this.duration;
if (t >= 1) {
t = 1;
}
// イージング(イーズアウト: t*(2-t))を適用するかどうか
if (this.useEasing) {
t = t * (2 - t);
}
// ステップ数を制限したい場合、t をステップにスナップする
if (this.maxStepCount > 0) {
const step = 1 / this.maxStepCount;
const snapped = Math.floor(t / step) * step;
t = Math.min(snapped, 1);
}
const value = this._startAnimValue + (this._endAnimValue - this._startAnimValue) * t;
this._currentValue = value;
this._updateLabelText(this._currentValue);
if (this._elapsed >= this.duration) {
this._isPlaying = false;
this._currentValue = this._endAnimValue;
this._updateLabelText(this._currentValue);
}
}
// ====== 公開メソッド ======
/**
* 現在値を返します(内部的にはアニメーション中の値)。
*/
public getCurrentValue(): number {
return this._currentValue;
}
/**
* 即座に値を変更し、Label に表示します(アニメーションなし)。
* @param value 設定したい値
*/
public setInstantValue(value: number) {
this._isPlaying = false;
this._elapsed = 0;
this._currentValue = value;
this._startAnimValue = value;
this._endAnimValue = value;
this._updateLabelText(this._currentValue);
}
/**
* 指定した値へ向かってアニメーションします。
* @param target 到達させたい値
*/
public playToTarget(target: number) {
if (!this._label) {
error('[NumberTicker] Label が存在しないため playToTarget は実行できません。');
return;
}
this._startAnimValue = this._currentValue;
this._endAnimValue = target;
this._elapsed = 0;
this._isPlaying = true;
}
/**
* 現在の targetValue に向かってアニメーションします。
* (スクリプトからも呼び出し可能なショートカット)
*/
public play() {
this.playToTarget(this.targetValue);
}
// ====== 内部ユーティリティ ======
/**
* 数値を設定に応じてフォーマットし、Label に反映します。
*/
private _updateLabelText(value: number) {
if (!this._label) {
return;
}
// 小数桁数の処理
let formatted: string;
if (this.decimalPlaces > 0) {
const factor = Math.pow(10, this.decimalPlaces);
const rounded = Math.round(value * factor) / factor;
formatted = rounded.toFixed(this.decimalPlaces);
} else {
// 整数表示
const roundedInt = Math.round(value);
formatted = roundedInt.toString();
}
// 先頭ゼロパディング
if (this.useLeadingZeros) {
const parts = formatted.split('.');
let intPart = parts[0];
const sign = intPart.startsWith('-') ? '-' : '';
if (sign) {
intPart = intPart.substring(1);
}
if (intPart.length < this.minDigits) {
intPart = intPart.padStart(this.minDigits, '0');
}
parts[0] = sign + intPart;
formatted = parts.join('.');
}
this._label.string = formatted;
}
}
コードのポイント解説
- onLoad
this.getComponent(Label)で同じノード上の Label を取得。- 見つからない場合は
errorログを出して、以降の処理では何もしないようにしています。 startValueを内部の_currentValueにセットし、Label に初期表示します。
- start
autoStartOnLoadが true の場合、startValue → targetValueのアニメーションを自動開始します。
- update
_isPlayingが true の間だけアニメーション処理を行います。duration <= 0のときは即座に最終値へスキップ。- 経過時間から
t(0〜1)を算出し、useEasingが true ならt*(2-t)のイーズアウトを適用。 maxStepCountが正の場合は、tをステップ単位にスナップして「パラパラ感」を出します。_startAnimValueと_endAnimValueの間を補間して_currentValueを更新し、Label に反映。- 経過時間が
durationを超えたらアニメーション終了。
- _updateLabelText
decimalPlacesに応じて整数/小数のフォーマットを行います。useLeadingZerosが true の場合、整数部をminDigits桁になるようpadStartで 0 埋めします。
- Apply_Target プロパティ
- Cocos Creator では直接「ボタン」をインスペクタに置けないため、getter/setter を使ったダミープロパティで疑似ボタンを実現しています。
- 実行中にこのプロパティをクリックすると setter が呼ばれ、
playToTarget(this.targetValue)を実行します。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
scripts/ui)を右クリックします。 - Create → TypeScript を選び、ファイル名を
NumberTicker.tsにします。 - 自動生成されたテンプレートコードをすべて削除し、本記事の 「TypeScriptコードの実装」 セクションのコードを丸ごと貼り付けて保存します。
2. テスト用ノードと Label の作成
- 上部メニューから Game → Canvas を作成していない場合は作成します(既にあるなら不要)。
- Hierarchy パネルで Canvas を右クリックし、Create → UI → Label を選択して Label ノードを作成します。
- ノード名は分かりやすく
ScoreLabelなどにしておきましょう。
- ノード名は分かりやすく
- 作成した Label ノードを選択し、Inspector パネルで Label コンポーネントの
stringをとりあえず0にしておきます。
3. NumberTicker コンポーネントのアタッチ
- Hierarchy で先ほど作成した ScoreLabel ノードを選択します。
- Inspector 下部の Add Component ボタンをクリックします。
- Custom Script セクション(または検索バー)から
NumberTickerを選択して追加します。 - このとき、同じノードに Label が存在しないとエラーになるので、必ず Label が付いていることを確認してください。
4. プロパティの設定例
Inspector で NumberTicker の各プロパティを次のように設定してみます。
- startValue:
0 - targetValue:
1000 - duration:
0.8(0.8 秒で 0 → 1000) - maxStepCount:
25(25 ステップでパラパラと更新) - useEasing: チェックを入れる
- decimalPlaces:
0(整数表示) - useLeadingZeros: チェックを入れる
- minDigits:
6(例: 000123 のように6桁表示) - autoStartOnLoad: チェックを入れる
- playOnTargetChangeInInspector: チェックを入れる
5. 再生して動作確認
- エディタ上部の ▶(Play) ボタンを押してゲームを再生します。
- Scene ビューまたは Game ビューで ScoreLabel を観察すると、シーン開始と同時に
000000 → 001000のように、約 0.8 秒かけてパラパラと数値が変化していくはずです。
6. 実行中にターゲット値を変えてみる
再生中にスコアの変化を試したい場合:
- 再生中に Hierarchy から
ScoreLabelを選択します。 - Inspector の NumberTicker コンポーネント内で
targetValueを5000に変更します。 - その下にある Apply_Target プロパティの右側をクリック(値を変更する操作)します。
- これにより setter が呼ばれ、
playToTarget(5000)が実行されます。
- これにより setter が呼ばれ、
- Label の数字が、現在値から
5000に向かって再びパラパラと変化していくのを確認できます。
7. スクリプトから制御する例
他のスクリプトからスコア加算時に呼び出す場合も、NumberTicker 自体は外部に依存していないので、単に参照してメソッドを呼ぶだけです。
import { _decorator, Component, Node } from 'cc';
import { NumberTicker } from './NumberTicker';
const { ccclass, property } = _decorator;
@ccclass('ScoreExample')
export class ScoreExample extends Component {
@property({ type: NumberTicker, tooltip: 'スコア表示用の NumberTicker コンポーネント' })
public scoreTicker: NumberTicker | null = null;
private _score: number = 0;
addScore(amount: number) {
if (!this.scoreTicker) {
return;
}
this._score += amount;
// 現在のアニメーション中の値から新しいスコアへ向かってカウント
this.scoreTicker.playToTarget(this._score);
}
}
このように、NumberTicker は他のゲームロジックに依存していないため、どの UI にも簡単に再利用できます。
まとめ
- NumberTicker は、Label にアタッチするだけで数値をパラパラとカウントアップ/ダウンさせる汎用コンポーネントです。
- 外部の GameManager やシングルトンに依存せず、インスペクタのプロパティだけで完結するように設計しました。
- duration / maxStepCount / useEasing を組み合わせることで、軽快な「ドラム」演出から、ゆっくりしたカウントアップまで柔軟に調整できます。
- decimalPlaces / useLeadingZeros / minDigits により、スコア・時間・通貨など、さまざまな数値表示フォーマットに対応可能です。
- このコンポーネントを使い回すことで、どの画面でも統一された気持ちいい数値演出を簡単に実装でき、UI 演出の開発効率が大きく向上します。
スコアだけでなく、HP やゲージ値、所持金など、あらゆる数値表示に NumberTicker を適用して、ゲーム全体の演出クオリティを底上げしてみてください。




