【Cocos Creator 3.8】AutoScroll の実装:アタッチするだけで親 ScrollView をゆっくり自動スクロールする汎用スクリプト
ゲームのエンドロールやお知らせ画面などで、テキストをゆっくり自動スクロールさせたい場面はよくあります。本記事では、ScrollView のコンテンツやその子ノードにアタッチするだけで、親の ScrollView を一定速度で自動スクロールしてくれる「AutoScroll」コンポーネントを、Cocos Creator 3.8 / TypeScript で実装します。
外部の GameManager などには一切依存せず、インスペクタで速度やスクロール方向、ループ動作などを調整できる完全独立コンポーネントとして設計します。
コンポーネントの設計方針
要件整理
- このコンポーネントをアタッチしたノードの 親階層にある ScrollView を自動でスクロールさせる。
- 主な用途はクレジット表示なので、縦方向(上方向 or 下方向)へのゆっくりスクロールを想定。
- スクロールが端まで到達したときに、停止・ループ(先頭に戻る)などを選べるようにする。
- ゲームの時間スケール(TimeScale)に影響されないように、リアルタイムベースで動作させる。
- 他のカスタムスクリプトに依存せず、ScrollView さえあれば単体で動作する。
ScrollView との関係
AutoScroll は以下のような前提で動作します。
- AutoScroll をアタッチしたノードの 親階層を上方向にたどり、最初に見つかった ScrollView を自動スクロール対象とする。
- ScrollView の 垂直スクロール(vertical) を利用する。
- 水平方向のスクロールは今回の用途から外し、縦専用とする(必要なら拡張可能な設計にする)。
インスペクタで設定可能なプロパティ
AutoScroll コンポーネントで設定できるプロパティと役割は以下の通りです。
- scrollSpeed (number)
- ツールチップ: 「1秒あたりのスクロール量(0.0〜1.0)。正の値で上方向、負の値で下方向にスクロールします。」
- ScrollView の
scrollToOffsetではなく、normalizedPosition.y を 0.0〜1.0 の範囲で毎フレーム加算するイメージで扱います。 - 例:
0.05にすると、約 20 秒で 0 → 1 までスクロール。
- autoStart (boolean)
- ツールチップ: 「シーン開始時に自動スクロールを開始するかどうか。」
- オンの場合、
start()で自動的にスクロール開始。 - オフの場合、スクリプトの
public startScroll()を他スクリプトから呼び出すことで開始(単体でも動作するが、柔軟性を確保)。
- loopMode (Enum)
- ツールチップ: 「スクロール端に到達した時の動作。」
- 選択肢:
None: 端に到達したら停止。Restart: 端に到達したら反対側の端に戻ってスクロールを継続(ループ)。
- startFromEnd (boolean)
- ツールチップ: 「スクロール開始位置を末尾(下端)からにするかどうか。」
- クレジットを下から上に流す場合などに便利。
trueの場合、開始時にnormalizedPosition.yを0または1に設定(方向に応じて決定)。
- useUnscaledTime (boolean)
- ツールチップ: 「TimeScale の影響を受けないリアルタイムでスクロールするかどうか。」
trueの場合、game.deltaTimeではなくdirector.getTotalFrames()を利用して、実時間ベースで計算(擬似的な方法)。falseの場合、通常のdtを使用。
- debugLog (boolean)
- ツールチップ: 「ScrollView が見つからない場合などに詳細なログを出力するかどうか。」
- 開発中だけオンにしておくとデバッグが楽になります。
※ ScrollView への参照は 自動で親階層から検索するため、インスペクタで手動設定する必要はありません。見つからない場合は警告ログを出して自動スクロールを無効化します。
TypeScriptコードの実装
import { _decorator, Component, Node, ScrollView, Vec2, director, game, Enum, log, warn } from 'cc';
const { ccclass, property } = _decorator;
/**
* スクロール端に到達した時の動作モード
*/
enum AutoScrollLoopMode {
None = 0, // 端で停止
Restart = 1, // 反対側の端に戻ってループ
}
Enum(AutoScrollLoopMode); // インスペクタに表示するための登録
@ccclass('AutoScroll')
export class AutoScroll extends Component {
@property({
tooltip: '1秒あたりのスクロール量(0.0〜1.0)。正の値で上方向、負の値で下方向にスクロールします。',
slide: true,
range: [-1, 1],
})
public scrollSpeed: number = 0.05;
@property({
tooltip: 'シーン開始時に自動スクロールを開始するかどうか。',
})
public autoStart: boolean = true;
@property({
type: AutoScrollLoopMode,
tooltip: 'スクロール端に到達した時の動作。\nNone: 端で停止\nRestart: 反対側の端に戻ってループ',
})
public loopMode: AutoScrollLoopMode = AutoScrollLoopMode.None;
@property({
tooltip: 'スクロール開始位置を末尾(下端 or 上端)からにするかどうか。\nクレジットを下から上に流す場合などに使用します。',
})
public startFromEnd: boolean = false;
@property({
tooltip: 'TimeScale の影響を受けないリアルタイムでスクロールするかどうか。',
})
public useUnscaledTime: boolean = false;
@property({
tooltip: 'ScrollView が見つからない場合などに詳細なログを出力するかどうか。',
})
public debugLog: boolean = false;
// 内部状態
private _scrollView: ScrollView | null = null;
private _isScrolling: boolean = false;
private _lastFrame: number = 0; // useUnscaledTime 用
private _normalizedPos: Vec2 = new Vec2(0, 1); // ScrollView.normalizedPosition をキャッシュ
onLoad() {
// 親階層から ScrollView を検索
this._scrollView = this._findParentScrollView();
if (!this._scrollView) {
warn('[AutoScroll] 親階層に ScrollView が見つかりません。自動スクロールは無効化されます。');
return;
}
// 垂直スクロールが有効かチェック
if (!this._scrollView.vertical) {
warn('[AutoScroll] 対象の ScrollView で vertical が無効になっています。自動スクロールを行うには vertical を有効にしてください。');
}
// 初期位置の設定
this._normalizedPos = this._scrollView.getScrollOffset(); // 実際には normalizedPosition を取得したいが、API上は直接アクセス
// ScrollView.normalizedPosition は public プロパティなので直接参照
// @ts-ignore: プロパティ存在チェックをバイパス
this._normalizedPos = this._scrollView.normalizedPosition.clone();
if (this.startFromEnd) {
this._setStartPositionByDirection();
}
// useUnscaledTime 用に現在のフレームを記録
this._lastFrame = director.getTotalFrames();
if (this.debugLog) {
log('[AutoScroll] onLoad 完了。ScrollView を検出しました。');
}
}
start() {
if (this.autoStart) {
this.startScroll();
}
}
update(dt: number) {
if (!this._isScrolling || !this._scrollView) {
return;
}
// 有効な ScrollView か確認
if (!this._scrollView.isValid) {
warn('[AutoScroll] ScrollView が無効になりました。自動スクロールを停止します。');
this._isScrolling = false;
return;
}
// 実際に使用する dt を計算
let delta = dt;
if (this.useUnscaledTime) {
const currentFrame = director.getTotalFrames();
const frameDiff = currentFrame - this._lastFrame;
this._lastFrame = currentFrame;
// 60fps を基準に実時間を近似
const approxDelta = frameDiff / 60;
delta = approxDelta;
}
// normalizedPosition を更新
// ScrollView.normalizedPosition.y は 0 (下端) 〜 1 (上端)
// scrollSpeed > 0 なら上方向(y を増加)、scrollSpeed < 0 なら下方向(y を減少)
// @ts-ignore
this._normalizedPos = this._scrollView.normalizedPosition.clone();
const newY = this._normalizedPos.y + this.scrollSpeed * delta;
// 端に到達したかどうか判定
if (this.scrollSpeed > 0) {
// 上方向スクロール(y → 1)
if (newY >= 1) {
this._handleReachEnd(1);
return;
}
} else if (this.scrollSpeed 0 (上方向) なら下端(0)から、scrollSpeed < 0 (下方向) なら上端(1)から開始
let startY = 0;
if (this.scrollSpeed > 0) {
startY = 0; // 下端から上へ
} else if (this.scrollSpeed < 0) {
startY = 1; // 上端から下へ
} else {
// scrollSpeed == 0 の場合は特に変更しない
// ただし、ユーザが startFromEnd を true にしている場合もあるので、
// デフォルトで下端から開始するようにしておく
startY = 0;
}
this._setNormalizedY(startY);
}
/**
* ScrollView.normalizedPosition.y を設定するヘルパー
*/
private _setNormalizedY(y: number) {
if (!this._scrollView) {
return;
}
// 範囲を 0〜1 にクランプ
const clampedY = Math.max(0, Math.min(1, y));
// @ts-ignore: normalizedPosition は public プロパティ
const current = this._scrollView.normalizedPosition as Vec2;
const newPos = new Vec2(current.x, clampedY);
// @ts-ignore
this._scrollView.normalizedPosition = newPos;
this._normalizedPos = newPos;
}
}
コードのポイント解説
- onLoad()
this._findParentScrollView()で、自分自身から親方向にたどって最初に見つかった ScrollView を取得します。- ScrollView が見つからなければ
warnを出し、以降の処理はスキップ(防御的実装)。 startFromEndが有効なら、_setStartPositionByDirection()で開始位置を端にセットします。
- start()
autoStartがtrueのときだけstartScroll()を呼び出し、自動スクロールを開始します。
- update(dt)
_isScrollingがtrueかつ_scrollViewが存在するときだけ動作。useUnscaledTimeがtrueの場合、director.getTotalFrames()の差分から擬似的な実時間deltaを計算します。scrollSpeedをnormalizedPosition.yに加算し、0〜1の範囲外に出たら_handleReachEnd()で端到達処理を行います。
- _handleReachEnd()
loopMode === Noneなら端で停止して_isScrolling = false。loopMode === Restartなら0 ↔ 1を行き来するループ動作を行います。
- _findParentScrollView()
this.nodeから親方向にcurrent.parentをたどり、最初に見つかった ScrollView を返します。- これにより、AutoScroll を ScrollView 自身・content・その子ノードのどこに付けても動作します。
使用手順と動作確認
ここからは、実際に Cocos Creator 3.8.7 のエディタ上で AutoScroll を使ってみる手順を説明します。
1. スクリプトファイルの作成
- Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択します。
- 新しく作成されたスクリプトの名前を
AutoScroll.tsに変更します。 - ダブルクリックしてエディタで開き、本文をすべて削除して、上記の TypeScript コードを丸ごと貼り付けて保存します。
2. ScrollView を使ったクレジット用 UI の準備
- Hierarchy パネルで右クリックし、Create → UI → ScrollView を選択します。
- 自動的に ScrollView、Viewport、Content など必要なノードが作成されます。
- ScrollView ノードを選択し、Inspector で以下を確認・設定します。
- ScrollView コンポーネントの
Verticalが ON になっていること。 - クレジットを縦スクロールさせたいので、
Horizontalは OFF で構いません。
- ScrollView コンポーネントの
- ScrollView の
Contentノード配下に、クレジット用の Label や Layout などを配置して、縦長の内容を作成します。 - Content の高さが ScrollView の表示領域より大きくなるように調整します(そうしないとスクロールしても見た目が変わりません)。
3. AutoScroll コンポーネントのアタッチ
AutoScroll は、ScrollView 自身・Content・Content の子ノードなど、ScrollView の親階層に ScrollView が存在するノードならどこに付けても動きます。ここでは分かりやすく Content に付けます。
- Hierarchy で ScrollView → Viewport → Content ノードを選択します。
- Inspector の下部にある Add Component ボタンをクリックします。
- Custom → AutoScroll を選択してアタッチします。
4. プロパティの設定例
クレジットを「下から上にゆっくり流す」典型的な設定例を示します。
- scrollSpeed:
0.03- 値が小さいほどゆっくりスクロールします。
0.02〜0.05くらいがクレジットにちょうど良い範囲です。
- 値が小さいほどゆっくりスクロールします。
- autoStart:
true- シーンが再生されたら自動的にスクロール開始します。
- loopMode:
None- 一番上までスクロールしたら停止します。エンドロールなどでよく使うパターンです。
- startFromEnd:
true- 下端から上方向へ流したいので、有効にしておきます。
- useUnscaledTime:
false(まずはオフでOK)- ゲーム全体の TimeScale を変更する予定がなければオフで問題ありません。
- debugLog:
false(動作確認中だけ true にしてもよい)- ScrollView が見つからないなどの問題がある場合にログが出るので、トラブルシュートに便利です。
5. プレビューで動作確認
- エディタ右上の Preview ボタン(または ▶ ボタン)をクリックしてゲームを実行します。
- ゲーム画面に表示された ScrollView 内のクレジットテキストが、自動的に下から上に向かってゆっくりスクロールしていれば成功です。
- スクロール速度が速すぎる/遅すぎる場合は、scrollSpeed の値を少しずつ調整して、好みの速度に合わせてください。
6. よくあるつまづきポイント
- ScrollView が見つからないと言われる
- AutoScroll をアタッチしたノードの親階層のどこかに ScrollView があるか確認してください。
- ScrollView より上(親)に AutoScroll を付けてしまうと、親方向にたどっても ScrollView が見つかりません。
- スクロールしているようだが見た目が変わらない
- Content の高さが ScrollView の表示領域より小さいと、スクロールしても見た目が変わりません。Content の高さを十分大きくしてください。
- スクロール方向が逆
- 下から上に流したいのに上から下に動いてしまう場合は、
scrollSpeedの符号を逆にしてください(正→負、負→正)。 startFromEndの設定も合わせて見直すと、意図した位置から流し始められます。
- 下から上に流したいのに上から下に動いてしまう場合は、
まとめ
今回実装した AutoScroll コンポーネントは、
- 親階層から ScrollView を自動検出し、
- インスペクタで 速度・ループモード・開始位置・時間スケール依存などを柔軟に調整でき、
- 他のカスタムスクリプトやシングルトンに一切依存しない、完全に独立した汎用スクリプト
として設計されています。
クレジット表示だけでなく、
- お知らせやニュースティッカーの自動スクロール
- ランキングリストの自動デモ表示
- チュートリアルテキストの自動送り
など、ScrollView を使うあらゆる UI で「とりあえず自動で流したい」場面にそのまま再利用できます。
プロジェクト内のどの ScrollView にも、Content かその子ノードに AutoScroll を一つアタッチするだけで自動スクロールを実現できるので、シーンごとに複雑な制御スクリプトを書く必要がなくなり、UI 実装の効率が大きく向上します。
必要に応じて、今後は水平方向の自動スクロールや、一定時間停止後に再開する機能などを追加してもよいでしょう。その際も、本記事の設計方針(外部依存をなくし、インスペクタで完結させる)をベースに拡張していくと、再利用性の高いコンポーネント群を構築できます。




