外观
TypeScript推箱子游戏项目详解
这是一个使用 TypeScript 和 Vue 开发的推箱子游戏项目,是我早期学习 TypeScript 时的练手项目。通过这个项目,我深入理解了 TypeScript 的类型系统、类设计、枚举使用等核心特性,同时也实践了 Vue 的响应式数据绑定和组件化开发思想。
项目概述
推箱子(Sokoban)是一款经典的益智游戏,玩家需要在一个狭小的仓库中,将木箱推到指定的位置。游戏的核心挑战在于需要巧妙地利用有限的空间和通道,合理安排移动次序,避免箱子被卡住或通道被堵死。
项目开源地址
技术栈
- TypeScript - 实现游戏核心逻辑,提供类型安全保障
- Vue.js - 负责游戏界面的渲染和用户交互
- RequireJS - 模块化加载和管理
- LocalStorage - 保存游戏进度和关卡信息
项目结构
ChestGame/
├── chest.html # 游戏主页面
├── src/
│ ├── js/
│ │ ├── chest.ts # TypeScript源文件(游戏核心逻辑)
│ │ ├── chest.js # 编译后的JavaScript文件
│ │ ├── global.d.ts # TypeScript类型声明
│ │ ├── msg.js # 消息提示模块
│ │ └── storage.js # 本地存储模块
│ ├── img/ # 游戏资源图片
│ └── lib/ # 第三方库(Vue、RequireJS、Layer等)
└── README.md # 项目说明文档核心设计
1. 地图数据表示
游戏地图使用二维数组 number[][] 来表示,其中:
- 一维数组的下标表示坐标系的 Y 轴(行)
- 二维数组的下标表示坐标系的 X 轴(列)
- 数组的值表示该坐标点的元素角色
2. 角色枚举(Role Enum)
使用 TypeScript 枚举定义了地图上的所有角色类型:
enum Role {
Blank, // 空白
Wall, // 墙
People, // 小人
PeopleDot, // 站在点上的小人
Chest, // 箱子
Dot, // 箱子目标点
ChestDot // 已到达目标点的箱子
}这种设计充分利用了 TypeScript 的类型系统,使得代码更加清晰和安全。枚举值对应数字,便于在二维数组中存储和比较。
3. 移动方向枚举(MovePotion Enum)
enum MovePotion {
Up,
Down,
Left,
Right
}核心类设计
Map 类 - 地图管理
Map 类负责管理游戏地图数据和关卡信息:
class Map {
private level: number = 0;
private levelMap: string[] = [/* 50个关卡的JSON字符串 */];
// 获取当前关卡地图数据
getMapData(): number[][]
// 获取下一关地图数据
getNextLevelMapData(): number[][]
// 获取上一关地图数据
getPrevLevelMapData(): number[][]
// 获取小人初始坐标
getPeopleInitCoord(): Coord
}设计亮点:
- 关卡数据以 JSON 字符串形式存储,节省内存
- 使用 LocalStorage 保存当前关卡进度
- 提供关卡切换、重置等便捷方法
People 类 - 小人移动逻辑
People 类是游戏的核心,负责处理小人的移动逻辑:
class People {
private currentCoord: Coord;
// 移动小人
move(potion: MovePotion) {
// 1. 计算新坐标
// 2. 检查新坐标是否可移动
// 3. 处理不同场景:
// - 移动到空白处
// - 移动到目标点
// - 推动箱子
// 4. 更新地图状态
// 5. 验证是否通关
}
}核心移动逻辑:
- 空白移动:小人从当前位置移动到空白处或目标点
- 推动箱子:如果前方是箱子,检查箱子前方是否可移动
- 状态更新:正确处理小人站在目标点、箱子到达目标点等复合状态
- 步数统计:每次有效移动后增加步数
关键代码片段:
if (newCoordRole == Role.Blank) {
// 移动到空白处
game.setCoordRole(peopleNewCoord, Role.People);
game.setCoordRole(this.currentCoord,
currentCoordRole == Role.PeopleDot ? Role.Dot : Role.Blank);
updateCurrentCoord();
} else if (newCoordRole == Role.Chest || newCoordRole == Role.ChestDot) {
// 推动箱子
let newChestCoord: Coord = this.getNewCoord(peopleNewCoord, potion);
let newChestCoordRole: Role = game.getCoordRole(newChestCoord);
// 检查箱子前方是否可移动
if (newChestCoordRole == Role.Wall ||
newChestCoordRole == Role.ChestDot ||
newChestCoordRole == Role.Chest)
return;
// 更新箱子位置
game.setCoordRole(newChestCoord,
newChestCoordRole == Role.Dot ? Role.ChestDot : Role.Chest);
// 更新小人位置
game.setCoordRole(peopleNewCoord,
newCoordRole == Role.ChestDot ? Role.PeopleDot : Role.People);
// 更新原位置
game.setCoordRole(this.currentCoord,
currentCoordRole == Role.PeopleDot ? Role.Dot : Role.Blank);
updateCurrentCoord();
}Game 类 - 游戏主控制器
Game 类负责整合所有功能,使用 Vue 进行界面渲染和交互:
class Game {
private vueObj: any;
private isOver: boolean = false;
constructor(mapData: number[][]) {
// 初始化 Vue 实例
this.vueObj = new vue({
el: '#app',
data: {
Map: mapData, // 地图数据
IsDesign: false, // 是否设计模式
IsLook: false, // 是否选择关卡模式
Level: 1, // 当前关卡
Pace: 0 // 步数
},
methods: {
// 方向控制方法
Up(), Down(), Left(), Right(),
// 游戏控制方法
Reload(), Reset(), Look()
}
});
}
// 验证是否通关
verifySuccess(): void {
// 检查是否所有目标点都有箱子
// 如果通关,自动进入下一关或显示通关提示
}
}Vue 模板渲染:
<template v-for="(yItem,y) in Map">
<ul>
<li v-for="(xItem,x) in yItem" @click="SetRole(y,x)">
<div :class="getRoleClass(xItem)"
style="width:100%;height:100%;"
:title="xItem"></div>
</li>
</ul>
<div style="clear:both;"></div>
</template>通过 Vue 的响应式系统,地图数据的变化会自动反映到界面上,无需手动操作 DOM。
关键技术点
1. TypeScript 类型系统
项目充分利用了 TypeScript 的类型系统:
- 接口定义:使用
Coord接口定义坐标结构 - 枚举类型:使用枚举定义角色和方向,提高代码可读性
- 类型注解:所有方法参数和返回值都有明确的类型定义
- 访问修饰符:使用
private保护内部状态
2. 状态管理
游戏状态通过以下方式管理:
- 地图状态:存储在 Vue 的响应式数据中
- 小人位置:由
People类维护 - 关卡进度:使用 LocalStorage 持久化
- 游戏状态:通过
isOver标志控制
3. 事件处理
支持两种操作方式:
- 键盘操作:监听
keyup事件,支持方向键控制 - 按钮操作:界面提供方向按钮,适合移动端使用
4. 通关检测
通关检测逻辑:
verifySuccess(): void {
let isPass: boolean = true;
// 遍历地图,检查是否还有未完成的目标点
for (var y = 0; y < this.vueObj.Map.length; y++) {
for (var x = 0; x < this.vueObj.Map[y].length; x++) {
let role: Role = this.vueObj.Map[y][x];
// 如果还有 Dot 或 PeopleDot,说明未通关
if (role == Role.Dot || role == Role.PeopleDot) {
isPass = false;
break;
}
}
}
if (isPass) {
// 通关处理逻辑
}
}项目特色功能
- 多关卡支持:内置 50 个精心设计的关卡
- 进度保存:自动保存当前关卡,刷新页面不丢失进度
- 步数统计:记录每关使用的步数
- 关卡选择:支持快速切换关卡
- 重置功能:可以重置当前关卡或回到第一关
- 响应式设计:适配不同屏幕尺寸
开发心得
通过这个项目,我深刻体会到了 TypeScript 的优势:
- 类型安全:编译时就能发现很多潜在错误,减少运行时 bug
- 代码提示:IDE 的智能提示让开发效率大大提升
- 重构友好:类型系统让代码重构更加安全和便捷
- 文档作用:类型定义本身就是最好的文档
同时,这个项目也让我更好地理解了:
- 面向对象设计在游戏开发中的应用
- Vue 响应式系统的工作原理
- 状态管理和数据流的设计
- 游戏逻辑的抽象和封装
总结
这个推箱子游戏项目虽然规模不大,但涵盖了 TypeScript 的核心特性、Vue 的基础应用、游戏逻辑设计等多个方面。对于学习 TypeScript 和前端游戏开发来说,是一个很好的练手项目。
项目代码结构清晰,职责分离明确,充分体现了 TypeScript 在类型安全和代码组织方面的优势。如果你也想学习 TypeScript,不妨从这样一个小游戏项目开始,在实践中掌握类型系统的使用。
回头再看这个几年前完全靠自己手搓出来的项目,心里还是挺感慨的——那时候的自己真的挺厉害。要是搁现在,估计第一反应就是让 AI 来帮忙实现了。
贡献者
更新日志
2025/12/2 15:02
查看所有更新日志
8cf01-docs(blog): 添加推箱子游戏演示链接于ae92e-docs(blog): 添加TypeScript推箱子游戏项目详解博客文章于
版权所有
版权归属:ntzw
许可证:CC0 1.0 通用 (CC0)
