叶语霄:负责定义全局变量和项目架构
袁沛文:编写view模块
李子昂:编写model模块
梁瑛平:编写controller模块
项目地址:https://github.com/Insomnia-y/Gold-Miner
若要开始游戏,请运行./Release/GAME.exe。
若上述方法无法开始游戏,可以在visual studio中切换到Debug模式下手动编译运行。编译前请先修改链接器中的附加库目录和属性页最下方MS Assembly中的IncludePath。分别添加masm32的目录和项目的目录。
项目文件夹的结构如下:
Gold-Miner
├─Release
│ ├─GAME.exe
├─Gold-Miner
│ ├─include
│ ├─main.asm
│ ├─vars.asm
│ ├─controller.asm
│ ├─model.asm
│ ├─view.asm
├─lib
└─resource
├─icon
└─music
./Release/GAME.exe:生成的可执行文件,点击即可直接开始游戏
./Gold-Miner/main.asm:程序入口,包含main子函数,即程序开始运行的地方。
./Gold-Miner/vars.asm:定义的全局变量
./Gold-Miner/controller.asm:控制器模块,负责捕获用户的鼠标和键盘事件
./Gold-Miner/model.asm:以不同方式响应用户的不同事件,根据游戏逻辑调用函数,修改全局变量
./Gold-Miner/view.asm:根据当前的全局变量,绘制游戏界面
./Gold-Miner/include:包含所有.inc文件
./lib 包含图形库acllib,以及自己手写的c语言库
./resource:游戏的图片和音乐素材
鼠标左键点击:
- 在欢迎界面点击任意位置,开始游戏
- 在游戏界面点击地下部分区域,释放钩索
- 在游戏界面点击menu,回到欢迎界面并重新开始游戏
- 在商店界面点击道具,购买道具
键盘:
- 空格,释放鞭炮
←
键和→
,操作电动勾
不同物体的半径、重量、价值:
类别 | 半径 | 重量 | 价值 |
---|---|---|---|
0(石头) | 20像素 | 80像素/秒 | 10 |
0(石头) | 35像素 | 40像素/秒 | 20 |
1(金块) | 20像素 | 120像素/秒 | 50 |
1(金块) | 35像素 | 80像素/秒 | 100 |
1(金块) | 50像素 | 30像素/秒 | 500 |
2(钻石) | 20像素 | 120像素/秒 | 600 |
3(福袋) | 20像素 | 30~120像素/秒 | 10~1200 |
4(TNT) | 35像素(爆炸半径200) | 无 | 无 |
不同物体在各关的出现概率:
类别 | 出现概率(1 |
---|---|
石头 | 0.1\0.2\0.2 |
金块 | 0.4\0.4\0.3 |
钻石 | 0.2\0.1\0.1 |
福袋 | 0.2\0.1\0.1 |
TNT | 0.1\0.2\0.3 |
道具功能介绍:
道具 | 效果 |
---|---|
石头收藏书 | 提高石头价值(石头价值*2) |
鞭炮 | 不必多说,空格释放 |
神水 | 提高拉回速度(拉回速度*2) |
幸运草 | 提高福袋出现概率(由10%提升为33%) |
磁铁 | 可以吸住金子(金子的判定半径增加30) |
电动勾 | 可以控制钩子下降角度(使用←、→控制方向) |
这部分玩家不用看,只是为了向老师证明我们是自己做的
采用MVC架构,model负责根据游戏逻辑维护游戏的全局变量,view负责根据全局变量绘制界面,controller负责响应用户事件。
当前窗口curWindow
:DD,指示当前所在的游戏界面。0为欢迎界面,1为游戏界面,2为过关界面,3为失败界面。
游戏有效区域的高度和宽度gameX
gameY
:DD。
本关卡剩余时间restTime
:DD。以s为单位。
定时器timer
:每10ms为一个时间片,为游戏的最小时间单元。在每个时间片开始或结束时触发定时器,model维护全局变量,且view根据维护后的全局变量重绘界面。
目标得分goalScore
:DD,本局游戏的目标得分。
当前得分playerScore
:DD,当前得分。在各关卡累加。仅当用户主动返回菜单或游戏未过关被动结束时清零。
minerPosX
:DD,矿工位置X坐标。
minerPosY
:DD,矿工位置Y坐标。
lastHit
DD,上一次命中的物体的下标。(注意是28的倍数)
itemNum
DD,物体数量。
为方便起见,认为物体的逻辑形状是圆心位置固定的圆,视觉形状是不规则的图形(加载素材)。
描述一个物体的结构体Item
定义如下:
- 存在
exist
:DD,1存在,0不存在。 - 类型
type
:DD,枚举值,{石头,金块,钻石,福袋,TNT}。 - X位置
posX
:DD,物体的圆心位置X坐标。 - Y位置
posY
:DD,物体的圆心位置Y坐标。 - 半径
radius
:DD,物体的半径。 - 重量
weight
:DD。 - 价值
value
:DD。
当前钩索状态hookStat
:DD,当前钩索是否被释放。1时释放,表现为下一次触发时间片时钩索位置变化,钩索角度不变;0时不释放,表现为下一次触发时间片时钩索角度变化,钩索位置不变。
当前钩索角度移动方向hookODir
:DD,**仅当hookStat为false时有意义。**为0时向右转,为1时向左转。
当前钩索位置移动方向hookDir
: DD,**仅当hookStat为true时有意义。**为0时向下移动(回收),为1时向上移动(下放)。
钩索角速度hookOmega
,DD,常量。
钩索线速度hookV
,DD,有一基础值(下放和未命中回收时),命中回收时依赖于抓到的物体类型。
钩索角度hookDeg
:DD,取值范围为180~360度。
钩索位置hookPosX
hookPosY
:DD。
不同hookStat和hookDir(或hookODir)组合的含义如下:
hookStat | hook(O)Dir | 含义 |
---|---|---|
0 | 0(ODir) | 向右转 |
0 | 1(ODir) | 向左转 |
1 | 0 | 向下移动 |
1 | 1 | 向上移动 |
; @brief: 鼠标点击游戏区域时,释放钩索。
; @read: 无
; @write: 写hookStat
为1,写hookDir
为0,写hookV
为默认值(120),写lastHit
为-1。
触发定时器时,依次调用以下函数。
根据hookStat
,计算并更新hookPos
或hookDeg
。
; @brief: 判断钩索是否命中物体。遍历items中所有物体的位置(posX、posY),判断钩索位置与物体位置的距离是否小于物体半径。 ; @read: hookPosX,hookPosY,Items ; @write: lastHit,hookDir,hookV。若命中,写lastHit为命中物体的下标,写hookDir为1,写hookV为f(Items[lastHit].weight)。
; @brief: 判断钩索是否出界或回到矿工手中。 ; @read: hookPosX,hookPosY,lastHit ; @write: hootDir,hookStat,Items,playerScore。若出界,写hookDir为1;若回到矿工手中,写hookStat为0,写Items[lastHit].exist为0,写playerScore+=Items[lastHit].value
- 离散刷新问题。在刷新间隔长且物体半径小的情况下可能“掠过”物体,即未响应应该响应的isHit。解决方法:缩短刷新间隔,增大物体的半径。
- 计算机坐标系与常规坐标系不一致。计算机坐标系$(x,y)$ = 常规坐标系$(-y',x')$。
hookDeg
在常规坐标系上定义,取值范围$[180,360]$。故当调用moovHook改变钩索位置时,移动增量$(Δx,Δy) = (-ρsinθ, ρcosθ)$,其中$θ$ 即hookDeg
,$ρ$ 即hookV
。 - 计算精度问题。最初计算三角函数时,由于疏忽采用(int)取整,但这种方式只能截断取整数部分,没有四舍五入的效果,因此导致误差,体现为计算结果与期望值不一致。解决方法:改为采用round()函数取整,达到了期望的四舍五入的效果。
- 取整误差问题。抓取大物体时速度过慢,当ρ很小sinθ也很小的时候容易出现Δx为0的情况,导致相乘后的偏移量四舍五入后变为0,表现为钩子水平移动无法回到矿工手中。解决方法:移动过程中,如果检测到hookX的偏移量等于0时,根据钩子目前下落将其设置为+1或-1。
- 使用的acllib图形库有导入图形的格式限制,无法导入无底色的png图片作为贴图。解决方法:采用更高级的图形库
用C语言处理一些计算,避免汇编中的浮点数运算。
计算两点间距离:
// 计算两点间距离
// @params: (x1,y1)第一个点坐标 (x2,y2)第二个点坐标
// @return: ans,int型距离。
extern "C" int calDistance(int x1, int y1, int x2, int y2) {
int ans = round(sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)));
return ans;
}
计算psinθ:
// 计算ρsinθ
// @params: theta,角度制,实际取值范围[180, 360];ρ,极径
// @return: ans
extern "C" int calPSin(int deg, int r) {
int ans = round(r*sin(deg * 2 * PI / 360));
return ans;
}
计算pcosθ:
// 计算ρcosθ
// @params: 同上
// @return: ans
extern "C" int calPCos(int deg, int r) {
int ans = round(r*cos(deg * 2 * PI / 360));
return ans;
}