promo-video
PlaywrightでPhaserゲームの高フレームレートなプロモーション動画を自動で録画します。「プロモ動画」「ゲームプレイ録画」「マーケティング動画」「ゲームキャプチャ」といった操作をトリガーとして起動します。
description の原文を見る
> Record a high-FPS autonomous promo video of a Phaser game using Playwright. Triggers on: promo video, gameplay recording, marketing video, game capture.
SKILL.md 本文
プロモビデオ録画
Phaser ゲームのスムーズな自動プロモ映像を記録して、マーケティング/ソーシャルメディア用に出力します。出力は 50 FPS の MP4 形式でモバイル縦向き (9:16) — TikTok、Reels、Moltbook、X に対応しています。
技術
Playwright の recordVideo は最大 25 FPS で設定オプションがありません。以下の方法で対応します:
- ゲームを 0.5× に遅速 — Phaser の 5 つ全ての時間サブシステムをパッチ
- 必要な期間の 2 倍の時間を記録 — Playwright のネイティブ 25 FPS で
- FFmpeg で 2× 速度化 → 有効な 50 FPS 出力
| パラメータ | デフォルト | 効果 |
|---|---|---|
SLOW_MO_FACTOR | 0.5 | ゲームが半速で実行 → 50 FPS 出力 |
WALL_CLOCK_DURATION | DESIRED_GAME_DURATION / SLOW_MO_FACTOR | 2 倍の期間を記録して正しいゲーム時間を取得 |
VIEWPORT | { width: 1080, height: 1920 } | 9:16 モバイル縦向き (ユーザー指定がない限りデフォ) |
DESIRED_GAME_DURATION | 13000 (ms) | 約 13 秒のゲーム時間 → 約 6.5 秒のプロモクリップ |
前提条件
- Playwright — インストール必須 (
npm install -D @playwright/test && npx playwright install chromium) - FFmpeg — PATH で利用可能 (macOS は
brew install ffmpeg) - 開発サーバー実行中 — ゲームは localhost で提供される必要があります
開始前に両方をチェック:
npx playwright --version
ffmpeg -version | head -1
FFmpeg が見つからない場合、ユーザーに警告してプロモビデオステップをスキップします (非ブロッキング — ゲームは無しでも動作します)。
キャプチャスクリプト — ゲーム固有の適応
すべてのゲームは scripts/capture-promo.mjs のカスタムスクリプトを取得します。サブエージェントは ゲームのソースファイルを読む必要があります 以下を決定するため:
1. 死亡/失敗パッチング (重要)
ビデオはゲームオーバーを見せずに継続的なゲームプレイを表示する必要があります。GameScene.js (または同等物) を読んで、死亡/失敗メソッドを見つけてモンキーパッチします。
探し方: 衝突/死亡時に呼ばれるメソッドを検索します。一般的なパターン:
this.triggerGameOver()— ドッジゲームthis.takeDamage()→this.lives <= 0— マルチライフゲームthis.gameOver()— 直接呼び出しeventBus.emit(Events.PLAYER_HIT)/eventBus.emit(Events.GAME_OVER)— イベント駆動
パッチテンプレート (ゲーム毎に適応):
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene("GameScene");
if (scene) {
// ゲームオーバーへのすべてのパスをパッチ
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
// マルチライフゲームの場合、ダメージも防ぐ:
// scene.takeDamage = () => {};
// scene.playerDied = () => {};
}
});
2. 入力シーケンス生成
ビデオは動的で自然に見えるゲームプレイを表示する必要があります。ゲームの入力処理を読んで以下を決定:
- どのキー — ArrowLeft/ArrowRight? Space? WASD? マウスクリック?
- 入力スタイル — 継続ホールド (移動)、タップ (ジャンプ/シュート)、または両方?
- 移動パターン — プレイヤーは画面を横断、反応的にドッジ、リズミカルにジャンプ?
ゲームタイプ別入力パターン:
| ゲームタイプ | 入力キー | パターン |
|---|---|---|
| サイドドッジ | ArrowLeft, ArrowRight | 交互ホールド (150-600ms) 可変ポーズ、時々ダブルタップ |
| プラットフォーマー / Flappy | Space | リズミカルなタップ (80-150ms ホールド) 可変間隔 (200-800ms) |
| トップダウン | WASD / Arrows | 混合方向ホールド、図形8パターン |
| シューター | ArrowLeft/Right + Space | 移動と連発を交互 |
| クリッカー/タッパー | マウスクリック / Space | 短いポーズで分離された高速バースト |
ロボットっぽくないように見せるため、タイミングをランダム化:
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);
最初にポーズを追加 (1-2秒) — 入場アニメーション再生を許可し、これがフック:
3. ゲームブート検出
/viral-game または /make-game パイプラインで構築されたすべての Phaser ゲームはこれらのグローバルを公開:
window.__GAME__— Phaser.Game インスタンスwindow.__GAME_STATE__— GameState シングルトンwindow.__EVENT_BUS__— EventBus シングルトン
ブートと活発なゲームプレイの両方を待ちます:
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, {
timeout: 10000,
});
4. 時間スケーリング注入
すべての 5 つの Phaser 時間サブシステムを遅速化:
await page.evaluate(
({ factor }) => {
const game = window.__GAME__;
const scene = game.scene.getScene("GameScene");
// 1. Update デルタ — フレームデルタ依存ロジックを遅速化
const originalUpdate = scene.update.bind(scene);
scene.update = function (time, delta) {
originalUpdate(time, delta * factor);
};
// 2. Tweens — すべてのトウィーンアニメーションを遅速化
scene.tweens.timeScale = factor;
// 3. シーンタイマー — scene.time.addEvent() タイマーを遅速化
scene.time.timeScale = factor;
// 4. Physics — Arcade/Matter フィジックスを遅速化
// 注: Arcade フィジックスタイムスケールは逆 (高い = 遅い)
if (scene.physics?.world) {
scene.physics.world.timeScale = 1 / factor;
}
// 5. アニメーション — スプライトアニメーション再生を遅速化
if (scene.anims) {
scene.anims.globalTimeScale = factor;
}
},
{ factor: SLOW_MO_FACTOR },
);
5 つのサブシステム:
- Update デルタ —
scene.update(time, delta * factor)フレームデルタ依存ロジックを遅速化 - Tweens —
scene.tweens.timeScaleすべてのトウィーンアニメーションを遅速化 - シーンタイマー —
scene.time.timeScalescene.time.addEvent()タイマーを遅速化 - Physics —
scene.physics.world.timeScaleArcade/Matter フィジックスを遅速化 (逆を使用:1/factor) - アニメーション —
scene.anims.globalTimeScaleスプライトアニメーション再生を遅速化
5. ビデオ最終化
const video = page.video();
await context.close(); // コンテキストを閉じてビデオファイルを最終化する必要があります
const videoPath = await video.path();
完全なキャプチャスクリプトテンプレート
import { chromium } from "playwright";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_DIR = path.resolve(__dirname, "..");
// --- Config ---
const args = process.argv.slice(2);
function getArg(name, fallback) {
const i = args.indexOf(`--${name}`);
return i !== -1 && args[i + 1] ? args[i + 1] : fallback;
}
const PORT = getArg("port", "3000");
const GAME_URL = `http://localhost:${PORT}/`;
const VIEWPORT = { width: 1080, height: 1920 }; // 9:16 モバイル縦向き
const SLOW_MO_FACTOR = 0.5;
const DESIRED_GAME_DURATION = parseInt(getArg("duration", "13000"), 10);
const WALL_CLOCK_DURATION = DESIRED_GAME_DURATION / SLOW_MO_FACTOR;
const OUTPUT_DIR = path.resolve(PROJECT_DIR, getArg("output-dir", "output"));
const OUTPUT_FILE = path.join(OUTPUT_DIR, "promo-raw.webm");
// <適応: ゲーム固有の入力シーケンスを生成>
function generateInputSequence(totalMs) {
const sequence = [];
let elapsed = 0;
// 入場アニメーションのポーズ
sequence.push({ key: null, holdMs: 0, pauseMs: 1500 });
elapsed += 1500;
// <適応: ゲーム固有のキーとタイミングに置き換え>
const keys = ["ArrowLeft", "ArrowRight"];
let keyIdx = 0;
while (elapsed < totalMs) {
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);
// 時々ダブルタップで多様性を追加
if (Math.random() < 0.15) {
sequence.push({ key: keys[keyIdx], holdMs: 100, pauseMs: 60 });
elapsed += 160;
}
sequence.push({ key: keys[keyIdx], holdMs, pauseMs });
elapsed += holdMs + pauseMs;
// 方向を交互 (時々同じ方向の繰り返し)
if (Math.random() < 0.75) keyIdx = 1 - keyIdx;
}
return sequence;
}
async function captureGameplay() {
console.log("プロモビデオをキャプチャ中...");
console.log(
` URL: ${GAME_URL} | ビューポート: ${VIEWPORT.width}x${VIEWPORT.height}`,
);
console.log(
` ゲーム期間: ${DESIRED_GAME_DURATION}ms | ウォールクロック: ${WALL_CLOCK_DURATION}ms`,
);
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: VIEWPORT,
recordVideo: { dir: OUTPUT_DIR, size: VIEWPORT },
});
const page = await context.newPage();
await page.goto(GAME_URL, { waitUntil: "networkidle" });
// ゲームブート + ゲームプレイアクティブを待機
await page.waitForFunction(() => window.__GAME__?.isBooted, {
timeout: 15000,
});
await page.waitForFunction(() => window.__GAME_STATE__?.started, {
timeout: 10000,
});
await page.waitForTimeout(300);
console.log(" ゲームアクティブ。");
// <適応: 死亡をパッチアウト — GameScene.js から実際のメソッドを検索>
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene("GameScene");
if (scene) {
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
}
});
console.log(" 死亡がパッチされました。");
// すべての 5 つの Phaser 時間サブシステムを遅速化
await page.evaluate(
({ factor }) => {
const game = window.__GAME__;
const scene = game.scene.getScene("GameScene");
const originalUpdate = scene.update.bind(scene);
scene.update = function (time, delta) {
originalUpdate(time, delta * factor);
};
scene.tweens.timeScale = factor;
scene.time.timeScale = factor;
if (scene.physics?.world) scene.physics.world.timeScale = 1 / factor;
if (scene.anims) scene.anims.globalTimeScale = factor;
},
{ factor: SLOW_MO_FACTOR },
);
console.log(` ${SLOW_MO_FACTOR}x に遅速化しました。`);
// 入力シーケンスを実行
const sequence = generateInputSequence(WALL_CLOCK_DURATION);
console.log(
` ${WALL_CLOCK_DURATION}ms で ${sequence.length} 入力を実行中...`,
);
for (const seg of sequence) {
if (!seg.key) {
await page.waitForTimeout(seg.pauseMs);
continue;
}
await page.keyboard.down(seg.key);
await page.waitForTimeout(seg.holdMs);
await page.keyboard.up(seg.key);
if (seg.pauseMs > 0) await page.waitForTimeout(seg.pauseMs);
}
console.log(" 入力完了。");
// ビデオ最終化
const video = page.video();
await context.close();
const videoPath = await video.path();
if (videoPath !== OUTPUT_FILE) {
fs.renameSync(videoPath, OUTPUT_FILE);
}
await browser.close();
console.log(` 生動画: ${OUTPUT_FILE}`);
console.log("完了。");
}
captureGameplay().catch((err) => {
console.error("キャプチャ失敗:", err);
process.exit(1);
});
FFmpeg 変換
記録後、生の遅速 WebM を高 FPS MP4 に変換します。convert-highfps.sh スクリプトはこのスキルに skills/promo-video/scripts/convert-highfps.sh で同梱されています。
# プロジェクトにコピー (オーケストレーターが実行)
cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/scripts/
# 変換を実行
bash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5
スクリプト:
setptsでビデオを1/factorで速度化- 出力フレームレートを
25 / factorに設定 (0.5× 遅速では 50 FPS) - H.264 で
crf 23、yuv420p、faststartでエンコード - 出力期間、フレームレート、ファイルサイズを検証
ビューポートデフォルト
ユーザーが明確に別の要求がない限り、常にモバイル縦向き (9:16) で記録します。根拠:
- ゲームは携帯で遊ぶ — プロモ映像は実際のモバイル体験を表示すべき
- 9:16 は TikTok、Instagram Reels、YouTube Shorts 用ネイティブ
- 1080×1920 は標準解像度
| アスペクト比 | ビューポート | ユースケース |
|---|---|---|
| 9:16 (デフォ) | 1080 × 1920 | モバイル縦向き — TikTok、Reels、Shorts、Moltbook |
| 1:1 | 1080 × 1080 | 正方形 — Instagram フィード、X 投稿 |
| 16:9 | 1920 × 1080 | ランドスケープ — YouTube、トレーラー、デスクトップゲーム |
期間ガイドライン
| ゲームタイプ | 推奨期間 | 理由 |
|---|---|---|
| アーケード/ドッジ | 10-15秒 | 高速アクション、複数ドッジサイクル |
| プラットフォーマー | 15-20秒 | ジャンプタイミング、レベル進行を表示 |
| シューター | 12-18秒 | ターゲティング、敵ウェーブを表示 |
| パズル | 8-12秒 | 1 つの解答シーケンスを表示 |
チェックリスト
キャプチャ実行前:
- 開発サーバーが実行中で応答している
- FFmpeg がシステムにインストール
- Playwright が Chromium でインストール
- ゲームがメニューなしで直接ゲームプレイに起動
- 死亡/失敗メソッドが識別され、パッチされた
- 入力キーがゲームの実際のコントロール一致
- 入場アニメーションポーズが含まれる (1-2秒)
- 出力ディレクトリが存在
キャプチャ後:
- 生 WebM が output/ に存在
- FFmpeg 変換が有効な MP4 を生成
- 期間が生記録の約半分 (速度化が機能)
- フレームレートが 50 FPS
- ビデオがゲームプレイを表示 (黒い画面ではない)
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- opusgamelabs
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/opusgamelabs/game-creator / ライセンス: MIT
関連スキル
seo-maps
ローカルSEO向けのマップインテリジェンス機能です。ジオグリッドのランク追跡、APIを通じたGBPプロフィール監査、Google・Tripadvisor・Trustpilotなど複数プラットフォームのレビュー分析、Google・Bing・Apple・OSM間のNAP(名前・住所・電話番号)検証、競合他社の半径マッピング、APIデータからのLocalBusinessスキーマ生成が可能です。3段階の機能レベルで対応でき、無料版(Overpass + Geoapify)、DataForSEO(フル機能)、DataForSEO + Google(最大カバレッジ)から選択できます。「maps」「geo-grid」「rank tracking」「GBP audit」「review velocity」「competitor radius」「maps analysis」「local rank tracking」「Share of Local Voice」「SoLV」などのキーワードで利用できます。
seo-content-brief
セクションごとの文字数、競合スコアリング、キーワード密度ガイダンス、ページタイプテンプレートを含む競争力のあるSEOコンテンツブリーフを生成します。新規ページのブリーフと既存ページの改善ブリーフの両方に対応しています。ユーザーが「コンテンツブリーフ」「ブリーフを作成」「コンテンツアウトライン」「ブログブリーフ」「サービスページブリーフ」「ブリーフ〜」「ライティングブリーフ」「コンテンツプラン」「アウトライン〜」などと言った場合に使用します。
rakuten-seo
楽天市場の商品名・キャッチコピーをSEO最適化するスキル。「楽天SEO」「商品名最適化」「楽天の商品名」「キャッチコピー」「楽天のタイトル」「商品名を直して」「楽天検索対策」など、楽天市場の商品名やキャッチコピーの作成・改善・チェックに関するリクエストで必ずこのスキルを使う。既存の商品名の改善も、ゼロからの作成も対応。あらゆるジャンル(食品・ファッション・化粧品・家電・サプリ・インテリア・ベビー・ペット・業務用など)に対応。 【ALSEL独自スキル】株式会社ALSEL が、19年・5,000社超の EC 支援で得たノウハウをもとに開発したオリジナルスキルです。
amazon-seo-jp
Amazon.co.jp商品ページのSEO分析・最適化・自動採点スキル v2.0。 COSMO/Rufus/A10アルゴリズムに基づく採点。セラーセントラル出品レポート(.xlsm)を入力すると、 商品タイトル・箇条書き・検索キーワード・商品説明文を100点満点で採点し、 4項目すべての改善案を日本語で出力する。 トリガー: 「Amazon SEO」「商品ページ採点」「Amazon最適化」 「リスティング改善」「Amazon商品名」「箇条書き改善」 「COSMO対応」「Rufus最適化」「Amazon タイトル」 【ALSEL独自スキル】株式会社ALSEL が、19年・5,000社超の EC 支援で得たノウハウをもとに開発したオリジナルスキルです。
rakuten-bulk-control-csv
楽天RMSの一括登録/一括除外/一括更新用CSV(コントロールカラム,商品管理番号 の2列フォーマット)を作成するスキル。商品DL CSV・商品管理画面のコピペ・Excel・PDFなどから商品管理番号を抽出し、Shift-JIS+LF改行で出力する。「一括除外リスト作って」「楽天の除外CSV」「コントロールカラムnで」「2800円以下の商品をdで」「在庫0の商品を一括削除」「商品管理番号抜いてshift-jsで」「このフォーマットで」など、楽天RMSの商品一括処理用CSVを作るタスクで必ずこのスキルを使う。コントロールカラム値(n=新規/d=削除/u=更新)と抽出条件(全件・価格・在庫・販売状態など)をユーザー指示に応じて柔軟に切り替える。 【ALSEL独自スキル】株式会社ALSEL が、19年・5,000社超の EC 支援で得たノウハウをもとに開発したオリジナルスキルです。
amazon-a-plus-content-brief
Amazon A+コンテンツの構成・モジュール選定・画像指示・比較表・FAQを設計するスキル。「A+コンテンツ作って」「Aプラス構成」「ブランドストーリー」「比較表つきA+」「A+モジュール選定」「Amazonのページに画像入れたい」「A+のヘッダー画像」「A+コンテンツマネージャー」など、Amazon A+コンテンツの企画・設計・改善のリクエストで必ずこのスキルを使う。ベーシック17モジュール/Premium追加機能/画像サイズ規定/文字数目安/審査リジェクト要因を踏まえて、デザイナーに渡せるブリーフ形式で出力。あらゆるジャンル(家電・コスメ・食品・アパレル・日用品・ベビー・ペット等)に対応。※ブランドストア(マルチページ)の設計は別スキル `amazon-brand-store-planner`、タイトル・bullet改善は `amazon-title-bullet-rewriter-jp`、メイン画像のチェックは `amazon-main-image-checker`。 【ALSEL独自スキル】株式会社ALSEL が、19年・5,000社超の EC 支援で得たノウハウをもとに開発したオリジナルスキルです。