refactor: 高内聚低耦合 - 配置集中化与 i18n 统一
结构优化:
- 新增 mobile-game/config.js:集中模块静态配置(资源路径、排行榜端点、
hero meta、子标签页定义),消除散落在各组件的硬编码
- 新增 composables/useLocale.js:共享语言辅助(t/isZh/pick),
消除 6 个组件各自重复定义的 L() helper
- 文案全部迁入 i18n locale(mobileGame 命名空间),消除模板内联 L('中','英')
- 数据文件 versions.js 去除 {zh,en} 对象,文案改由 i18n 键(tracks/versions)取
- 各组件改为读 config + t(),不再直接 useI18n,降低与 i18n 实现的耦合
headless 验证:中文(hero/meta/team/timeline/3D置顶) + 英文切换 + 切回中文 全通过
This commit is contained in:
15
src/composables/useLocale.js
Normal file
15
src/composables/useLocale.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
// 共享的语言辅助 composable,消除各组件重复定义 L()
|
||||||
|
// 用法:
|
||||||
|
// const { t, locale, isZh, pick } = useLocale()
|
||||||
|
// t('some.key') // 走 i18n(推荐,文案在 locale 文件)
|
||||||
|
// isZh.value // 判断当前是否中文
|
||||||
|
// pick({ zh, en }) // 从 {zh,en} 对象取当前语言值(用于数据文件)
|
||||||
|
export function useLocale() {
|
||||||
|
const { t, locale } = useI18n({ useScope: 'global' })
|
||||||
|
const isZh = computed(() => locale.value === 'zh-CN')
|
||||||
|
const pick = (obj) => (isZh.value ? obj.zh : obj.en)
|
||||||
|
return { t, locale, isZh, pick }
|
||||||
|
}
|
||||||
@@ -2,19 +2,96 @@
|
|||||||
// 只看中文的访客不会下载此文件。
|
// 只看中文的访客不会下载此文件。
|
||||||
export default {
|
export default {
|
||||||
app: {
|
app: {
|
||||||
title: 'Zikai',
|
title: 'Zikai'
|
||||||
nav: {
|
|
||||||
projects: 'Projects',
|
|
||||||
language: 'Language'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
loading: 'Loading…',
|
loading: 'Loading…',
|
||||||
comingSoon: 'Coming soon',
|
notAvailable: 'Not available'
|
||||||
notAvailable: 'Not available',
|
|
||||||
backToTop: 'Back to top'
|
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
mobileGame: 'Mobile Game Project'
|
mobileGame: 'Mobile Game Project'
|
||||||
|
},
|
||||||
|
mobileGame: {
|
||||||
|
hero: {
|
||||||
|
title: 'Mobile Game Project',
|
||||||
|
desc: 'A cross-semester Unity mobile-game capstone: 448 built the game (WebGL + Android APK), 449 built the showcase site and player leaderboard. Iterated across versions, from a character controller to a gyroscope-controlled 3D rolling-ball game, with ML-Agents reinforcement-learning experiments.'
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
course: 'Miami University · Capstone 448 / 449',
|
||||||
|
stack: 'Stack',
|
||||||
|
stackValue: 'Unity · C# · WebGL · Android · PHP',
|
||||||
|
period: 'Period',
|
||||||
|
periodValue: '2022'
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
label: 'Overview',
|
||||||
|
title: 'From character controller to gyro ball',
|
||||||
|
p1: 'The project spans courses 448 and 449. 448 built the game with Unity, producing browser-playable WebGL builds and installable Android APKs; 449 built the showcase site and a PHP+MySQL player leaderboard. The game evolved along several parallel lines, from 2D character controllers and platformers, ultimately shifting to a gyroscope-controlled 3D rolling ball, with ML-Agents reinforcement-learning experiments.',
|
||||||
|
p2: 'Gameplay iterated across versions: early builds were keyboard-controlled 2D character controllers with health/power stats; the final version became a gyroscope-tilt 3D rolling-ball game with falling snowflake bricks and bouncing yellow bricks.'
|
||||||
|
},
|
||||||
|
subtabs: {
|
||||||
|
team: 'Team · Leaderboard',
|
||||||
|
iterations: 'Iterations',
|
||||||
|
experiment: 'Experiment'
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
label: 'Team',
|
||||||
|
title: 'Members'
|
||||||
|
},
|
||||||
|
leaderboard: {
|
||||||
|
label: 'Leaderboard',
|
||||||
|
title: 'Top 30 Scores',
|
||||||
|
colRank: '#',
|
||||||
|
colName: 'Name',
|
||||||
|
colScore: 'Score',
|
||||||
|
noScores: 'No scores yet',
|
||||||
|
error: 'Leaderboard service unavailable'
|
||||||
|
},
|
||||||
|
iterations: {
|
||||||
|
label: 'Version History',
|
||||||
|
title: 'Iteration Record',
|
||||||
|
colControls: 'Controls',
|
||||||
|
colNote: 'Note',
|
||||||
|
colUnity: 'Unity',
|
||||||
|
playWebgl: 'Play WebGL',
|
||||||
|
downloadApk: 'Download APK',
|
||||||
|
pivotText: 'Earlier parallel lines: 2D'
|
||||||
|
},
|
||||||
|
experiment: {
|
||||||
|
label: 'Experiment',
|
||||||
|
title: 'ML-Agents Reinforcement Learning Demo',
|
||||||
|
caption: 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.'
|
||||||
|
},
|
||||||
|
tracks: {
|
||||||
|
'roll-a-ball': {
|
||||||
|
name: '3D Rolling Ball (Final Form)',
|
||||||
|
desc: 'The final form as the project shifted from 2D to 3D. Tilt the phone to roll the ball; snowflake bricks fall, yellow bricks bounce.'
|
||||||
|
},
|
||||||
|
'zikai-ui': {
|
||||||
|
name: 'zikai-ui Character Controller',
|
||||||
|
desc: 'The most iterated line. A 2D character with health/power stats; controls evolved from mouse, QWER/ASDF keys, finally to WASD.'
|
||||||
|
},
|
||||||
|
'main': {
|
||||||
|
name: 'main Character Controller',
|
||||||
|
desc: '2D character controller; upgraded Unity 2020 -> 2021, added on-screen buttons and jump.'
|
||||||
|
},
|
||||||
|
'doby': {
|
||||||
|
name: 'DobyAdventure',
|
||||||
|
desc: '2D side-scrolling platformer: collect coins, score, health, touch joystick.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
'zikai-0.0.0': { control: 'Left mouse: operate, Right mouse: track', note: 'Early mouse-controlled prototype' },
|
||||||
|
'zikai-0.0.1': { control: 'Health Q/W/E/R, Power A/S/D/F', note: 'Introduced health/power stat system' },
|
||||||
|
'zikai-0.0.2': { control: 'Same as 0.0.1', note: 'Build artifact renaming' },
|
||||||
|
'zikai-0.0.3': { control: 'WASD move, J jump, K fly (testing)', note: 'Switched to WASD, latest of this line' },
|
||||||
|
'main-0.0.0': { control: 'Character controller', note: 'Baseline' },
|
||||||
|
'main-0.0.1': { control: 'WASD move, K jump', note: 'Upgraded Unity, added on-screen buttons' },
|
||||||
|
'hongXiang-0.0.0': { control: 'A/D move, jump, collect coins', note: '2D platformer' },
|
||||||
|
'ball': { control: 'Tilt phone to move, tilt up/tap to jump', note: 'Final version, project shifted to 3D rolling ball' }
|
||||||
|
},
|
||||||
|
screenshots: {
|
||||||
|
caption: 'Screenshot {n}'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,98 @@
|
|||||||
// 中文语言包(默认语言,同步注入主 chunk,随首屏下发)
|
// 中文语言包(默认语言,同步注入主 chunk,随首屏下发)
|
||||||
export default {
|
export default {
|
||||||
app: {
|
app: {
|
||||||
title: 'Zikai',
|
title: 'Zikai'
|
||||||
nav: {
|
|
||||||
projects: '项目',
|
|
||||||
language: '语言'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
common: {
|
common: {
|
||||||
loading: '加载中…',
|
loading: '加载中…',
|
||||||
comingSoon: '即将上线',
|
notAvailable: '暂不可用'
|
||||||
notAvailable: '暂不可用',
|
|
||||||
backToTop: '回到顶部'
|
|
||||||
},
|
},
|
||||||
tabs: {
|
tabs: {
|
||||||
mobileGame: '移动游戏项目'
|
mobileGame: '移动游戏项目'
|
||||||
|
},
|
||||||
|
mobileGame: {
|
||||||
|
hero: {
|
||||||
|
title: '移动游戏项目',
|
||||||
|
desc: '一个跨学期的 Unity 移动游戏毕设项目:448 开发游戏本体(WebGL + Android APK),449 搭建展示网站与玩家排行榜。经历多版本迭代,从角色控制器演进到陀螺仪操控的 3D 滚球游戏,并尝试用 ML-Agents 做强化学习实验。'
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
course: '迈阿密大学 · 毕设 448 / 449',
|
||||||
|
stack: '技术栈',
|
||||||
|
stackValue: 'Unity · C# · WebGL · Android · PHP',
|
||||||
|
period: '时间',
|
||||||
|
periodValue: '2022'
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
label: '项目概览',
|
||||||
|
title: '从角色控制器到陀螺仪滚球',
|
||||||
|
p1: '项目跨越 448 与 449 两门课程。448 用 Unity 开发游戏本体,产出可浏览器试玩的 WebGL 构建与可安装的 Android APK;449 搭建展示网站与基于 PHP+MySQL 的玩家分数排行榜。游戏经历多条并行演进线,从 2D 角色控制器与平台跳跃,最终转向手机陀螺仪操控的 3D 滚球,并尝试用 ML-Agents 做强化学习实验。',
|
||||||
|
p2: '玩法经过多版本迭代:早期是键盘控制生命/能量的 2D 角色控制器,最终版改为手机陀螺仪倾斜操控的 3D 滚球,含雪花砖掉落、黄色砖弹跳等机制。'
|
||||||
|
},
|
||||||
|
subtabs: {
|
||||||
|
team: '团队 · 排行榜',
|
||||||
|
iterations: '迭代记录',
|
||||||
|
experiment: '实验'
|
||||||
|
},
|
||||||
|
team: {
|
||||||
|
label: '团队',
|
||||||
|
title: '成员'
|
||||||
|
},
|
||||||
|
leaderboard: {
|
||||||
|
label: '排行榜',
|
||||||
|
title: '玩家分数 Top 30',
|
||||||
|
colRank: '#',
|
||||||
|
colName: '玩家',
|
||||||
|
colScore: '分数',
|
||||||
|
noScores: '暂无分数记录',
|
||||||
|
error: '排行榜服务暂不可用'
|
||||||
|
},
|
||||||
|
iterations: {
|
||||||
|
label: '版本演进',
|
||||||
|
title: '迭代记录',
|
||||||
|
colControls: '操作',
|
||||||
|
colNote: '说明',
|
||||||
|
colUnity: 'Unity',
|
||||||
|
playWebgl: 'WebGL 试玩',
|
||||||
|
downloadApk: '下载 APK',
|
||||||
|
pivotText: '早期并行演进:2D 线'
|
||||||
|
},
|
||||||
|
experiment: {
|
||||||
|
label: '实验',
|
||||||
|
title: 'ML-Agents 强化学习演示',
|
||||||
|
caption: '胶囊体经 ML-Agents 训练,学习保球不掉与越障。'
|
||||||
|
},
|
||||||
|
// 版本演进线与版本数据中的可翻译文案
|
||||||
|
tracks: {
|
||||||
|
'roll-a-ball': {
|
||||||
|
name: '3D 滚球(最终形态)',
|
||||||
|
desc: '项目从 2D 转向 3D 的最终形态。手机陀螺仪倾斜操控滚球,含雪花砖掉落、黄色砖弹跳等机制。'
|
||||||
|
},
|
||||||
|
'zikai-ui': {
|
||||||
|
name: 'zikai-ui 角色控制器',
|
||||||
|
desc: '迭代次数最多的线。2D 角色带生命/能量属性,操作方式从鼠标、QWER/ASDF 键盘最终改为 WASD。'
|
||||||
|
},
|
||||||
|
'main': {
|
||||||
|
name: 'main 角色控制器',
|
||||||
|
desc: '2D 角色控制器,从 Unity 2020 升级到 2021,新增屏幕按键与跳跃。'
|
||||||
|
},
|
||||||
|
'doby': {
|
||||||
|
name: 'DobyAdventure',
|
||||||
|
desc: '2D 横版平台跳跃,吃金币、计分、血量、触屏摇杆。'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 各版本的操作与说明文案(key = 版本 id)
|
||||||
|
versions: {
|
||||||
|
'zikai-0.0.0': { control: '鼠标左键操作、右键追踪', note: '早期鼠标控制原型' },
|
||||||
|
'zikai-0.0.1': { control: '生命 Q/W/E/R、能量 A/S/D/F', note: '引入生命/能量属性系统' },
|
||||||
|
'zikai-0.0.2': { control: '同 0.0.1', note: '构建产物重命名整理' },
|
||||||
|
'zikai-0.0.3': { control: 'WASD 移动、J 跳跃、K 飞行(测试)', note: '改为 WASD 操作,本线最新版' },
|
||||||
|
'main-0.0.0': { control: '角色控制器', note: '基线版本' },
|
||||||
|
'main-0.0.1': { control: 'WASD 移动、K 跳跃', note: '升级 Unity,新增屏幕按键' },
|
||||||
|
'hongXiang-0.0.0': { control: 'A/D 移动、跳跃、吃金币', note: '2D 平台跳跃' },
|
||||||
|
'ball': { control: '手机陀螺仪倾斜移动、上倾/点击跳跃', note: '最终版本,项目转向 3D 滚球' }
|
||||||
|
},
|
||||||
|
screenshots: {
|
||||||
|
caption: '项目截图 {n}'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineAsyncComponent } from 'vue'
|
import { ref, defineAsyncComponent } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import HeroSection from './components/HeroSection.vue'
|
import HeroSection from './components/HeroSection.vue'
|
||||||
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
import ScreenshotCarousel from './components/ScreenshotCarousel.vue'
|
||||||
import SubTabs from './components/SubTabs.vue'
|
import SubTabs from './components/SubTabs.vue'
|
||||||
|
import { useLocale } from '../../composables/useLocale.js'
|
||||||
|
import { subTabs, defaultSubTab } from './config.js'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
|
||||||
|
|
||||||
// 子标签页:默认显示团队+排行榜
|
// 子标签页配置转为 SubTabs 需要的 { id, labelKey } 形式
|
||||||
const subTabs = [
|
const tabs = subTabs
|
||||||
{ id: 'team', label: { zh: '团队 · 排行榜', en: 'Team · Leaderboard' } },
|
const activeTab = ref(defaultSubTab)
|
||||||
{ id: 'iterations', label: { zh: '迭代记录', en: 'Iterations' } },
|
|
||||||
{ id: 'experiment', label: { zh: '实验', en: 'Experiment' } }
|
|
||||||
]
|
|
||||||
const activeTab = ref('team')
|
|
||||||
|
|
||||||
// ★ 按需加载:三个子标签页用 defineAsyncComponent 动态 import,
|
// ★ 按需加载:三个子标签页用 defineAsyncComponent 动态 import,
|
||||||
// 被 Vite/Rollup 拆成独立 chunk,只有切到对应标签页时才下载与渲染。
|
// 被 Vite/Rollup 拆成独立 chunk,只有切到对应标签页时才下载与渲染。
|
||||||
@@ -34,21 +30,11 @@ const ExperimentTab = defineAsyncComponent(() => import('./components/Experiment
|
|||||||
<div class="block__grid">
|
<div class="block__grid">
|
||||||
<div class="block__text">
|
<div class="block__text">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<small>{{ L('项目概览', 'Overview') }}</small>
|
<small>{{ t('mobileGame.overview.label') }}</small>
|
||||||
{{ L('从角色控制器到陀螺仪滚球', 'From character controller to gyro ball') }}
|
{{ t('mobileGame.overview.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>{{ t('mobileGame.overview.p1') }}</p>
|
||||||
{{ L(
|
<p>{{ t('mobileGame.overview.p2') }}</p>
|
||||||
'项目跨越 448 与 449 两门课程。448 用 Unity 开发游戏本体,产出可浏览器试玩的 WebGL 构建与可安装的 Android APK;449 搭建展示网站与基于 PHP+MySQL 的玩家分数排行榜。游戏经历多条并行演进线,从 2D 角色控制器与平台跳跃,最终转向手机陀螺仪操控的 3D 滚球,并尝试用 ML-Agents 做强化学习实验。',
|
|
||||||
'The project spans courses 448 and 449. 448 built the game with Unity, producing browser-playable WebGL builds and installable Android APKs; 449 built the showcase site and a PHP+MySQL player leaderboard. The game evolved along several parallel lines, from 2D character controllers and platformers, ultimately shifting to a gyroscope-controlled 3D rolling ball, with ML-Agents reinforcement-learning experiments.'
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{ L(
|
|
||||||
'玩法经过多版本迭代:早期是键盘控制生命/能量的 2D 角色控制器,最终版改为手机陀螺仪倾斜操控的 3D 滚球,含雪花砖掉落、黄色砖弹跳等机制。',
|
|
||||||
'Gameplay iterated across versions: early builds were keyboard-controlled 2D character controllers with health/power stats; the final version became a gyroscope-tilt 3D rolling-ball game with falling snowflake bricks and bouncing yellow bricks.'
|
|
||||||
) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="block__media">
|
<div class="block__media">
|
||||||
<ScreenshotCarousel />
|
<ScreenshotCarousel />
|
||||||
@@ -58,14 +44,14 @@ const ExperimentTab = defineAsyncComponent(() => import('./components/Experiment
|
|||||||
|
|
||||||
<!-- 子标签页区域:按需加载,v-if 确保未激活的不渲染 -->
|
<!-- 子标签页区域:按需加载,v-if 确保未激活的不渲染 -->
|
||||||
<section class="block">
|
<section class="block">
|
||||||
<SubTabs v-model="activeTab" :tabs="subTabs" />
|
<SubTabs v-model="activeTab" :tabs="tabs" />
|
||||||
|
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TeamLeaderboardTab v-if="activeTab === 'team'" />
|
<TeamLeaderboardTab v-if="activeTab === 'team'" />
|
||||||
<IterationsTab v-else-if="activeTab === 'iterations'" />
|
<IterationsTab v-else-if="activeTab === 'iterations'" />
|
||||||
<ExperimentTab v-else-if="activeTab === 'experiment'" />
|
<ExperimentTab v-else-if="activeTab === 'experiment'" />
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div class="tab-loading">{{ L('加载中…', 'Loading…') }}</div>
|
<div class="tab-loading">{{ t('common.loading') }}</div>
|
||||||
</template>
|
</template>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
|
import { assets } from '../config.js'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
|
||||||
// 站点根绝对路径,用绑定避免被 Vite 当作构建期静态资源解析
|
|
||||||
const videoPoster = '/448/2.jpg'
|
|
||||||
const videoSrc = '/448/ml/449ml.mp4'
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tab-experiment">
|
<div class="tab-experiment">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<small>{{ L('实验', 'Experiment') }}</small>
|
<small>{{ t('mobileGame.experiment.label') }}</small>
|
||||||
{{ L('ML-Agents 强化学习演示', 'ML-Agents Reinforcement Learning Demo') }}
|
{{ t('mobileGame.experiment.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="video-wrap">
|
<div class="video-wrap">
|
||||||
<video controls preload="none" :poster="videoPoster">
|
<video controls preload="none" :poster="assets.video.poster">
|
||||||
<source :src="videoSrc" type="video/mp4" />
|
<source :src="assets.video.src" type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
<p class="video-caption">
|
<p class="video-caption">{{ t('mobileGame.experiment.caption') }}</p>
|
||||||
{{ L('胶囊体经 ML-Agents 训练,学习保球不掉与越障。', 'Capsules trained via ML-Agents to keep the ball from falling and to navigate obstacles.') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,38 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Tag from '../../../components/ui/Tag.vue'
|
import Tag from '../../../components/ui/Tag.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
|
import { heroMeta } from '../config.js'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
|
|
||||||
const meta = {
|
|
||||||
course: { zh: '迈阿密大学 · 毕设 448 / 449', en: 'Miami University · Capstone 448 / 449' },
|
|
||||||
stack: { zh: 'Unity · C# · WebGL · Android · PHP', en: 'Unity · C# · WebGL · Android · PHP' },
|
|
||||||
period: '2022'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero__eyebrow">
|
<div class="hero__eyebrow">
|
||||||
<Tag>{{ locale === 'zh-CN' ? meta.course.zh : meta.course.en }}</Tag>
|
<Tag>{{ t(heroMeta.courseKey) }}</Tag>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="hero__title">
|
<h1 class="hero__title">{{ t('mobileGame.hero.title') }}</h1>
|
||||||
{{ $t('tabs.mobileGame') }}
|
<p class="hero__desc">{{ t('mobileGame.hero.desc') }}</p>
|
||||||
</h1>
|
|
||||||
<p class="hero__desc">
|
|
||||||
{{ locale === 'zh-CN'
|
|
||||||
? '一个跨学期的 Unity 移动游戏毕设项目:448 开发游戏本体(WebGL + Android APK),449 搭建展示网站与玩家排行榜。经历多版本迭代,从角色控制器演进到陀螺仪操控的 3D 滚球游戏,并尝试用 ML-Agents 做强化学习实验。'
|
|
||||||
: 'A cross-semester Unity mobile-game capstone: 448 built the game (WebGL + Android APK), 449 built the showcase site and player leaderboard. Iterated across versions, from a character controller to a gyroscope-controlled 3D rolling-ball game, with ML-Agents reinforcement-learning experiments.' }}
|
|
||||||
</p>
|
|
||||||
<div class="hero__meta">
|
<div class="hero__meta">
|
||||||
<div class="hero__meta-item">
|
<div v-for="item in heroMeta.items" :key="item.labelKey" class="hero__meta-item">
|
||||||
<span class="hero__meta-label">{{ locale === 'zh-CN' ? '技术栈' : 'Stack' }}</span>
|
<span class="hero__meta-label">{{ t(item.labelKey) }}</span>
|
||||||
<span>{{ locale === 'zh-CN' ? meta.stack.zh : meta.stack.en }}</span>
|
<span>{{ t(item.valueKey) }}</span>
|
||||||
</div>
|
|
||||||
<div class="hero__meta-item">
|
|
||||||
<span class="hero__meta-label">{{ locale === 'zh-CN' ? '时间' : 'Period' }}</span>
|
|
||||||
<span>{{ meta.period }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
import VersionTimeline from './VersionTimeline.vue'
|
import VersionTimeline from './VersionTimeline.vue'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tab-iterations">
|
<div class="tab-iterations">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<small>{{ L('版本演进', 'Version History') }}</small>
|
<small>{{ t('mobileGame.iterations.label') }}</small>
|
||||||
{{ L('迭代记录', 'Iteration Record') }}
|
{{ t('mobileGame.iterations.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<VersionTimeline />
|
<VersionTimeline />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
|
import { leaderboard } from '../config.js'
|
||||||
|
|
||||||
const { locale, t } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
|
|
||||||
const rows = ref([])
|
const rows = ref([])
|
||||||
const state = ref('loading') // loading | ok | error
|
const state = ref('loading') // loading | ok | error
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
|
|
||||||
// 复用 449 现有后端:POST /449/449rest.php {num} 取分页 JSON
|
// 复用 449 现有后端:POST endpoint {num} 取分页 JSON
|
||||||
async function load() {
|
async function load() {
|
||||||
state.value = 'loading'
|
state.value = 'loading'
|
||||||
try {
|
try {
|
||||||
const all = []
|
const all = []
|
||||||
// 拉取 3 页(每页 10 条,共 top30)
|
for (let page = 0; page < leaderboard.pages; page++) {
|
||||||
for (let page = 0; page < 3; page++) {
|
const res = await fetch(leaderboard.endpoint, {
|
||||||
const res = await fetch('/449/449rest.php', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: `num=${page * 10}`
|
body: `num=${page * leaderboard.perPage}`
|
||||||
})
|
})
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -30,12 +30,10 @@ async function load() {
|
|||||||
}
|
}
|
||||||
rows.value = all
|
rows.value = all
|
||||||
state.value = all.length ? 'ok' : 'error'
|
state.value = all.length ? 'ok' : 'error'
|
||||||
if (!all.length) errorMsg.value = locale.value === 'zh-CN' ? '暂无分数记录' : 'No scores yet'
|
if (!all.length) errorMsg.value = t('mobileGame.leaderboard.noScores')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state.value = 'error'
|
state.value = 'error'
|
||||||
errorMsg.value = locale.value === 'zh-CN'
|
errorMsg.value = t('mobileGame.leaderboard.error')
|
||||||
? '排行榜服务暂不可用'
|
|
||||||
: 'Leaderboard service unavailable'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +43,7 @@ onMounted(load)
|
|||||||
<template>
|
<template>
|
||||||
<div class="leaderboard">
|
<div class="leaderboard">
|
||||||
<div v-if="state === 'loading'" class="leaderboard__state">
|
<div v-if="state === 'loading'" class="leaderboard__state">
|
||||||
{{ $t('common.loading') }}
|
{{ t('common.loading') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="state === 'error'" class="leaderboard__state leaderboard__state--error">
|
<div v-else-if="state === 'error'" class="leaderboard__state leaderboard__state--error">
|
||||||
@@ -55,9 +53,9 @@ onMounted(load)
|
|||||||
<table v-else class="leaderboard__table">
|
<table v-else class="leaderboard__table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>{{ t('mobileGame.leaderboard.colRank') }}</th>
|
||||||
<th>{{ locale === 'zh-CN' ? '玩家' : 'Name' }}</th>
|
<th>{{ t('mobileGame.leaderboard.colName') }}</th>
|
||||||
<th>{{ locale === 'zh-CN' ? '分数' : 'Score' }}</th>
|
<th>{{ t('mobileGame.leaderboard.colScore') }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
import { screenshots } from '../data/versions.js'
|
import { assets } from '../config.js'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
|
const images = assets.screenshots
|
||||||
const current = ref(0)
|
const current = ref(0)
|
||||||
let timer = null
|
let timer = null
|
||||||
|
|
||||||
function go(i) {
|
function go(i) {
|
||||||
current.value = (i + screenshots.length) % screenshots.length
|
current.value = (i + images.length) % images.length
|
||||||
}
|
}
|
||||||
function next() {
|
function next() {
|
||||||
go(current.value + 1)
|
go(current.value + 1)
|
||||||
@@ -31,8 +32,8 @@ onUnmounted(pause)
|
|||||||
<div class="carousel" @mouseenter="pause" @mouseleave="resume">
|
<div class="carousel" @mouseenter="pause" @mouseleave="resume">
|
||||||
<div class="carousel__viewport">
|
<div class="carousel__viewport">
|
||||||
<div class="carousel__track" :style="{ transform: `translateX(-${current * 100}%)` }">
|
<div class="carousel__track" :style="{ transform: `translateX(-${current * 100}%)` }">
|
||||||
<figure v-for="(s, i) in screenshots" :key="i" class="carousel__slide">
|
<figure v-for="(src, i) in images" :key="i" class="carousel__slide">
|
||||||
<img :src="s.src" :alt="locale === 'zh-CN' ? s.caption.zh : s.caption.en" loading="lazy" />
|
<img :src="src" :alt="t('mobileGame.screenshots.caption', { n: i + 1 })" loading="lazy" />
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +43,7 @@ onUnmounted(pause)
|
|||||||
|
|
||||||
<div class="carousel__dots">
|
<div class="carousel__dots">
|
||||||
<button
|
<button
|
||||||
v-for="(s, i) in screenshots"
|
v-for="(src, i) in images"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="carousel__dot"
|
class="carousel__dot"
|
||||||
:class="{ 'is-active': i === current }"
|
:class="{ 'is-active': i === current }"
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
tabs: { type: Array, required: true }, // [{ id, label: {zh,en} }]
|
tabs: { type: Array, required: true }, // [{ id, labelKey }]
|
||||||
modelValue: { type: String, required: true }
|
modelValue: { type: String, required: true }
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="subtabs">
|
<div class="subtabs">
|
||||||
<button
|
<button
|
||||||
v-for="t in tabs"
|
v-for="tab in tabs"
|
||||||
:key="t.id"
|
:key="tab.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="subtabs__btn"
|
class="subtabs__btn"
|
||||||
:class="{ 'is-active': modelValue === t.id }"
|
:class="{ 'is-active': modelValue === tab.id }"
|
||||||
@click="emit('update:modelValue', t.id)"
|
@click="emit('update:modelValue', tab.id)"
|
||||||
>
|
>
|
||||||
{{ L(t.label) }}
|
{{ t(tab.labelKey) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
import TeamCard from './TeamCard.vue'
|
import TeamCard from './TeamCard.vue'
|
||||||
import Leaderboard from './Leaderboard.vue'
|
import Leaderboard from './Leaderboard.vue'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (zh, en) => (locale.value === 'zh-CN' ? zh : en)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tab-team block--two">
|
<div class="tab-team block--two">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<small>{{ L('团队', 'Team') }}</small>
|
<small>{{ t('mobileGame.team.label') }}</small>
|
||||||
{{ L('成员', 'Members') }}
|
{{ t('mobileGame.team.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<TeamCard />
|
<TeamCard />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
<small>{{ L('排行榜', 'Leaderboard') }}</small>
|
<small>{{ t('mobileGame.leaderboard.label') }}</small>
|
||||||
{{ L('玩家分数 Top 30', 'Top 30 Scores') }}
|
{{ t('mobileGame.leaderboard.title') }}
|
||||||
</h2>
|
</h2>
|
||||||
<Leaderboard />
|
<Leaderboard />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useLocale } from '../../../composables/useLocale.js'
|
||||||
import { versions, tracks } from '../data/versions.js'
|
import { versions, tracks } from '../data/versions.js'
|
||||||
|
|
||||||
const { locale } = useI18n({ useScope: 'global' })
|
const { t } = useLocale()
|
||||||
const L = (obj) => (locale.value === 'zh-CN' ? obj.zh : obj.en)
|
|
||||||
|
|
||||||
// 按 tracks 数组顺序分组(3D 最终形态在前);同一线内按 order 排序
|
// 按 tracks 数组顺序分组(3D 最终形态在前);同一线内按 order 排序
|
||||||
const grouped = computed(() => {
|
const grouped = computed(() => {
|
||||||
return tracks
|
return tracks
|
||||||
.map((t) => {
|
.map((tr) => {
|
||||||
const items = versions
|
const items = versions
|
||||||
.filter((v) => v.track === t.id)
|
.filter((v) => v.track === tr.id)
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
return { track: t, items }
|
return { track: tr, items }
|
||||||
})
|
})
|
||||||
.filter((g) => g.items.length > 0)
|
.filter((g) => g.items.length > 0)
|
||||||
})
|
})
|
||||||
@@ -23,6 +22,12 @@ const isFeatured = (track) => track.featured
|
|||||||
|
|
||||||
// 形态转变标记:出现在 3D 线(首位)之后、2D 线之前
|
// 形态转变标记:出现在 3D 线(首位)之后、2D 线之前
|
||||||
const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grouped.value[0].track)
|
const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grouped.value[0].track)
|
||||||
|
|
||||||
|
// i18n 取值辅助:track/version 的文案在 locale 的 mobileGame.tracks / mobileGame.versions 下
|
||||||
|
const trackName = (trackId) => t(`mobileGame.tracks.${trackId}.name`)
|
||||||
|
const trackDesc = (trackId) => t(`mobileGame.tracks.${trackId}.desc`)
|
||||||
|
const vControl = (versionId) => t(`mobileGame.versions.${versionId}.control`)
|
||||||
|
const vNote = (versionId) => t(`mobileGame.versions.${versionId}.note`)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -39,10 +44,10 @@ const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grou
|
|||||||
<!-- 线标题 -->
|
<!-- 线标题 -->
|
||||||
<div class="timeline__track-head">
|
<div class="timeline__track-head">
|
||||||
<span class="timeline__form" :class="{ 'is-3d': is3D(g.track) }">{{ g.track.form }}</span>
|
<span class="timeline__form" :class="{ 'is-3d': is3D(g.track) }">{{ g.track.form }}</span>
|
||||||
<span class="timeline__track-name">{{ L(g.track.name) }}</span>
|
<span class="timeline__track-name">{{ trackName(g.track.id) }}</span>
|
||||||
<span v-if="isFeatured(g.track)" class="timeline__star">★</span>
|
<span v-if="isFeatured(g.track)" class="timeline__star">★</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="timeline__track-desc">{{ L(g.track.desc) }}</p>
|
<p class="timeline__track-desc">{{ trackDesc(g.track.id) }}</p>
|
||||||
|
|
||||||
<!-- 该线下的版本 -->
|
<!-- 该线下的版本 -->
|
||||||
<div class="timeline__items">
|
<div class="timeline__items">
|
||||||
@@ -61,23 +66,23 @@ const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grou
|
|||||||
<span class="timeline__product">{{ v.product }}</span>
|
<span class="timeline__product">{{ v.product }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline__row">
|
<div class="timeline__row">
|
||||||
<span class="timeline__label">{{ locale === 'zh-CN' ? '操作' : 'Controls' }}</span>
|
<span class="timeline__label">{{ t('mobileGame.iterations.colControls') }}</span>
|
||||||
<span>{{ L(v.control) }}</span>
|
<span>{{ vControl(v.id) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline__row">
|
<div class="timeline__row">
|
||||||
<span class="timeline__label">{{ locale === 'zh-CN' ? '说明' : 'Note' }}</span>
|
<span class="timeline__label">{{ t('mobileGame.iterations.colNote') }}</span>
|
||||||
<span>{{ L(v.note) }}</span>
|
<span>{{ vNote(v.id) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline__row">
|
<div class="timeline__row">
|
||||||
<span class="timeline__label">Unity</span>
|
<span class="timeline__label">{{ t('mobileGame.iterations.colUnity') }}</span>
|
||||||
<span class="timeline__mono">{{ v.unity }}</span>
|
<span class="timeline__mono">{{ v.unity }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline__links">
|
<div class="timeline__links">
|
||||||
<a v-if="v.webgl" :href="v.webgl" target="_blank" rel="noopener">
|
<a v-if="v.webgl" :href="v.webgl" target="_blank" rel="noopener">
|
||||||
{{ locale === 'zh-CN' ? 'WebGL 试玩' : 'Play WebGL' }} ↗
|
{{ t('mobileGame.iterations.playWebgl') }} ↗
|
||||||
</a>
|
</a>
|
||||||
<a v-if="v.apk" :href="v.apk" target="_blank" rel="noopener">
|
<a v-if="v.apk" :href="v.apk" target="_blank" rel="noopener">
|
||||||
{{ locale === 'zh-CN' ? '下载 APK' : 'Download APK' }} ↗
|
{{ t('mobileGame.iterations.downloadApk') }} ↗
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,9 +92,7 @@ const showPivotAfter = (gi) => gi === 0 && grouped.value.length > 1 && is3D(grou
|
|||||||
<!-- 形态转变标记:3D 线之后、2D 线之前 -->
|
<!-- 形态转变标记:3D 线之后、2D 线之前 -->
|
||||||
<div v-if="showPivotAfter(gi)" class="timeline__pivot">
|
<div v-if="showPivotAfter(gi)" class="timeline__pivot">
|
||||||
<span class="timeline__pivot-line"></span>
|
<span class="timeline__pivot-line"></span>
|
||||||
<span class="timeline__pivot-text">
|
<span class="timeline__pivot-text">{{ t('mobileGame.iterations.pivotText') }}</span>
|
||||||
{{ locale === 'zh-CN' ? '早期并行演进:2D 线' : 'Earlier parallel lines: 2D' }}
|
|
||||||
</span>
|
|
||||||
<span class="timeline__pivot-line"></span>
|
<span class="timeline__pivot-line"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
46
src/modules/mobile-game/config.js
Normal file
46
src/modules/mobile-game/config.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// mobile-game 模块的静态配置(集中管理,避免散落在各组件)
|
||||||
|
// 文案走 i18n(见 locales 的 mobileGame 命名空间),此处只放纯配置与资源路径。
|
||||||
|
|
||||||
|
// 站点根绝对路径的资源引用
|
||||||
|
export const assets = {
|
||||||
|
// 截图轮播图源
|
||||||
|
screenshots: [
|
||||||
|
'/448/1.jpg',
|
||||||
|
'/448/2.jpg',
|
||||||
|
'/448/3.jpg'
|
||||||
|
],
|
||||||
|
// ML-Agents 演示视频
|
||||||
|
video: {
|
||||||
|
src: '/448/ml/449ml.mp4',
|
||||||
|
poster: '/448/2.jpg'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排行榜后端(复用 449 现有 PHP 接口)
|
||||||
|
export const leaderboard = {
|
||||||
|
endpoint: '/449/449rest.php',
|
||||||
|
// 拉取页数 x 每页条数 = 总条数
|
||||||
|
pages: 3,
|
||||||
|
perPage: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hero 区元数据(i18n 键,文案在 locales/mobileGame.meta 下)
|
||||||
|
export const heroMeta = {
|
||||||
|
// meta 项顺序:labelKey -> valueKey
|
||||||
|
items: [
|
||||||
|
{ labelKey: 'mobileGame.meta.stack', valueKey: 'mobileGame.meta.stackValue' },
|
||||||
|
{ labelKey: 'mobileGame.meta.period', valueKey: 'mobileGame.meta.periodValue' }
|
||||||
|
],
|
||||||
|
// 顶部 eyebrow tag
|
||||||
|
courseKey: 'mobileGame.meta.course'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子标签页定义(id 与 i18n 键)
|
||||||
|
export const subTabs = [
|
||||||
|
{ id: 'team', labelKey: 'mobileGame.subtabs.team' },
|
||||||
|
{ id: 'iterations', labelKey: 'mobileGame.subtabs.iterations' },
|
||||||
|
{ id: 'experiment', labelKey: 'mobileGame.subtabs.experiment' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 默认激活的子标签页
|
||||||
|
export const defaultSubTab = 'team'
|
||||||
@@ -1,162 +1,48 @@
|
|||||||
// 448 各版本数据(来自对构建产物与 index.html 的分析)
|
// 448 版本演进数据(纯结构化数据,文案走 i18n:mobileGame.tracks / mobileGame.versions)
|
||||||
//
|
//
|
||||||
// 项目是多条线并行演进的,且经历了「2D -> 3D 滚球」的形态转变:
|
// 项目是多条线并行演进的,且经历了「2D -> 3D 滚球」的形态转变:
|
||||||
// - zikai-ui 线(0.0.0 -> 0.0.3):2D 角色控制器,键盘控制生命/能量,最终改 WASD
|
// - roll-a-ball(3D,featured):项目转向 3D 的最终形态,置顶强调
|
||||||
// - main 线(0.0.0 -> 0.0.1):2D 角色控制器,升级 Unity,加屏幕按键
|
// - zikai-ui(2D):角色控制器,0.0.0 -> 0.0.3
|
||||||
// - doby 线(0.0.0):2D 平台跳跃(Hongxiang He)
|
// - main(2D):角色控制器,0.0.0 -> 0.0.1
|
||||||
// - roll-a-ball 线(0.0.2):3D 滚球,陀螺仪操控 —— 项目从 2D 转向 3D 的最终形态
|
// - doby(2D):2D 平台跳跃
|
||||||
//
|
//
|
||||||
// track 字段标识所属演进线;form 标识 2D/3D;order 用于时间线排序(按发布先后)。
|
// track 字段标识所属演进线;form 标识 2D/3D;order 用于线内排序。
|
||||||
|
// name/note/control 等文案通过 i18n 键 tracks[trackId] / versions[versionId] 取。
|
||||||
|
|
||||||
export const tracks = [
|
export const tracks = [
|
||||||
{
|
{
|
||||||
id: 'roll-a-ball',
|
id: 'roll-a-ball',
|
||||||
name: { zh: '3D 滚球(最终形态)', en: '3D Rolling Ball (Final Form)' },
|
|
||||||
form: '3D',
|
form: '3D',
|
||||||
featured: true,
|
featured: true
|
||||||
desc: {
|
|
||||||
zh: '项目从 2D 转向 3D 的最终形态。手机陀螺仪倾斜操控滚球,含雪花砖掉落、黄色砖弹跳等机制。',
|
|
||||||
en: 'The final form as the project shifted from 2D to 3D. Tilt the phone to roll the ball; snowflake bricks fall, yellow bricks bounce.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'zikai-ui',
|
id: 'zikai-ui',
|
||||||
name: { zh: 'zikai-ui 角色控制器', en: 'zikai-ui Character Controller' },
|
form: '2D'
|
||||||
form: '2D',
|
|
||||||
desc: {
|
|
||||||
zh: '迭代次数最多的线。2D 角色带生命/能量属性,操作方式从鼠标、QWER/ASDF 键盘最终改为 WASD。',
|
|
||||||
en: 'The most iterated line. A 2D character with health/power stats; controls evolved from mouse, QWER/ASDF keys, finally to WASD.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'main',
|
id: 'main',
|
||||||
name: { zh: 'main 角色控制器', en: 'main Character Controller' },
|
form: '2D'
|
||||||
form: '2D',
|
|
||||||
desc: {
|
|
||||||
zh: '2D 角色控制器,从 Unity 2020 升级到 2021,新增屏幕按键与跳跃。',
|
|
||||||
en: '2D character controller; upgraded Unity 2020 -> 2021, added on-screen buttons and jump.'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'doby',
|
id: 'doby',
|
||||||
name: { zh: 'DobyAdventure', en: 'DobyAdventure' },
|
form: '2D'
|
||||||
form: '2D',
|
|
||||||
desc: {
|
|
||||||
zh: '2D 横版平台跳跃,吃金币、计分、血量、触屏摇杆。',
|
|
||||||
en: '2D side-scrolling platformer: collect coins, score, health, touch joystick.'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const versions = [
|
export const versions = [
|
||||||
// zikai-ui 线
|
// zikai-ui 线
|
||||||
{
|
{ id: 'zikai-0.0.0', track: 'zikai-ui', version: '0.0.0', unity: '-', product: 'My project / 1.0.2', webgl: '/448/zikai-0.0.0/', apk: null, order: 1 },
|
||||||
id: 'zikai-0.0.0',
|
{ id: 'zikai-0.0.1', track: 'zikai-ui', version: '0.0.1', unity: '2020.3.30f1', product: 'zikai-ui / 1.0.2', webgl: '/448/zikai-0.0.1/', apk: '/448/zikai-0.0.1/zikai.apk', order: 2 },
|
||||||
track: 'zikai-ui',
|
{ id: 'zikai-0.0.2', track: 'zikai-ui', version: '0.0.2', unity: '2020.3.30f1', product: 'zikai-ui / 1.0.2', webgl: '/448/zikai-0.0.2/', apk: '/448/zikai-0.0.2/zikai.apk', order: 3 },
|
||||||
version: '0.0.0',
|
{ id: 'zikai-0.0.3', track: 'zikai-ui', version: '0.0.3', unity: '2020.3.30f1', product: 'zikai-ui / 1.0.2.5', webgl: '/448/zikai-0.0.3/', apk: '/448/zikai-0.0.3/zikai.apk', order: 4 },
|
||||||
unity: '-',
|
|
||||||
product: 'My project / 1.0.2',
|
|
||||||
webgl: '/448/zikai-0.0.0/',
|
|
||||||
apk: null,
|
|
||||||
control: { zh: '鼠标左键操作、右键追踪', en: 'Left mouse: operate, Right mouse: track' },
|
|
||||||
note: { zh: '早期鼠标控制原型', en: 'Early mouse-controlled prototype' },
|
|
||||||
order: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zikai-0.0.1',
|
|
||||||
track: 'zikai-ui',
|
|
||||||
version: '0.0.1',
|
|
||||||
unity: '2020.3.30f1',
|
|
||||||
product: 'zikai-ui / 1.0.2',
|
|
||||||
webgl: '/448/zikai-0.0.1/',
|
|
||||||
apk: '/448/zikai-0.0.1/zikai.apk',
|
|
||||||
control: { zh: '生命 Q/W/E/R、能量 A/S/D/F', en: 'Health Q/W/E/R, Power A/S/D/F' },
|
|
||||||
note: { zh: '引入生命/能量属性系统', en: 'Introduced health/power stat system' },
|
|
||||||
order: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zikai-0.0.2',
|
|
||||||
track: 'zikai-ui',
|
|
||||||
version: '0.0.2',
|
|
||||||
unity: '2020.3.30f1',
|
|
||||||
product: 'zikai-ui / 1.0.2',
|
|
||||||
webgl: '/448/zikai-0.0.2/',
|
|
||||||
apk: '/448/zikai-0.0.2/zikai.apk',
|
|
||||||
control: { zh: '同 0.0.1', en: 'Same as 0.0.1' },
|
|
||||||
note: { zh: '构建产物重命名整理', en: 'Build artifact renaming' },
|
|
||||||
order: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'zikai-0.0.3',
|
|
||||||
track: 'zikai-ui',
|
|
||||||
version: '0.0.3',
|
|
||||||
unity: '2020.3.30f1',
|
|
||||||
product: 'zikai-ui / 1.0.2.5',
|
|
||||||
webgl: '/448/zikai-0.0.3/',
|
|
||||||
apk: '/448/zikai-0.0.3/zikai.apk',
|
|
||||||
control: { zh: 'WASD 移动、J 跳跃、K 飞行(测试)', en: 'WASD move, J jump, K fly (testing)' },
|
|
||||||
note: { zh: '改为 WASD 操作,本线最新版', en: 'Switched to WASD, latest of this line' },
|
|
||||||
order: 4
|
|
||||||
},
|
|
||||||
|
|
||||||
// main 线
|
// main 线
|
||||||
{
|
{ id: 'main-0.0.0', track: 'main', version: '0.0.0', unity: '2020.3.30f1', product: 'zikai-main / 1.0.2.6', webgl: '/448/main-0.0.0/', apk: '/448/main-0.0.0.apk', order: 2 },
|
||||||
id: 'main-0.0.0',
|
{ id: 'main-0.0.1', track: 'main', version: '0.0.1', unity: '2021.3.12f1', product: 'beta / 1.0.2', webgl: '/448/main-0.0.1/', apk: '/448/main-0.0.1/zikai.apk', order: 5 },
|
||||||
track: 'main',
|
|
||||||
version: '0.0.0',
|
|
||||||
unity: '2020.3.30f1',
|
|
||||||
product: 'zikai-main / 1.0.2.6',
|
|
||||||
webgl: '/448/main-0.0.0/',
|
|
||||||
apk: '/448/main-0.0.0.apk',
|
|
||||||
control: { zh: '角色控制器', en: 'Character controller' },
|
|
||||||
note: { zh: '基线版本', en: 'Baseline' },
|
|
||||||
order: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'main-0.0.1',
|
|
||||||
track: 'main',
|
|
||||||
version: '0.0.1',
|
|
||||||
unity: '2021.3.12f1',
|
|
||||||
product: 'beta / 1.0.2',
|
|
||||||
webgl: '/448/main-0.0.1/',
|
|
||||||
apk: '/448/main-0.0.1/zikai.apk',
|
|
||||||
control: { zh: 'WASD 移动、K 跳跃', en: 'WASD move, K jump' },
|
|
||||||
note: { zh: '升级 Unity,新增屏幕按键', en: 'Upgraded Unity, added on-screen buttons' },
|
|
||||||
order: 5
|
|
||||||
},
|
|
||||||
|
|
||||||
// doby 线
|
// doby 线
|
||||||
{
|
{ id: 'hongXiang-0.0.0', track: 'doby', version: '0.0.0', unity: '2021.2.10f1', product: 'DobyAdventure', webgl: null, apk: '/448/hongXiang-0.0.0/2D game.apk', order: 3 },
|
||||||
id: 'hongXiang-0.0.0',
|
|
||||||
track: 'doby',
|
|
||||||
version: '0.0.0',
|
|
||||||
unity: '2021.2.10f1',
|
|
||||||
product: 'DobyAdventure',
|
|
||||||
webgl: null,
|
|
||||||
apk: '/448/hongXiang-0.0.0/2D game.apk',
|
|
||||||
control: { zh: 'A/D 移动、跳跃、吃金币', en: 'A/D move, jump, collect coins' },
|
|
||||||
note: { zh: '2D 平台跳跃', en: '2D platformer' },
|
|
||||||
order: 3
|
|
||||||
},
|
|
||||||
|
|
||||||
// roll-a-ball 线(3D,项目形态转变)
|
// roll-a-ball 线(3D,项目形态转变)
|
||||||
{
|
{ id: 'ball', track: 'roll-a-ball', version: '0.0.2', unity: '2021.3.12f1', product: '3dRoingBallt2 / 0.0.2', webgl: '/448/ball/', apk: '/448/203wangz199.apk', order: 6 }
|
||||||
id: 'ball',
|
|
||||||
track: 'roll-a-ball',
|
|
||||||
version: '0.0.2',
|
|
||||||
unity: '2021.3.12f1',
|
|
||||||
product: '3dRoingBallt2 / 0.0.2',
|
|
||||||
webgl: '/448/ball/',
|
|
||||||
apk: '/448/203wangz199.apk',
|
|
||||||
control: { zh: '手机陀螺仪倾斜移动、上倾/点击跳跃', en: 'Tilt phone to move, tilt up/tap to jump' },
|
|
||||||
note: { zh: '最终版本,项目转向 3D 滚球', en: 'Final version, project shifted to 3D rolling ball' },
|
|
||||||
order: 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 截图(复用 448 现有图片)
|
|
||||||
export const screenshots = [
|
|
||||||
{ src: '/448/1.jpg', caption: { zh: '项目截图 1', en: 'Screenshot 1' } },
|
|
||||||
{ src: '/448/2.jpg', caption: { zh: '项目截图 2', en: 'Screenshot 2' } },
|
|
||||||
{ src: '/448/3.jpg', caption: { zh: '项目截图 3', en: 'Screenshot 3' } }
|
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user