完成项目基础搭建:配置 Vite 开发构建环境,集成 Phaser 3 与虚拟摇杆插件,实现地图漫游、景点触发、拍立得弹窗、进度存档等核心功能,添加占位资源与批量生成脚本,配置项目元数据与忽略规则。
16 KiB
16 KiB
西安旅行记忆互动地图游戏 · 技术规格说明(细化版)
本文档是
docs/overview.md(原始需求)经过需求澄清与设计确认后的细化规格,作为后续实现的唯一依据。 日期:2026-10-16 · 状态:已与需求方逐节确认
1. 项目概述
一款 2D 俯视角互动地图游戏:玩家控制 Q 版小人在手绘手账风的西安地图上漫游,走进景点触发区即弹出"拍立得照片"回忆卡。集齐全部景点后解锁"旅行完成"纪念页。
- 一句话玩法:走地图 → 触发景点 → 翻拍立得照片 → 集齐进度 → 纪念页
- 目标平台:手机/PC 浏览器(Web 优先交付);Android APK 为第二阶段
- 分享形态:URL 链接即玩;后期 APK 微信分发
- 情感定位:一本"可以走进去的旅行手账",暖色调、手写感、记忆感
2. 已确认决策记录
| # | 决策项 | 结论 | 备注 |
|---|---|---|---|
| 1 | 照片资源 | 暂未准备 | 占位图先行开发,后期按《照片替换指南》换真实照片 |
| 2 | 地图范围 | 主城区 + 经典景点 | 约 8–12 个景点,地图 3000×2100 viewBox |
| 3 | 玩法深度 | 收集进度 + 方向引导 | 屏幕边缘箭头指向最近未解锁景点 |
| 4 | 地图视觉 | 手绘旅行手账风 | 羊皮纸底色 + 棕褐线条 + 手绘图标 |
| 5 | 底图制作 | AI 素材 + SVG 路网混合 | 位置准确性归 SVG,氛围归 AI 生成素材 |
| 6 | 角色形象 | Q 版小人精灵图 | AI 生成 4 方向 × 3 帧精灵表,帧格 48×64 |
| 7 | 照片弹窗 | 拍立得照片风 | 白边相纸 + 手写描述 + 轻微旋转,多照片叠沓翻看 |
| 8 | 进度存档 | localStorage 持久化 | 可重置 |
| 9 | 开场页 | 手账风封面页 | "开始/继续旅行" + 进度概览 |
| 10 | 声音 | 暂不做 | 后期想加再加(预留 audio 目录) |
| 11 | 交付顺序 | Web 优先,APK 后置 | M1–M5 为 Web,M6 为 Capacitor 打包 |
| 12 | 架构 | Phaser 地图层 + DOM UI 层混合 | 取代原 overview.md 的纯 Phaser 场景方案 |
3. 架构设计
3.1 分层
index.html
├── #game → Phaser Canvas 层(下层):游戏世界
├── #ui → DOM + CSS UI 层(上层,pointer-events 按需开合)
└── 横切模块:event-bus.js(事件桥)、game-state.js(存档)、data/spots.js(内容配置)
Phaser 层(仅两个场景,较原方案砍掉 PhotoScene):
PreloadScene:进度条 + 资源预加载,完成后自动进入 MapSceneMapScene:地图渲染、相机跟随、角色精灵与动画、键盘/摇杆输入、景点触发检测(Zone overlap)、方向引导箭头渲染
DOM UI 层(纯 DOM 组件,零 Phaser 依赖):
TitleOverlay:手账封面页(开始/继续旅行、进度概览、重新开始)PhotoModal:拍立得弹窗(照片叠沓、翻看、手写描述、关闭)Hud:收集进度徽章(如5/12)+ 已解锁数量CompleteOverlay:100% 纪念页(照片九宫格 + 旅行统计 + 继续漫游)dom-layer.js:层管理器,负责显示/隐藏/互斥/覆盖
3.2 事件桥(EventBus)
自写轻量事件模块(约 20 行,on/once/off/emit)。跨层事件协议:
| 事件名 | 方向 | 载荷 | 语义 |
|---|---|---|---|
poi:reached |
Phaser → UI | spot 配置对象 |
玩家进入景点触发区,请求弹窗 |
ui:closed |
UI → Phaser | 无 | 弹窗/封面关闭,恢复游戏输入 |
state:changed |
game-state → 两层 | 进度快照 | 解锁变化,刷新 HUD 与地图标记 |
game:complete |
game-state → UI | 进度快照 | 全部解锁,弹出纪念页 |
3.3 输入互斥(弹窗打开时)
双保险:
- DOM 层全屏遮罩拦截所有触摸/点击(
pointer-events: auto) game.input.enabled = false禁用 Phaser 输入(摇杆同时失效)
关闭弹窗后反向恢复。弹窗期间 MapScene 不 pause(scene.pause 也不再需要,简化状态机)。
3.4 数据流
spots.js(静态配置) ──┐
├→ MapScene(渲染地图/标记/触发) ──poi:reached→ PhotoModal(展示照片)
game-state.js ──state:changed──┴→ Hud(进度徽章) / PoiMarker(亮色标记) / TitleOverlay(进度概览)
↑ localStorage 读写
4. 游戏流程(状态流转)
打开页面
→ PreloadScene(加载进度条)
→ TitleOverlay(手账封面)
├─ 首次: "开始旅行"
└─ 有存档: "继续旅行 (5/12)" + "重新开始"
→ MapScene 漫游(WASD/方向键/摇杆)
├─ 进入未解锁景点触发区 → PhotoModal(拍立得, 首张)
│ → 关闭 → 解锁标记盖章动效 + HUD +1 → state:changed → 存档
├─ 进入已解锁景点触发区 → PhotoModal(重看, 不重复计数)
└─ 屏幕边缘箭头始终指向最近未解锁景点
→ 全部解锁 → CompleteOverlay(纪念页: 照片九宫格+统计)
└─ "继续漫游" → 回到地图(引导箭头消失, 标记全亮)
触发防抖规则:关闭弹窗后,玩家必须离开该景点触发区,才能再次触发该景点(防止原地连弹)。实现:记录 activePoi 与 cooldown 标记,overlap 回调中判断。
5. 数据模型
5.1 景点配置 src/data/spots.js(唯一内容源)
/**
* 景点清单:坐标为 SVG 底图 viewBox 坐标系单位。
* 替换真实照片时:同名覆盖文件 + 更新 photos[].caption / story / date。
*/
export const SPOTS = [
{
id: 'dayanta', // 唯一 ID,存档键使用
name: '大雁塔',
x: 1820, y: 1450, // 地图像素坐标(SVG viewBox 单位)
radius: 70, // 触发半径
icon: '🗼', // 地图标记图标
photos: [ // 1–5 张,弹窗叠沓翻看
{ src: 'assets/photos/dayanta_01.jpg', caption: '夕阳下的大雁塔' },
],
story: '国庆第 3 天,我们在塔下等灯光亮起。',
date: '10月3日', // 拍立得右下角手写日期
},
// ... 共 8–12 个景点;M1 内置 10 个景点占位数据完整跑通,后期随真实照片增删
];
5.2 存档结构 src/game-state.js
- localStorage key:
xian-travel-progress - 结构:
{ unlocked: string[], updatedAt: number } - API:
loadProgress()/unlock(spotId)/resetProgress()/isUnlocked(spotId)/getSnapshot() - 隐私模式降级:localStorage 不可用时回退内存 Map,会话内有效,封面页不显示"继续"
5.3 占位资源规范
| 资源 | 占位形态 | 替换路径 |
|---|---|---|
| 照片 | 4:3 渐变 SVG 色块(每个景点不同色相,含景点名水印) | 同名覆盖 + 更新 caption |
| 底图 | 简化 SVG 路网(城墙环/主干道/水系/POI 点) | 路网不动,叠加 AI 纹理层 |
| 角色 | 程序绘制方向指示标记 | AI 精灵表替换加载 key |
| 纪念页九宫格 | 复用照片占位色块 | 随照片自动生效 |
6. 地图与美术制作管线
- 参照层:高德/OSM 截图仅作绘制参照,不进仓库(规避版权)
- SVG 路网层(一次性人工绘制):明城墙环 + 护城河、二环、主干道网、渭河水系、POI 圆点;viewBox
0 0 3000 2100;景点坐标在 SVG 内直接读取 - AI 素材层(文生图 API 生成):纸张纹理底图、罗盘装饰、地图边框、区域淡彩;作为 Phaser 叠加 image 层
- 角色精灵表:AI 生成 Q 版小人,4 方向(上下左右)× 3 帧(idle/迈步),帧格 48×64,导出单张 spritesheet PNG
- POI 标记:图标 + 状态样式——未解锁为灰调轮廓,已解锁为亮色 + 解锁瞬间"盖章"缩放动效(手账贴纸语义)
- 视觉基调:羊皮纸
#f5ecd7底、棕褐#8a6a42线条、暖橙#e67e22强调色;中文字体优先Kaiti SC/手写体回退
7. 玩法系统细节
7.1 触发
- Phaser
physics.add.overlap(player, poiZone)检测 - 弹窗关闭后设
cooldown[spotId] = true,玩家离开该 Zone 的overlap消失事件清除 - 已解锁景点重复进入仍可重看(不重复计数)
7.2 进度
- 解锁即
game-state.unlock(id)→state:changed→ HUD 徽章n/total+ 标记亮色 - 达到
total时额外广播game:complete→ CompleteOverlay
7.3 方向引导
- 每帧计算:玩家位置 → 最近未解锁景点向量
- 渲染屏幕边缘箭头(贴边固定距离 + 轻微浮动动画 + 箭头根部显示目标景点名标签,不显示数字距离——viewBox 单位无真实米义)
- 触发条件补充:目标已在相机视野内时箭头淡出(避免遮住玩家要看的内容)
- 全解锁后移除
7.4 重置
- 封面页"重新开始" → 原生
confirm确认 →resetProgress()→ 重载页面
7.5 纪念页
- 手账风全屏页:已解锁景点照片九宫格(点击可放大重看)+ 景点数/旅行天数统计(天数口径 = spots 配置中出现的不重复
date数量)+ "继续漫游"按钮
8. UI 设计规格(DOM 层)
8.1 拍立得弹窗 PhotoModal
- 结构:全屏半透明遮罩 + 居中拍立得卡(白边相纸:照片区 4:3,下边加宽放手写描述)
- 旋转:卡片
-2.5°随机 ±2°,投影0 6px 18px rgba(0,0,0,.35) - 多照片:向右错位叠沓(每张偏移 12px 旋转 +2°),点击当前卡片翻至下一张(CSS transform 过渡 300ms)
- 描述:手写体(
Kaiti SC回退),右下角日期角标 - 关闭:遮罩点击 / ESC / 空格(PC);关闭按钮常显(移动端单手区)
- 照片缺失降级:显示"照片待补"占位卡(兼作换图核对清单)
8.2 封面页 TitleOverlay
- 手账封面质感:纸张底纹 + 标题"西安旅行记忆" + 装饰(罗盘/胶带贴纸)
- 首次进入:单一"开始旅行"大按钮
- 有存档:显示"继续旅行 (n/12)" + 次级"重新开始"
- 进入游戏时整页翻页/淡出过渡(600ms)
8.3 HUD
- 右上角进度徽章:
🗺️ 5/12,解锁时数字跳动动效 - 移动端安全区内渲染
8.4 竖屏提示
- 检测
window.innerHeight > window.innerWidth且为触摸设备时全屏显示"请旋转手机"插画页;回到横屏自动消失
9. 移动端适配
| 项 | 方案 |
|---|---|
| 缩放 | Phaser.Scale.FIT + autoCenter: CENTER_BOTH,设计分辨率 1600×900 |
| 屏幕方向 | Web 竖屏显示旋转提示页;M6 Capacitor 阶段锁横屏 |
| 摇杆 | rexVirtualJoystick 左下角,半径 60,仅触摸设备显示,安全区偏移 |
| 触控 | CSS touch-action: none、user-select: none、viewport-fit=cover + env(safe-area-inset-*) |
| 字号 | DOM UI 层 clamp() 响应式字号 |
| 调试 | vite --host 局域网真机调试 |
10. 项目结构
xian-travel-game/
├── public/assets/
│ ├── maps/ # map-base.svg(路网) map-texture.png(AI 纸纹) 等分层素材
│ ├── photos/ # 占位 SVG → 真实照片(景点id_序号.jpg)
│ ├── sprites/ # player.png(Q版精灵表)
│ └── audio/ # 预留(本期不实现)
├── src/
│ ├── main.js # Phaser 配置与启动(FIT/1600×900/插件注册)
│ ├── event-bus.js # 自写事件桥 on/once/off/emit
│ ├── game-state.js # localStorage 存档模块
│ ├── data/spots.js # 景点配置(唯一内容源)
│ ├── scenes/
│ │ ├── PreloadScene.js # 进度条 + 资源加载 + 失败降级
│ │ └── MapScene.js # 地图/角色/摇杆/触发/引导箭头/标记状态
│ ├── objects/
│ │ ├── Player.js # 角色移动与动画(键盘+摇杆,斜向归一化)
│ │ └── PoiMarker.js # 景点标记(图标/状态样式/盖章动效)
│ └── ui/ # DOM UI 层
│ ├── dom-layer.js # 层管理(挂载/互斥/过渡)
│ ├── TitleOverlay.js
│ ├── PhotoModal.js
│ ├── Hud.js
│ ├── CompleteOverlay.js
│ └── rotate-hint.js # 竖屏提示
├── index.html # #game + #ui 容器 + 移动端 meta
├── vite.config.js # base './' / host:true / assetsInlineLimit:0
└── package.json # phaser + phaser3-rex-plugins(Capacitor 依赖 M6 再装)
编码规范:每个文件顶部文件头注释(用途/职责/主要流程);函数级 Google 风格注释(中文);ESM 模块。
11. 错误处理
| 场景 | 处理 |
|---|---|
| 资源加载失败 | PreloadScene load.on('loaderror') 记录并以内联占位图降级,不中断 |
| localStorage 不可用 | game-state 内存降级;封面页隐藏"继续旅行" |
| 照片文件缺失 | PhotoModal"照片待补"占位卡 |
| resize / orientationchange | scale.refresh() + DOM 层重排 |
| 触发风暴 | 冷却机制(见 7.1) |
12. 测试与验收标准
| # | 验收项 | 通过标准 |
|---|---|---|
| 1 | PC 移动 | WASD/方向键八方向,斜向速度归一化不超速 |
| 2 | 触发弹窗 | 进入景点弹拍立得;ESC/空格/点击遮罩/关闭按钮可关;无原地连弹 |
| 3 | 移动端 | 摇杆可移动;弹窗可关;竖屏出现旋转提示,回横屏恢复 |
| 4 | 进度持久化 | 刷新/重开浏览器进度保留;重新开始可用并清档 |
| 5 | 引导 | 边缘箭头指向最近未解锁景点;目标进入视野时箭头淡出;全解锁后消失 |
| 6 | 纪念页 | n/n 全解锁出现;九宫格可点开放大;继续漫游可回地图 |
| 7 | 占位降级 | 断照片/断资源不白屏,显示占位卡 |
| 8 | 性能 | 首屏 < 3s(WiFi);底图 < 1.5MB;单照片 < 300KB;60fps 漫游(中端手机) |
验收方式:M1/M2 结束各跑一遍 PC + 真机清单;npm run build 后本地 preview 冒烟。
13. 交付路线(里程碑)
| 阶段 | 内容 | 出口标准 |
|---|---|---|
| M1 可玩核心 | 项目脚手架 + 占位地图/角色 + 移动 + 触发 + 拍立得弹窗 | 验收 1/2/7 |
| M2 完整体验 | 封面页 + 存档/进度 + 引导箭头 + 纪念页 + 摇杆 + 竖屏提示 | 验收 3/4/5/6 |
| M3 手账美化 | AI 纸纹/罗盘素材 + Q 版精灵表 + 盖章动效 + 封面精化 | 视觉走查 |
| M4 真实内容 | 照片替换(附《照片替换指南》)+ 坐标微调 | 验收 7/8 |
| M5 Web 发布 | build + Vercel/GitHub Pages(base: './') |
手机打开链接可玩 |
| M6 APK(后置) | 安装 Capacitor 依赖 + init + 打包(锁横屏) | 真机安装可玩 |
M1–M5 为本期承诺范围;M6 在 Web 版验收后单独启动。
14. 照片替换指南(后期运营,随 M4 交付)
- 命名规范:
public/assets/photos/<景点id>_<两位序号>.jpg(如dayanta_01.jpg) - 压缩规范:宽度 ≤ 1200px,单张 ≤ 300KB,优先 WebP(文件名可保留 .jpg 后缀或统一改并同步 spots.js)
- 配置更新:在
spots.js对应景点的photos[]增删条目、写 caption;story/date按手账文案填写 - 自检:跑一遍游戏,缺失照片会显示"照片待补"占位卡,逐个补齐即可
15. 与原 overview.md 的差异说明
| 原方案 | 细化后 | 原因 |
|---|---|---|
| PhotoScene 独立场景 + scene.pause/launch | DOM 层 PhotoModal + 输入禁用 | 拍立得/中文排版/动画是 CSS 强项,Phaser UI 弱项 |
| 三场景 | 两场景(Preload/Map) | 弹窗职责外移,简化状态机 |
| 图片地图 + 手动取坐标 | SVG 路网 + viewBox 坐标 | 位置准确、可分层叠加 AI 素材、坐标在源文件中直接可读 |
| 1280×720 | 1600×900 | 地图比例 3000×2100 匹配 10:7,横屏视野更舒展 |
| 无开场/进度/引导/纪念页 | 封面页 + 进度徽章 + 边缘箭头 + 纪念页 | 需求澄清新增的游戏化决策 |
| 一次性全部依赖安装 | Capacitor 依赖 M6 再装 | Web 优先交付,减少前期装机量 |