【Cocos Creator】アタッチするだけ!KeepDistance (距離維持)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

【Cocos Creator 3.8】KeepDistance の実装:アタッチするだけで「ターゲットとの距離を一定に保ちながら射撃戦を行う」汎用スクリプト

このガイドでは、任意の敵キャラやドローンなどにアタッチするだけで、プレイヤー(ターゲット)に近づきすぎず、離れすぎず、一定距離を保ちつつ射撃を行う汎用コンポーネント KeepDistance を実装します。

ターゲットノードをインスペクタで指定するだけで動作し、GameManager や別スクリプトへの依存は一切ありません。距離のしきい値・移動速度・射撃間隔・弾のプレハブなどもすべてインスペクタから調整可能です。


コンポーネントの設計方針

1. 機能要件の整理

  • 指定したターゲットノード(通常はプレイヤー)との距離を監視する。
  • 近すぎる場合: ターゲットから離れる方向に移動する。
  • 遠すぎる場合: ターゲットに近づく方向に移動する。
  • 適正距離の範囲内(ミニマムとマキシマムの間)では、基本的に停止またはゆっくり調整する。
  • ターゲットの方向を向き、一定間隔で弾を発射する。
  • 発射する弾は Prefab としてインスペクタから指定し、弾の初速・寿命・発射位置オフセットも調整可能にする。
  • 2D/3D どちらでも使えるよう、移動は Vec3 ベースで行い、Y 軸を固定するオプションも用意する。

2. 外部依存をなくすためのアプローチ

  • ターゲット参照: @property(Node) でインスペクタから直接指定。
  • 移動制御: RigidBody などには依存せず、this.node.positionupdate() 内で直接更新。
  • 射撃制御: 弾のプレハブ(Prefab)をインスペクタから指定し、instantiate で生成。生成先の親ノードもプロパティで指定できるようにする(未指定なら自ノードの親にぶら下げる)。
  • 防御的実装:
    • ターゲット未設定時は警告を出し、移動と射撃を無効化。
    • 弾プレハブ未設定時は射撃機能のみ無効化し、ログで通知。
    • 親ノードが存在しない(ルートノード)場合の射出先は this.node.scene を利用。

3. インスペクタで設定可能なプロパティ設計

以下のプロパティを用意します。

  • target: Node | null
    • 追従&距離維持の対象となるノード(通常はプレイヤー)。
    • 未設定の場合はログ警告を出し、コンポーネントは何もしない。
  • minDistance: number(例: 3.0)
    • ターゲットからこれより近いと「離れる」挙動になる距離。
  • maxDistance: number(例: 6.0)
    • ターゲットからこれより遠いと「近づく」挙動になる距離。
    • minDistance より常に大きくなるように防御的チェックを行う。
  • moveSpeed: number(例: 4.0)
    • ターゲットに近づく/離れるときの移動速度(ユニット/秒)。
  • stopOnComfortZone: boolean(例: true)
    • 距離が [minDistance, maxDistance] の範囲にあるときに完全停止するかどうか。
    • false の場合は、ちょうど中央の距離を目指して微調整移動する。
  • faceTarget: boolean(例: true)
    • ターゲットの方向を常に向くかどうか。
    • 2D 横スクロールの場合は X 方向の反転のみを行うオプションも用意。
  • use2DFlipX: boolean(例: false)
    • 2D スプライト用の簡易向き制御。
    • true の場合、ターゲットが右にいれば scale.x = |scale.x|、左にいれば scale.x = -|scale.x| にして向きを変える。
  • lockYMovement: boolean(例: true)
    • 移動時に Y 座標を固定するかどうか(2D 横スクロールや平面上の移動向け)。
  • shootEnabled: boolean(例: true)
    • 射撃機能を有効/無効にする。
  • projectilePrefab: Prefab | null
    • 発射する弾のプレハブ。
    • 未設定の場合、射撃処理はスキップし警告ログを出す。
  • shootInterval: number(例: 1.0)
    • 弾を発射する間隔(秒)。
  • projectileSpeed: number(例: 10.0)
    • 弾の初速度(ターゲット方向)。
    • 発射方向ベクトルにこのスカラーを掛けて velocity として設定。
  • projectileLifetime: number(例: 5.0)
    • 弾が自動的に破棄されるまでの時間(秒)。
  • shootOnlyInComfortZone: boolean(例: true)
    • 距離が適正範囲内のときだけ射撃するかどうか。
    • false の場合、距離に関係なく常に射撃間隔で撃ち続ける。
  • shootOriginOffset: Vec3(例: (0, 0.5, 0))
    • 自ノードのローカル座標から見た発射位置オフセット。
    • 敵の手元や銃口の位置に合わせるために使用。
  • projectileParent: Node | null
    • 生成した弾をぶら下げる親ノード。
    • 未指定の場合は this.node.parent(なければシーンルート)を使用。

TypeScriptコードの実装


import { _decorator, Component, Node, Vec3, Prefab, instantiate, math, log, warn, director } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('KeepDistance')
export class KeepDistance extends Component {

    // === ターゲット関連 ===
    @property({
        type: Node,
        tooltip: '距離を維持する対象ノード(通常はプレイヤー)。\n未設定の場合、このコンポーネントは移動・射撃を行いません。'
    })
    public target: Node | null = null;

    @property({
        tooltip: 'ターゲットに対してこれより近いと離れる挙動になる距離。'
    })
    public minDistance: number = 3.0;

    @property({
        tooltip: 'ターゲットに対してこれより遠いと近づく挙動になる距離。\nminDistance より大きい値にしてください。'
    })
    public maxDistance: number = 6.0;

    @property({
        tooltip: 'ターゲットに近づく/離れるときの移動速度(ユニット/秒)。'
    })
    public moveSpeed: number = 4.0;

    @property({
        tooltip: 'ターゲットとの距離が [minDistance, maxDistance] の範囲内のとき完全に停止するかどうか。\nOFF の場合は中央距離を目指して微調整移動します。'
    })
    public stopOnComfortZone: boolean = true;

    @property({
        tooltip: 'ターゲットの方向を常に向くかどうか。'
    })
    public faceTarget: boolean = true;

    @property({
        tooltip: '2D スプライト用の簡易向き制御。\nON の場合、ターゲットが右にいれば scale.x を正、左にいれば負にします。'
    })
    public use2DFlipX: boolean = false;

    @property({
        tooltip: '移動時に Y 座標を固定するかどうか。\n2D 横スクロールや平面上の移動に便利です。'
    })
    public lockYMovement: boolean = true;

    // === 射撃関連 ===
    @property({
        tooltip: '射撃機能を有効にするかどうか。'
    })
    public shootEnabled: boolean = true;

    @property({
        type: Prefab,
        tooltip: '発射する弾のプレハブ。\n未設定の場合、射撃は行われません。'
    })
    public projectilePrefab: Prefab | null = null;

    @property({
        tooltip: '弾を発射する間隔(秒)。'
    })
    public shootInterval: number = 1.0;

    @property({
        tooltip: '弾の初速度(ターゲット方向)。'
    })
    public projectileSpeed: number = 10.0;

    @property({
        tooltip: '弾が自動的に破棄されるまでの時間(秒)。'
    })
    public projectileLifetime: number = 5.0;

    @property({
        tooltip: 'ターゲットとの距離が [minDistance, maxDistance] の範囲内のときのみ射撃するかどうか。'
    })
    public shootOnlyInComfortZone: boolean = true;

    @property({
        type: Vec3,
        tooltip: '自ノードのローカル座標から見た発射位置オフセット。\n敵の手元や銃口の位置に合わせて調整してください。'
    })
    public shootOriginOffset: Vec3 = new Vec3(0, 0.5, 0);

    @property({
        type: Node,
        tooltip: '生成した弾をぶら下げる親ノード。\n未設定の場合、自ノードの親(なければシーンルート)に追加します。'
    })
    public projectileParent: Node | null = null;

    // === 内部状態 ===
    private _shootTimer: number = 0;
    private _initialY: number = 0;
    private _hasLoggedNoTarget: boolean = false;
    private _hasLoggedNoProjectile: boolean = false;

    onLoad() {
        // min/max 距離の防御的な補正
        if (this.maxDistance <= this.minDistance) {
            warn(`[KeepDistance] maxDistance (${this.maxDistance}) が minDistance (${this.minDistance}) 以下です。minDistance + 1 に自動調整します。`);
            this.maxDistance = this.minDistance + 1.0;
        }

        // 初期 Y 座標を記録(lockYMovement 用)
        this._initialY = this.node.position.y;

        // タイマー初期化
        this._shootTimer = 0;
    }

    start() {
        if (!this.target) {
            warn('[KeepDistance] target が設定されていません。このコンポーネントは動作しません。');
            this._hasLoggedNoTarget = true;
        }

        if (!this.projectilePrefab && this.shootEnabled) {
            warn('[KeepDistance] projectilePrefab が設定されていないため、射撃は行われません。');
            this._hasLoggedNoProjectile = true;
        }
    }

    update(deltaTime: number) {
        // ターゲットがいない場合は何もしない
        if (!this.target) {
            // 一度だけ警告ログを出す
            if (!this._hasLoggedNoTarget) {
                warn('[KeepDistance] target が設定されていません。');
                this._hasLoggedNoTarget = true;
            }
            return;
        }

        // 現在位置とターゲット位置を取得
        const selfPos = this.node.worldPosition;
        const targetPos = this.target.worldPosition;

        // 高さを無視して距離計算したい場合は Y を揃える
        const tempSelf = new Vec3(selfPos.x, selfPos.y, selfPos.z);
        const tempTarget = new Vec3(targetPos.x, targetPos.y, targetPos.z);
        if (this.lockYMovement) {
            tempSelf.y = 0;
            tempTarget.y = 0;
        }

        const toTarget = new Vec3();
        Vec3.subtract(toTarget, tempTarget, tempSelf);
        const distance = toTarget.length();

        // 移動方向ベクトル
        let moveDir = new Vec3(0, 0, 0);

        if (distance < this.minDistance) {
            // 近すぎるのでターゲットから離れる方向へ
            if (distance > 0.0001) {
                Vec3.normalize(moveDir, toTarget);
                moveDir.multiplyScalar(-1); // 反対方向
            }
        } else if (distance > this.maxDistance) {
            // 遠すぎるのでターゲットに近づく方向へ
            if (distance > 0.0001) {
                Vec3.normalize(moveDir, toTarget);
            }
        } else {
            // 適正距離範囲内
            if (!this.stopOnComfortZone) {
                // 中央の距離を目指して微調整
                const centerDist = (this.minDistance + this.maxDistance) * 0.5;
                const diff = distance - centerDist;
                if (Math.abs(diff) > 0.05 && distance > 0.0001) {
                    Vec3.normalize(moveDir, toTarget);
                    // diff > 0 なら少し離れる、< 0 なら少し近づく
                    moveDir.multiplyScalar(diff > 0 ? 1 : -1);
                }
            }
        }

        // 実際の移動
        if (!moveDir.equals(Vec3.ZERO)) {
            Vec3.normalize(moveDir, moveDir);
            const moveDelta = new Vec3();
            Vec3.multiplyScalar(moveDelta, moveDir, this.moveSpeed * deltaTime);

            let newPos = this.node.position.clone();
            newPos.add(moveDelta);

            if (this.lockYMovement) {
                newPos.y = this._initialY;
            }

            this.node.setPosition(newPos);
        }

        // ターゲットの方向を向く
        if (this.faceTarget) {
            this._updateFacing(tempSelf, tempTarget);
        }

        // 射撃処理
        if (this.shootEnabled && this.projectilePrefab) {
            const inComfortZone = (distance >= this.minDistance && distance <= this.maxDistance);
            if (!this.shootOnlyInComfortZone || inComfortZone) {
                this._shootTimer += deltaTime;
                if (this._shootTimer >= this.shootInterval) {
                    this._shootTimer = 0;
                    this._shootProjectile();
                }
            } else {
                // 射撃条件を満たさないときはタイマーだけ進める or リセットする設計もあり
                // ここではリセットして「範囲に入ったときからカウント開始」とする
                this._shootTimer = 0;
            }
        } else if (this.shootEnabled && !this.projectilePrefab && !this._hasLoggedNoProjectile) {
            warn('[KeepDistance] projectilePrefab が設定されていないため、射撃は行われません。');
            this._hasLoggedNoProjectile = true;
        }
    }

    /**
     * ターゲットの方向を向く処理
     * 2D 用の FlipX と 3D 用の LookAt を両方サポート
     */
    private _updateFacing(selfPos: Vec3, targetPos: Vec3) {
        const dir = new Vec3();
        Vec3.subtract(dir, targetPos, selfPos);

        // 2D FlipX モード
        if (this.use2DFlipX) {
            const scale = this.node.scale.clone();
            if (dir.x > 0) {
                scale.x = Math.abs(scale.x);
            } else if (dir.x < 0) {
                scale.x = -Math.abs(scale.x);
            }
            this.node.setScale(scale);
            return;
        }

        // 3D の場合は Y 軸を上にした LookAt を簡易実装
        // dir がほぼゼロの場合は何もしない
        if (dir.lengthSqr() < 0.0001) {
            return;
        }

        // LookAt の簡易版(カスタム実装)
        // forward を dir の正規化、up を (0,1,0) として回転を作る
        Vec3.normalize(dir, dir);
        const up = Vec3.UP.clone();
        const right = new Vec3();
        Vec3.cross(right, up, dir);
        if (right.lengthSqr() < 0.0001) {
            // up と dir が平行に近い場合は回転を変更しない
            return;
        }
        Vec3.normalize(right, right);
        const newUp = new Vec3();
        Vec3.cross(newUp, dir, right);

        // 回転行列からクォータニオンを作る
        const m00 = right.x, m01 = right.y, m02 = right.z;
        const m10 = newUp.x, m11 = newUp.y, m12 = newUp.z;
        const m20 = dir.x, m21 = dir.y, m22 = dir.z;

        const trace = m00 + m11 + m22;
        const quat = this.node.rotation.clone();

        if (trace > 0) {
            const s = Math.sqrt(trace + 1.0) * 2;
            quat.w = 0.25 * s;
            quat.x = (m21 - m12) / s;
            quat.y = (m02 - m20) / s;
            quat.z = (m10 - m01) / s;
        } else if ((m00 > m11) && (m00 > m22)) {
            const s = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
            quat.w = (m21 - m12) / s;
            quat.x = 0.25 * s;
            quat.y = (m01 + m10) / s;
            quat.z = (m02 + m20) / s;
        } else if (m11 > m22) {
            const s = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
            quat.w = (m02 - m20) / s;
            quat.x = (m01 + m10) / s;
            quat.y = 0.25 * s;
            quat.z = (m12 + m21) / s;
        } else {
            const s = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
            quat.w = (m10 - m01) / s;
            quat.x = (m02 + m20) / s;
            quat.y = (m12 + m21) / s;
            quat.z = 0.25 * s;
        }

        this.node.setRotation(quat);
    }

    /**
     * 弾を生成してターゲット方向に発射する
     */
    private _shootProjectile() {
        if (!this.projectilePrefab) {
            return;
        }

        // 発射位置(ワールド座標)を計算
        const localOrigin = this.shootOriginOffset.clone();
        const worldOrigin = new Vec3();
        this.node.getWorldMatrix().transformPoint(localOrigin, worldOrigin);

        // インスタンス生成
        const projectile = instantiate(this.projectilePrefab);

        // 親ノードを決定
        let parent: Node | null = this.projectileParent;
        if (!parent) {
            parent = this.node.parent;
        }
        if (!parent) {
            // 親がいない場合はシーンルートに追加
            const scene = director.getScene();
            if (!scene) {
                warn('[KeepDistance] シーンが取得できなかったため、弾を生成できませんでした。');
                return;
            }
            parent = scene;
        }

        projectile.setParent(parent);
        projectile.setWorldPosition(worldOrigin);

        // ターゲット方向に初速度を与える
        if (this.target) {
            const targetPos = this.target.worldPosition;
            const dir = new Vec3();
            Vec3.subtract(dir, targetPos, worldOrigin);

            if (this.lockYMovement) {
                dir.y = 0;
            }

            if (dir.lengthSqr() > 0.0001) {
                Vec3.normalize(dir, dir);
            } else {
                // ターゲットがほぼ同じ位置なら、前方方向(Z+)に撃つ
                dir.set(0, 0, 1);
            }

            const velocity = new Vec3();
            Vec3.multiplyScalar(velocity, dir, this.projectileSpeed);

            // 弾側に velocity プロパティがあれば設定する(任意の実装と相性が良いように)
            const anyProjectile = projectile as any;
            if ('velocity' in anyProjectile) {
                anyProjectile.velocity = velocity;
            }

            // RigidBody 系を使わない独立実装のため、ここでは Transform ベースの簡易移動コンポーネントを
            // 弾プレハブ側に用意しておくことを想定しています。
            // ただしこの KeepDistance 自体は一切依存しません。
        }

        // 一定時間後に自動破棄
        if (this.projectileLifetime > 0) {
            this.scheduleOnce(() => {
                if (projectile && projectile.isValid) {
                    projectile.destroy();
                }
            }, this.projectileLifetime);
        }
    }
}

コードの要点解説

  • onLoad
    • minDistancemaxDistance の関係をチェックし、maxDistance <= minDistance の場合は自動で補正します。
    • Y 座標固定用に初期 Y を記録します。
  • start
    • ターゲット未設定・弾プレハブ未設定の場合に警告ログを出します。
  • update
    • ターゲットがない場合は何もせず早期 return。
    • ターゲットとの距離を計算し、minDistancemaxDistance に応じて「近づく/離れる/停止 or 微調整」を行います。
    • lockYMovement が true の場合、移動後も Y 座標を初期値に固定します。
    • faceTarget が true の場合、ターゲット方向に向くように回転または FlipX を行います。
    • 射撃タイマーを進め、条件を満たしたときに _shootProjectile() を呼び出します。
  • _updateFacing
    • use2DFlipX が true の場合、X 方向の位置関係に応じて scale.x を正負反転します。
    • 3D 向けには、ターゲット方向を forward として簡易的にクォータニオンを構築し、this.node.setRotation() で適用します。
  • _shootProjectile
    • shootOriginOffset をローカル座標としてワールド座標に変換し、その位置に弾を生成します。
    • 弾の親ノードは projectileParentthis.node.parent → シーンルート の優先順で決定します。
    • ターゲット方向に正規化したベクトルに projectileSpeed を掛けて velocity を算出し、弾インスタンスに velocity プロパティがあればそこに代入します(弾側の実装と疎結合にするため)。
    • scheduleOnce を使って projectileLifetime 秒後に自動で destroy() します。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を KeepDistance.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の KeepDistance コードを貼り付けて保存します。

2. テスト用シーンの準備

プレイヤーノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite(または 3D Object → Cube など)を選択し、Player という名前に変更します。
  2. 位置を分かりやすくするため、Inspector の Position(0, 0, 0) に設定します。

敵(距離維持するキャラ)ノードの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite(または 3D Object → Cube)を選択し、Enemy という名前に変更します。
  2. Inspector の Position(5, 0, 0) など、プレイヤーから少し離れた位置に設定します。

3. KeepDistance コンポーネントのアタッチ

  1. Hierarchy で Enemy ノードを選択します。
  2. Inspector の下部にある Add Component ボタンをクリックします。
  3. Custom ComponentKeepDistance を選択して追加します。

4. プロパティの設定

Enemy ノードの Inspector に表示される KeepDistance の各プロパティを以下のように設定してみましょう(2D 横スクロール想定)。

  • target: Hierarchy から Player ノードをドラッグ&ドロップ。
  • minDistance: 3
  • maxDistance: 6
  • moveSpeed: 4
  • stopOnComfortZone: ON
  • faceTarget: ON
  • use2DFlipX: ON(2D Sprite の場合)
  • lockYMovement: ON
  • shootEnabled: ON
  • projectilePrefab: 後述の手順で作成する弾プレハブを設定
  • shootInterval: 1.0(1秒ごとに発射)
  • projectileSpeed: 10
  • projectileLifetime: 5
  • shootOnlyInComfortZone: ON
  • shootOriginOffset: (0, 0.5, 0)(敵の少し上から撃つイメージ)
  • projectileParent: 未設定のままで OK(自動的に Enemy の親ノード or シーンルートに追加されます)

5. 弾プレハブの作成

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、Bullet という名前にします。
  2. 見た目が分かるように小さな画像を設定するか、単色スプライトなどを割り当てます。
  3. Assets パネルの任意のフォルダに Bullet ノードをドラッグ&ドロップして Prefab 化します。
  4. Hierarchy 上の Bullet ノードはもう不要なので削除して構いません。
  5. Enemy ノードの Inspector で、KeepDistance > projectilePrefab に先ほど作成した Bullet プレハブをドラッグ&ドロップします。

※ このガイドでは KeepDistance 自体は弾の移動ロジックを持ちません。
単純に「velocity プロパティ」を弾インスタンスに渡しているだけなので、必要であれば弾プレハブ側に「velocity を使って毎フレーム移動する簡単なスクリプト」を追加するとよいです。(ただし、KeepDistance はそのスクリプトに依存していないため、完全に任意です。)

6. 再生して動作確認

  1. ツールバーの Play ボタン(▶)を押してゲームを実行します。
  2. シーン上で Player を移動させるために、簡単な移動スクリプトを別途用意しても良いですし、エディタの Scene ビュー 上で Player をドラッグして動かしてみても挙動を確認できます(エディタ上のドラッグは再生中の Transform を直接動かせます)。
  3. Enemy が Player に対して
    • 近づきすぎると後退し、
    • 離れすぎると前進し、
    • [minDistance, maxDistance] の範囲で距離を保とうとする

    様子を確認できます。

  4. 距離が適正範囲に入っているときに、一定間隔で弾が Player 方向に発射されることを確認します。

もし動かない、あるいはログに警告が出ている場合は、以下をチェックしてください。

  • Enemy の target に Player が正しく設定されているか。
  • projectilePrefab に Bullet プレハブが設定されているか。
  • shootEnabled が ON になっているか。
  • minDistance < maxDistance になっているか(自動補正されますが、値も確認)。

まとめ

KeepDistance コンポーネントは、

  • ターゲットとの距離を 近すぎず・遠すぎずに保つ AI 的な挙動
  • 距離条件に応じて 射撃を行う戦闘キャラクター の実装

を、単一スクリプトをアタッチするだけで実現できる汎用コンポーネントです。外部の GameManager やシングルトンに一切依存せず、必要なパラメータはすべてインスペクタから設定可能なため、

  • 敵 AI のバリエーション作成(距離や速度、射撃間隔を変えるだけで別キャラに)
  • テスト用のターゲット追従ドローン・護衛キャラの実装
  • チュートリアル用の簡易敵キャラ

など、さまざまなシーンで再利用できます。

本記事の設計方針をベースに、

  • HP やダメージ判定を持つ弾プレハブ
  • 回り込みやランダムなストレイフ移動
  • 複数ターゲットから最も近いものを自動で選ぶ拡張

といった機能を追加していくことで、より高度な敵 AI を構築していくことも可能です。まずはこの KeepDistance をベースに、あなたのゲームに合った距離維持型の射撃キャラクターを量産してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!