最近又做了一个小游戏,由于上次做的小游戏(打砖块),分享时主要是贴的代码和注释,可能在阅读上不太容易理解;那么这次就主要和大家聊一聊开发时的心路历程,分享一下解决小游戏流程开发的一些思路。有兴趣的小伙伴也可以联系我,互相探讨一下。
这次分享的小游戏是一个RPG风格的角色对战游戏,可以通过按键分别控制两个角色移动和攻击。在游戏中,角色
站立、
移动、
攻击、
受伤、
死亡分别对应一套动画去渲染,同时还有
攻击范围判定、
攻击掉血机制以及
死亡判定等。在实现游戏各种功能的思路上,我主要是将每个需要实现功能拆分开,独立实现,再组合起来,接下来会详细介绍每个功能实现的思路。
传送门:
h5小游戏-打砖块:
http://yangyunhe.oschina.io/my_blog/code_page/h5-game-heroVSmonster/index.html:
https://github.com/yangyunhe369/h5-game-heroVSmonsterps:github 地址有代码演示,以及源码可供参考,线上演示地址可供预览,关于源码有任何疑问可通过 guihub 的联系方式联系本人
游戏实现截图
- class Animation{
- constructor (type, action, fps) {
- let a = {
- type: type, // 角色类型,怪兽或英雄
- action: action, // 根据传入动作生成不同动画对象数组
- images: [], // 当前引入角色图片对象数组
- img: null, // 当前显示角色图片
- imgIdx: 0, // 当前角色图片序列号
- count: 0, // 计数器,控制动画运行
- fps: fps, // 角色动画运行速度系数,值越小,速度越快
- }
- Object.assign(this, a)
- }
- /**
- * 为角色不同动作创造动画序列
- */
- create () {
- let self = this
- if (self.type === 'hero') {
- for(let item of allImg.hero[self.action]){
- self.images.push(imageFromPath(item))
- }
- } else if (self.type === 'monster') {
- for(let item of allImg.monster[self.action]){
- self.images.push(imageFromPath(item))
- }
- }
- }
- }
首先,要实现游戏中角色不同状态下的动画效果,我的想法是封装一个类,来将游戏不同动作对应的序列帧动画都生成好并保存起来,然后通过调用运行动画的相关方法来控制动画逐帧播放。这样角色就实现了动画效果,而且可以在不同动画效果之间切换也能流畅播放;这里的动画类,有两个参数 type 和 action,其中 type 表示角色类型是英雄或怪兽,action 则对应角色的站立、移动、攻击、受伤、死亡动作类型,这样分别生成的动画序列就完成了。这里的 count 在下面角色类的方法中会有应用,主要用于控制角色动画的播放速度,原理就是游戏引擎中的定时器以每秒60帧的速度绘制动画,在每一帧时累加 count 值,并在特定值去切换下一角色动画序列,达到动画连续播放的效果。
这是实现动画的图片素材截图,连续播放的动画就是由一张一张的图片连续切换实现的。
角色类关于角色类的属性属于这个游戏实现的核心,所以相关的属性会比较多,其中部分属性如下:
- class Role{
- constructor (_main, obj) {
- let h = {
- _main: _main, // 游戏主函数对象
- type: obj.type, // 角色类型,怪兽或英雄
- x: obj.x, // x轴坐标
- y: obj.y, // y轴坐标
- w: obj.w, // 角色图片宽度
- h: obj.h, // 角色图片高度
- speedX: 3, // 角色x轴移动速度
- speedY: 3, // 角色y轴移动速度
- life: 8, // 角色血量
- idle: null, // 站立动画对象
- run: null, // 奔跑动画对象
- attack: null, // 攻击动画对象
- hurt: null, // 受伤动画对象
- die: null, // 死亡动画对象
- canMove: true, // 能否移动
- isFlipX: false, // 是否翻转画布绘制图片,用于绘制人物朝右动画
- isAttacking: false, // 是否处于攻击状态
- isDie: false, // 是否死亡,血量降为0即死亡
- direction: null, // 角色朝向
- state: 1, // 保存当前状态值,默认为0
- state_IDLE: 1, // 站立不动状态
- state_RUN: 2, // 奔跑状态
- state_ATTACK: 3, // 攻击状态
- state_HURT: 4, // 受伤状态
- state_DIE: 5, // 死亡状态
- }
- Object.assign(this, h)
- }
- ...
- }
* type:角色类型,英雄或怪兽
* x,y:角色坐标
* speedX,speedY:角色 X 轴和 Y 轴的移动速度
* life:角色生命值
* idle、run、attack、hurt、die:角色不同状态(站立、移动、攻击、受伤、死亡)下的动画序列对象,并为每个状态设置一个值判断当前角色处于什么状态
* direction:角色朝向,记录当前角色向右或向左
* isFlipX:并判断角色动画是否需要翻转,因为游戏素材角色默认是朝向左侧的,在角色向右时需将画布翻转并重绘动画
* canMove:角色能否移动
* isDie:记录角色是否死亡
* state:记录角色当前状态值
* state_IDLE、state_RUN、state_ATTACK、state_HURT、state_DIE:分别对应角色(站立、移动、攻击、受伤、死亡)几种状态
这样看,可能会觉得比较复杂,那我们先逐步分解一下,逐步还原游戏流程。这个游戏最基础的功能,通过之前的动画类和角色类可以实现绘制角色,这样就可以让角色渲染在地图中,并保持 state_IDLE 站立状态。
接下来,给角色绑定按键事件,使角色响应不同方向按键的动作,同时在角色移动时将角色状态切换为 state_RUN 移动状态,并在按键事件结束时,将角色状态重新切换为 state_IDLE 站立状态。在这里有个需要注意的点,就是角色到达地图边界的判定,当角色到达地图边界时,需要将角色速度改为0;当角色向右时角色的状态为 state_RUN 或其他状态,设置 isFlipX 为 true 将动画翻转绘制。
当角色触发攻击按键事件时,首先执行攻击动画,这里攻击动画会在按键按下并松开时执行完一次完整的动画(否则角色会一直保持攻击姿势),然后需要判断被攻击角色是否处于其攻击范围(这里要判断当前角色的朝向,如果当前角色向右,被攻击角色必须在当前角色右侧时才可攻击),当被攻击角色处于攻击范围时(这里有一个小 bug,当按键按住不松开时,角色攻击判定为 false,即攻击另一角色无效;因为攻击判定会在按键松开时进行判定并扣除血量);如果攻击判定为 true 则扣除被攻击角色血量,同时执行受伤动画,而当角色血量为0时,则执行角色死亡动画,同时将游戏状态切换为游戏结束状态,角色状态切换为state_DIE。
游戏引擎类
- class Game {
- constructor (fps = 60) {
- let g = {
- actions: {}, // 按键事件方法集,并在按键事件触发时调用对应方法
- keydowns: {}, // 按键事件生成对象集
- state: 1, // 游戏状态值,初始默认为 1
- state_START: 1, // 游戏初始化
- state_RUNNING: 2, // 游戏开始
- state_STOP: 3, // 游戏暂停
- state_GAMEOVER: 4, // 游戏结束
- canvas: document.getElementById("canvas"), // canvas 元素
- context: document.getElementById("canvas").getContext("2d"), // canvas 画布
- timer: null, // 轮询定时器
- fps: fps, // 动画帧数,默认 60
- }
- Object.assign(this, g)
- }
- ...
- }
这里控制游戏画面绘制的属性中,主要以 state 和 timer 为主,在游戏定时器函数中,会根据动画帧数属性 fps 来逐帧刷新 canvas 画布并绘制图形(fps 默认60帧时,即每1/60秒的时间就会刷新一次画布,并在这里处理相关游戏逻辑)。这里的游戏状态值 state 会根据不同按键操作及游戏流程的变化来进行改变;首先,进入游戏默认为 state_START (游戏初始化阶段),然后按键空格键将游戏状态值切换为 state_RUNNING (游戏运行阶段);在游戏进行过程中,一旦两个角色中其中一方血量为 0 ,即触发游戏结束事件,这时游戏状态值则切换为 state_GAMEOVER;最后关于特定按键事件:暂停按键事件触发时则切换为 state_STOP (游戏暂停阶段)
。
这里和大家分享一下开发类似小游戏的一点小心得:当你设计一个完整的游戏流程时,试着逐个分解每个流程(例如游戏初始化、游戏运行、游戏暂停、游戏结束等等),并将每个流程再细化成一个一个的小需求时,这时解决问题的难度会降低很多,希望能对小游戏感兴趣的朋友能有一点帮助
关于这个游戏的一些流程设计上,主要由本人独立构想完成(如有雷同,纯属瞎扯),所以可能与真实游戏场景会有差异;最后,关于游戏设计上的一些 bug 也欢迎大家指正。
来源: http://www.qdfuns.com/notes/26253/5c24b5aa2f39864d42a70b4963eebf83.html