promo-video
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePromo Video Recording
宣传视频录制
Record smooth, autonomous promo footage of a Phaser game for marketing / social media. The output is a 50 FPS MP4 in mobile portrait (9:16) — ready for TikTok, Reels, Moltbook, or X.
为营销/社交媒体平台录制流畅的Phaser游戏自动宣传片段,输出为9:16移动端竖屏的50 FPS MP4文件,可直接用于TikTok、Reels、Moltbook或X平台。
Technique
实现技术
Playwright's caps at 25 FPS with no config option. We work around it:
recordVideo- Slow the game to 0.5× by patching all 5 Phaser time subsystems
- Record for 2× the desired duration at Playwright's native 25 FPS
- FFmpeg speed-up 2× → effective 50 FPS output
| Parameter | Default | Effect |
|---|---|---|
| | Game runs at half speed → 50 FPS output |
| | Record for 2× to get correct game-time |
| | 9:16 mobile portrait (always default unless user specifies otherwise) |
| | ~13s of game-time → ~6.5s promo clip |
Playwright的功能默认帧率上限为25FPS且无自定义配置选项,我们通过以下方案绕开限制:
recordVideo- 将游戏速度降至0.5倍:通过修改Phaser的全部5个时间子系统实现
- 以Playwright原生25FPS录制2倍所需时长的内容
- 通过FFmpeg将视频2倍速加速 → 最终得到等效50FPS的输出
| 参数 | 默认值 | 作用 |
|---|---|---|
| | 游戏以半速运行 → 输出50FPS视频 |
| | 录制2倍时长以得到正确的游戏内时长 |
| | 9:16移动端竖屏(无用户特殊指定时默认使用该配置) |
| | 约13秒游戏内时长 → 输出约6.5秒宣传片段 |
Prerequisites
前置依赖
- Playwright — must be installed ()
npm install -D @playwright/test && npx playwright install chromium - FFmpeg — must be available on PATH (on macOS)
brew install ffmpeg - Dev server running — game must be served on localhost
Check both before starting:
bash
npx playwright --version
ffmpeg -version | head -1If FFmpeg is not found, warn the user and skip the promo video step (it's non-blocking — the game still works without it).
- Playwright — 必须已安装(执行)
npm install -D @playwright/test && npx playwright install chromium - FFmpeg — 必须已添加到系统PATH中(macOS可通过安装)
brew install ffmpeg - 开发服务正在运行 — 游戏必须已在本地localhost上启动
开始录制前请先检查依赖是否正常:
bash
npx playwright --version
ffmpeg -version | head -1如果未找到FFmpeg,请向用户发出警告并跳过宣传视频生成步骤(该步骤是非阻塞的,没有FFmpeg游戏也可以正常运行)。
Capture Script — Game-Specific Adaptation
捕捉脚本 — 针对游戏适配调整
Every game gets a custom . The subagent must read the game's source files to determine:
scripts/capture-promo.mjs每个游戏都需要一个自定义的脚本。子Agent 必须读取游戏的源文件 来确定以下配置:
scripts/capture-promo.mjs1. Death/Failure Patching (CRITICAL)
1. 死亡/失败逻辑屏蔽(关键步骤)
The video must show continuous gameplay — never game over. Read (or equivalent) to find the death/failure method and monkey-patch it out.
GameScene.jsHow to find it: Search for the method called on collision/death. Common patterns:
- — dodge games
this.triggerGameOver() - →
this.takeDamage()— multi-life gamesthis.lives <= 0 - — direct call
this.gameOver() - /
eventBus.emit(Events.PLAYER_HIT)— event-driveneventBus.emit(Events.GAME_OVER)
Patch template (adapt per game):
js
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene('GameScene');
if (scene) {
// Patch ALL paths to game over
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
// For multi-life games, also prevent damage:
// scene.takeDamage = () => {};
// scene.playerDied = () => {};
}
});视频需要展示连续的游戏玩法,不能出现游戏结束的画面。读取(或对应的场景文件)找到死亡/失败触发方法,通过猴子补丁将其功能屏蔽。
GameScene.js查找方法:搜索碰撞/死亡时调用的方法,常见的模式包括:
- — 闪避类游戏
this.triggerGameOver() - →
this.takeDamage()— 多生命类游戏this.lives <= 0 - — 直接调用的结束方法
this.gameOver() - /
eventBus.emit(Events.PLAYER_HIT)— 事件驱动模式eventBus.emit(Events.GAME_OVER)
补丁模板(根据游戏情况调整):
js
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene('GameScene');
if (scene) {
// 屏蔽所有触发游戏结束的路径
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
// 多生命类游戏还需要屏蔽受伤逻辑:
// scene.takeDamage = () => {};
// scene.playerDied = () => {};
}
});2. Input Sequence Generation
2. 输入序列生成
The video must show dynamic, natural-looking gameplay. Read the game's input handling to determine:
- Which keys — ArrowLeft/ArrowRight? Space? WASD? Mouse clicks?
- Input style — continuous hold (movement), tap (jump/shoot), or both?
- Movement pattern — should the player sweep across the screen, dodge reactively, jump rhythmically?
Input patterns by game type:
| Game Type | Input Keys | Pattern |
|---|---|---|
| Side dodger | ArrowLeft, ArrowRight | Alternating holds (150-600ms) with variable pauses, occasional double-taps |
| Platformer / Flappy | Space | Rhythmic taps (80-150ms hold) with variable gaps (200-800ms) |
| Top-down | WASD / Arrows | Mixed directional holds, figure-eight patterns |
| Shooter | ArrowLeft/Right + Space | Movement interleaved with rapid fire |
| Clicker/Tapper | Mouse click / Space | Rapid bursts separated by brief pauses |
Randomize timing to avoid robotic-looking movement:
js
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);Add a pause at the start (1-2s) to let the entrance animation play — this is the hook.
视频需要展示动态、自然的游戏玩法。读取游戏的输入处理逻辑来确定以下内容:
- 支持的按键:是左/右方向键?空格?WASD?鼠标点击?
- 输入风格:是长按(移动)、点按(跳跃/射击)还是两者结合?
- 移动模式:玩家应该是横扫屏幕、反应式闪避、还是有节奏地跳跃?
不同游戏类型对应的输入模式:
| 游戏类型 | 输入按键 | 模式 |
|---|---|---|
| 横向闪避类 | 左方向键、右方向键 | 交替长按(150-600毫秒)搭配可变时长停顿,偶尔穿插双击 |
| 平台跳跃 / 类Flappy游戏 | 空格 | 有节奏的点按(80-150毫秒长按)搭配可变间隔(200-800毫秒) |
| 俯视视角游戏 | WASD / 方向键 | 混合方向长按,走八字形路线 |
| 射击类 | 左/右方向键 + 空格 | 移动和快速射击交替进行 |
| 点击类 | 鼠标点击 / 空格 | 快速点按 burst 搭配短暂停顿 |
随机化时间间隔避免动作看起来像机器人:
js
const holdMs = 150 + Math.floor(Math.random() * 450);
const pauseMs = 50 + Math.floor(Math.random() * 250);在开头添加1-2秒停顿让入场动画播放完毕 —— 这是吸引用户的钩子。
3. Game Boot Detection
3. 游戏启动检测
All games built with the make-game pipeline expose these globals:
- — Phaser.Game instance
window.__GAME__ - — GameState singleton
window.__GAME_STATE__ - — EventBus singleton
window.__EVENT_BUS__
Wait for both boot and active gameplay:
js
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });所有基于make-game流水线构建的游戏都会暴露以下全局变量:
- — Phaser.Game实例
window.__GAME__ - — GameState单例
window.__GAME_STATE__ - — EventBus单例
window.__EVENT_BUS__
等待游戏启动完成并进入可玩状态:
js
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });4. Time Scaling Injection
4. 时间缩放注入
Slow all 5 Phaser time subsystems for the recording:
js
await page.evaluate(({ factor }) => {
const game = window.__GAME__;
const scene = game.scene.getScene('GameScene');
// 1. Update delta — slows frame-delta-dependent logic
const originalUpdate = scene.update.bind(scene);
scene.update = function(time, delta) {
originalUpdate(time, delta * factor);
};
// 2. Tweens — slows all tween animations
scene.tweens.timeScale = factor;
// 3. Scene timers — slows scene.time.addEvent() timers
scene.time.timeScale = factor;
// 4. Physics — slows Arcade/Matter physics
// NOTE: Arcade physics timeScale is INVERSE (higher = slower)
if (scene.physics?.world) {
scene.physics.world.timeScale = 1 / factor;
}
// 5. Animations — slows sprite animation playback
if (scene.anims) {
scene.anims.globalTimeScale = factor;
}
}, { factor: SLOW_MO_FACTOR });The 5 subsystems:
- Update delta — slows frame-delta-dependent logic
scene.update(time, delta * factor) - Tweens — slows all tween animations
scene.tweens.timeScale - Scene timers — slows
scene.time.timeScaletimersscene.time.addEvent() - Physics — slows Arcade/Matter physics (uses inverse:
scene.physics.world.timeScale)1/factor - Animations — slows sprite animation playback
scene.anims.globalTimeScale
录制时放慢Phaser的全部5个时间子系统:
js
await page.evaluate(({ factor }) => {
const game = window.__GAME__;
const scene = game.scene.getScene('GameScene');
// 1. 更新delta —— 放慢依赖帧delta的逻辑
const originalUpdate = scene.update.bind(scene);
scene.update = function(time, delta) {
originalUpdate(time, delta * factor);
};
// 2. 补间动画 —— 放慢所有tween动画
scene.tweens.timeScale = factor;
// 3. 场景计时器 —— 放慢scene.time.addEvent()创建的计时器
scene.time.timeScale = factor;
// 4. 物理引擎 —— 放慢Arcade/Matter物理引擎
// 注意:Arcade物理引擎的timeScale是倒数关系(数值越大速度越慢)
if (scene.physics?.world) {
scene.physics.world.timeScale = 1 / factor;
}
// 5. 精灵动画 —— 放慢精灵动画播放速度
if (scene.anims) {
scene.anims.globalTimeScale = factor;
}
}, { factor: SLOW_MO_FACTOR });5个时间子系统说明:
- 更新delta —— 放慢依赖帧delta的逻辑
scene.update(time, delta * factor) - 补间动画 —— 放慢所有tween动画
scene.tweens.timeScale - 场景计时器 —— 放慢
scene.time.timeScale创建的计时器scene.time.addEvent() - 物理引擎 —— 放慢Arcade/Matter物理引擎(使用倒数:
scene.physics.world.timeScale)1/factor - 精灵动画 —— 放慢精灵动画播放速度
scene.anims.globalTimeScale
5. Video Finalization
5. 视频生成完成
js
const video = page.video();
await context.close(); // MUST close context to finalize the video file
const videoPath = await video.path();js
const video = page.video();
await context.close(); // 必须关闭context才能完成视频文件的写入
const videoPath = await video.path();Full Capture Script Template
完整捕捉脚本模板
js
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 mobile portrait
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');
// <ADAPT: Generate game-specific input sequence>
function generateInputSequence(totalMs) {
const sequence = [];
let elapsed = 0;
// Pause for entrance animation
sequence.push({ key: null, holdMs: 0, pauseMs: 1500 });
elapsed += 1500;
// <ADAPT: Replace with game-specific keys and timing>
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);
// Occasional double-tap for variety
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;
// Alternate direction (with occasional same-direction repeats)
if (Math.random() < 0.75) keyIdx = 1 - keyIdx;
}
return sequence;
}
async function captureGameplay() {
console.log('Capturing promo video...');
console.log(` URL: ${GAME_URL} | Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`);
console.log(` Game duration: ${DESIRED_GAME_DURATION}ms | Wall clock: ${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' });
// Wait for game boot + gameplay active
await page.waitForFunction(() => window.__GAME__?.isBooted, { timeout: 15000 });
await page.waitForFunction(() => window.__GAME_STATE__?.started, { timeout: 10000 });
await page.waitForTimeout(300);
console.log(' Game active.');
// <ADAPT: Patch out death — find the actual methods from GameScene.js>
await page.evaluate(() => {
const scene = window.__GAME__.scene.getScene('GameScene');
if (scene) {
scene.triggerGameOver = () => {};
scene.onPlayerHit = () => {};
}
});
console.log(' Death patched.');
// Slow all 5 Phaser time subsystems
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(` Slowed to ${SLOW_MO_FACTOR}x.`);
// Execute input sequence
const sequence = generateInputSequence(WALL_CLOCK_DURATION);
console.log(` Playing ${sequence.length} inputs over ${WALL_CLOCK_DURATION}ms...`);
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(' Input complete.');
// Finalize video
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(` Raw recording: ${OUTPUT_FILE}`);
console.log('Done.');
}
captureGameplay().catch(err => { console.error('Capture failed:', err); process.exit(1); });js
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, '..');
// --- 配置项 ---
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(` 访问地址: ${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(' 死亡逻辑已屏蔽。');
// 放慢Phaser的全部5个时间子系统
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}倍速。`);
// 执行输入序列
const sequence = generateInputSequence(WALL_CLOCK_DURATION);
console.log(` 正在执行${sequence.length}个输入操作,总时长${WALL_CLOCK_DURATION}ms...`);
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 Conversion
FFmpeg格式转换
After recording, convert the raw slow-mo WebM to a high-FPS MP4. The script is bundled with this skill at .
convert-highfps.shskills/promo-video/scripts/convert-highfps.shbash
undefined录制完成后,将慢动作的原始WebM文件转换为高帧率MP4文件。脚本已经随该Skill打包在路径下。
convert-highfps.shskills/promo-video/scripts/convert-highfps.shbash
undefinedCopy to project (orchestrator does this)
复制到项目目录(编排器会自动执行该步骤)
cp <plugin-root>/skills/promo-video/scripts/convert-highfps.sh <project-dir>/scripts/
cp <插件根目录>/skills/promo-video/scripts/convert-highfps.sh <项目目录>/scripts/
Run conversion
执行转换
bash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5
The script:
- Applies `setpts` to speed up the video by `1/factor`
- Sets output framerate to `25 / factor` (= 50 FPS for 0.5× slow-mo)
- Encodes H.264 with `crf 23`, `yuv420p`, `faststart`
- Verifies output duration, frame rate, and file sizebash scripts/convert-highfps.sh output/promo-raw.webm output/promo.mp4 0.5
该脚本会执行以下操作:
- 应用`setpts`参数将视频加速`1/factor`倍
- 将输出帧率设置为`25 / factor`(0.5倍慢动作时对应50FPS)
- 使用`crf 23`、`yuv420p`、`faststart`参数编码为H.264格式
- 校验输出文件的时长、帧率和文件大小是否正常Viewport Defaults
视口默认配置
Always record in mobile portrait (9:16) unless the user explicitly requests otherwise. Rationale:
- Games are played on phones — promo footage should show the real mobile experience
- 9:16 is native for TikTok, Instagram Reels, YouTube Shorts
- 1080×1920 is the standard resolution
| Aspect Ratio | Viewport | Use Case |
|---|---|---|
| 9:16 (default) | | Mobile portrait — TikTok, Reels, Shorts, Moltbook |
| 1:1 | | Square — Instagram feed, X posts |
| 16:9 | | Landscape — YouTube, trailers, desktop games |
除非用户明确要求,否则始终使用移动端竖屏(9:16)录制,原因如下:
- 游戏主要在手机上游玩,宣传片段应该展示真实的移动端体验
- 9:16是TikTok、Instagram Reels、YouTube Shorts的原生比例
- 1080×1920是标准分辨率
| 宽高比 | 视口大小 | 使用场景 |
|---|---|---|
| 9:16(默认) | | 移动端竖屏 —— TikTok、Reels、Shorts、Moltbook |
| 1:1 | | 正方形 —— Instagram信息流、X帖子 |
| 16:9 | | 横屏 —— YouTube、预告片、桌面端游戏 |
Duration Guidelines
时长指导
| Game Type | Recommended Duration | Why |
|---|---|---|
| Arcade / dodger | 10-15s | Fast action, multiple dodge cycles |
| Platformer | 15-20s | Show jump timing, level progression |
| Shooter | 12-18s | Show targeting, enemy waves |
| Puzzle | 8-12s | Show one solve sequence |
| 游戏类型 | 推荐时长 | 原因 |
|---|---|---|
| 街机 / 闪避类 | 10-15秒 | 快节奏动作,可以展示多个闪避循环 |
| 平台跳跃类 | 15-20秒 | 展示跳跃时机、关卡进度 |
| 射击类 | 12-18秒 | 展示瞄准操作、敌人波次 |
| 解谜类 | 8-12秒 | 展示一个完整的解谜流程 |
Checklist
检查清单
Before running the capture:
- Dev server is running and responding
- FFmpeg is installed on the system
- Playwright is installed with Chromium
- Game boots directly into gameplay (no menu blocking)
- Death/failure method identified and patched
- Input keys match the game's actual controls
- Entrance animation pause is included (1-2s)
- Output directory exists
After capture:
- Raw WebM exists in output/
- FFmpeg conversion produces valid MP4
- Duration is ~half the raw recording (speed-up worked)
- Frame rate is 50 FPS
- Video shows gameplay (not a black screen)
运行录制前:
- 开发服务正在运行且可以正常访问
- 系统已安装FFmpeg
- Playwright已安装且包含Chromium
- 游戏启动后直接进入玩法界面(没有菜单阻挡)
- 已找到死亡/失败方法并完成屏蔽
- 输入按键和游戏实际控制逻辑匹配
- 已添加1-2秒的入场动画停顿
- 输出目录已存在
录制完成后:
- 原始WebM文件已生成在output/目录下
- FFmpeg转换生成了有效的MP4文件
- 输出视频时长约为原始录制时长的一半(加速生效)
- 输出视频帧率为50FPS
- 视频展示的是正常游戏画面(不是黑屏)