在chrome浏览器的断网页面,按空格键或者向上键会出现一个小恐龙跑酷小游戏,这个2D小游戏在设计上精致小巧,在代码上也只有三千多行,思路清晰严谨,很有学习价值

demo

在非断网情况下,可以通过chrome://dino 进行访问,源代码在source面板中无法显示,可以前往这里下载。在这篇文章中异名会梳理2D游戏的制作思路,主要包括游戏的mainloop主循环和实例的update更新、帧图的动态绘制和切换、帧率的控制、游戏对象的运动控制、碰撞检测的实现等

游戏循环

循环是游戏的心跳,是一个定时回调,每隔一段时间去更新游戏的逻辑,比如处理用户的交互,更新游戏的状态,绘制动画等等

mainloop() {

this.clearCanvas()  // 清除画布

//  处理逻辑....

window.requestAnimationFrame(this.mainloop.bind(this));

}

在rAF没出现之前,大家使用setTimeout和setInterval来触发视觉的变化,但是这两个api在时间的精准控制上有缺陷。因为「定时器属于异步任务,它必须等到同步任务执行完毕之后,以及异步队列里面的任务清空之后才轮到自己执行,它的实际执行时机一般都比设定的时间晚」,这就说明了它不能精准地按照一定的时间间隔去执行。还有一点就是「定时器的调用间隔和屏幕绘制频率不一致」,显示器的频率一般都默认是60Hz(1s绘制60次),每次绘制的时间差是16.7ms(1000/60≈16.7),因为定时器的调用间隔和屏幕频率不一致,所以下面这种情况就一定会出现

settimeout

红色叉叉那里就丢帧了,下面通过一个更清晰的例子来说明:

这也是为什么以前大家把setInterval的间隔设置为1000/60的原因,但是这本质上是硬件的差异,只要换个硬件,定时器的执行步调和屏幕的刷新步调不一致就一定会产生丢帧。这也就是rAF的最大优势,它是「由系统来决定回调函数的执行时机,系统每次绘制之前会主动调用 rAF 中的回调函数」,它能够确保回调函数是按照系统的绘制频率来调用,无论是60Hz还是50Hz,只要画面刷新就会调用回调函数,它就解决了步调统一以及回调频率可靠这两个问题。但是因为是系统主动调用,所以需要我们自己去做时间管理,raf的回调第一个参数是一个时间戳,但是在实践上一般我们自己计时

mainloop() {

const now = performance.now()

const deltaTime = now - (this.time || now)

this.time = now

this.clearCanvas()  // 清除画布

// 处理逻辑...

window.requestAnimationFrame(this.mainloop.bind(this))

}

在源码中,这里还做了一个严谨的设计,它在非游戏中的时候会暂停mainloop循环并且清除rAF,再次游戏的时候会再次触发mainloop,所以这里还做了一个加锁

scheduleNextUpdate: function (){

if (!this.updatePending) {

this.updatePending = true

this.raqId = requestAnimationFrame(this.update.bind(this))

}

}

画面绘制

游戏基于canvas来绘制,游戏的图片资源只有一张base64格式的精灵图,如下

sprite

游戏的对象都在这张精灵图中,我们先从精灵图中把地面绘制出来。这里面涉及到的知识点是canvas的创建、画面清除,以及drawImage的应用。通过drawImage我们可以裁剪精灵图中某一部分的图像,并绘制到画布中,drawImage一共有9个参数context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height) 分别是精灵图、裁剪区域的坐标,裁剪的区域大小,在画布上放置图像的位置坐标,在画布上放置图像的大小。简单拆分一下任务:

下载图片资源

创建画布

从精灵图中裁剪地面部分并绘制

核心代码如下

// 下载资源

loadImage() {

return new Promise((resolve, reject) => {

const img = new Image()

img.src = "精灵图的base64"

img.onload = () => {

window.imageSprite = img

resolve(img)

}

img.onerror = () => {

reject()

}

})

}

// 绘制画布

initCanvas() {

const canvas = document.createElement('canvas')

canvas.width = CANVAS_WIDTH

canvas.height = CANVAS_HEIGHT

document.body.appendChild(canvas)

this.canvas = canvas

this.ctx = canvas.getContext('2d')

}

// 二次绘制的时候清除画布

this.ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_WIDTH, CANVAS_HEIGHT)

// 绘制地面

this.ctx.drawImage(window.imageSprite,

2, 54, 600, 12,

this.xPos, this.yPos, 600, 12

)

同样利用context.drawImage可以把精灵图里面的其他对象也绘制画布上,组合出游戏里面的对象

绘制画面

动画和帧频控制

游戏中的每个实例都有update的方法, update在每次主循环中都会执行,在这个小恐龙游戏中每个实例的update都被直接地调用,如果需要更好地解耦和维护可以使用订阅发布等模式

mainloop() {

// ...

ground.update()

trex.update()

}

ground.update = function() {

// ...

context.drawImage() // 更新绘制

}

动画就涉及到更新频率,如果像上面那样每次循环的时候都去绘制,mainloop一秒会执行60次,但是绘制的内容更新并没有这么频繁,所以我们需要做时间管理。「游戏中的帧频可以分为两种,一个是序列帧的帧频,一个是游戏的全局帧频」。比如恐龙就是由指定的序列帧动画展示的,它一共有5种状态,其帧动画参数定义如下

Trex.animFrames = {

WAITING: {                    // 等待状态下的序列帧

frames: [44, 0],            // 每一帧的起点位置

msPerFrame: 1000 / 3        // 绘制的频率

},

RUNNING: {                    // 奔跑状态下的序列帧

frames: [88, 132],          // 每一帧的地点位置

msPerFrame: 1000 / 12       // 绘制的频率

},

CRASHED: {

frames: [220],

msPerFrame: 1000 / 60

},

JUMPING: {

frames: [0],

msPerFrame: 1000 / 60

},

DUCKING: {

frames: [264, 323],

msPerFrame: 1000 / 8

}

};

拿奔跑状态来说,它是由两张图片按12Hz的频率来更新的,每一帧的耗时是1000/12,我们在update的时候做一个计时:

class Trex{

constructor(ctx) {

this.ctx = ctx

this.currentAnimFrames = Trex.animFrames['RUNNING'].frames

this.msPerFrame = Trex.animFrames['RUNNING'].msPerFrame

this.currentFrame = 0

this.timer = 0

}

update(dt) {

this.timer += dt

// 更新当前帧序号

if (this.timer >= this.msPerFrame) {

this.currentFrame = this.currentFrame == this.currentAnimFrames.length - 1 ? 0 : this.currentFrame + 1;

this.timer = 0;

}

// 绘制当前帧图

const sx = this.currentAnimFrames[this.msPerFrame]

this.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)

}

}

另外一种动画就是非序列帧动画,比如地面的运动,因为没有指定的帧频所以它的运动频率就是全局的帧频

const FPS = 60    // 设定全局的帧频为60

ground.update(dt) {

// 根据全局的帧频计算速度

const increment = Math.floor(speed * (FPS / 1000) * dt);

this.xPos -= increment

// 绘制当前帧图

const x = this.xPos

this.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)

}

给小恐龙加上序列帧动画以及给跑道加上位移之后效果如下:

run

值得注意的是,在小恐龙游戏中没有对主循环做帧频控制,每一次循环的时候都会执行清除画布和画面重绘操作,如果遇到需要可控帧频的场景主循环就可能会产生过度绘制或者丢帧的情况了

用户交互和运动状态

小恐龙游戏中的用户交互主要是跳和下蹲,监听用户按键事件,根据键码去切换小恐龙的状态和处理位置信息。这里有两个小逻辑,在蹲的时候因为帧图的大小有变化需要做宽高的切换;在跳的时候因为游戏是变速运动,所以也根据游戏的当前速度做了一个关联

我们把仙人掌加上之后,游戏的核心交互流程就已经实现出来了:

碰撞检测

小恐龙里面使用的是矩形检测,每个碰撞体都是一个矩形,游戏循环的时候判断每个矩形是否重叠就知道是否碰撞了。

collision_boxs

因为物体是不规则的形状,所以像左上图那样只有两个矩形是做不到精准地描述物体的边界的。「在游戏中,为了简化每一帧中的计算计算量,只有当这两个外矩形相碰的时候,才会去遍历每个对象下的细分矩形」,比如右上图小恐龙和仙人掌都分别用了四个矩形来描述它们的边界,当外矩形重叠的时候,内部矩形才开始遍历判断重叠,下面这个过程图很好地把这个过程演示了出来:

collision

碰撞盒子以及恐龙的碰撞盒子定义:

矩形重合判断

在mainloop中进行碰撞检测:

结尾

上面就已经把小恐龙的核心功能过了一遍,剩下的一些小功能堆叠和细节的完善,就不再展开。异名以往都是通过游戏引擎或者互动框架来开发游戏,这还是第一次生撸,引擎封装带来的开发体验和自己从零开发是不一样的,这也是前段时间异名的小困惑,高度封装就代表底层的隐藏,开发一段时间之后很快就会遇到概念上的困惑,甚至你的理解和真实的情况完全相反,虽然他们的表现一致,这次跟着代码敲完一次之后,异名对2D游戏的制作思路也有了更清晰的理解。

小恐龙游戏python_从Chrome小恐龙游戏学习2D游戏制作相关推荐

  1. unity2d游戏开发系列教程:四、一个2D游戏所需要的主要功能(游戏框架)

    目录 unity2d游戏开发系列教程:一.环境安装 unity2d游戏开发系列教程:二.新建工程并熟悉Unity编辑器常用功能 unity2d游戏开发系列教程:三.场景布置,增加怪物和机关 原文下载 ...

  2. 【J2ME 2D 游戏开发系列】◣HIMI游戏开发启蒙教程◢JAVA零基础学习J2ME游戏开发全过程!...

    本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/j2me-2/774.html Himi从写 ...

  3. 2d游戏引擎_8年,从2D到3D,我的学习之路

    Mickey 写了一篇 <一个本科毕业生创业两年的感悟>,从他的视角,总结了我们合作的两年经历. 我也来写一篇,介绍我的学习之路,希望对大家有所帮助,谢谢大家- 我的学习方法 1.直接从0 ...

  4. 2d游戏和 3d游戏的区别

    2D游戏和3D游戏的主要区别 一.总结 一句话总结:2D中的单位就是贴图,3D中的单位还有高 1. 3D 和 2D 游戏的区别主要体现在呈现画面和文件体积上: 2. 借助 3D 引擎可以提升 2D 游 ...

  5. 2D游戏和3D游戏的主要区别

    游戏的体现形式最主要是 2D 和 3D.最近比较流行的 AR/VR 等,都是属于 3D 类的体现形式.       最初的游戏,2D 是绝对的主流.虽然现如今 3D 游戏大行其道,但是 2D 游戏还是 ...

  6. 2D游戏比3D游戏哪个更好做?游戏行业什么职业最吃香?

    通常情况下,同样档次2D游戏,开发成本和难度低于3D游戏. 对比两组共六个游戏,分别说明目前业界在2D游戏和3D游戏不同做法. 2D游戏(征途) 2D游戏(龙之皇冠) 2D游戏(奥利和迷雾森林) 3D ...

  7. WEBGL 2D游戏引擎研发系列 第三章 正交视口

    WEBGL 2D游戏引擎研发系列 第三章 <正交视口> 作者:HTML5游戏开发者社区-白泽 转载请注明出处:http://html5gamedev.org/ 目录 HTML5 2D游戏引 ...

  8. WEBGL 2D游戏引擎研发系列 第一章 新的开始

    WEBGL 2D游戏引擎研发系列 第一章 <新的开始> ~\(≥▽≤)/~HTML5游戏开发者社区(群号:326492427) 转载请注明出处:http://html5gamedev.or ...

  9. 网页及移动平台2D游戏开发探索

    最近入手了一台Nexsus S,发现在移动平台上3D游戏的体验比较差,还是2D细腻的画面表现力更好,也更适合休闲游戏的开发,于是搜索了一下2D游戏开发环境: 1. 可以跨PC,IOS,Android的 ...

  10. 认识AndEngine选自Android 2D游戏引擎AndEngine快速入门教程

    认识AndEngine什么是AndEngine 随着Android手机.平板的盛行,Android下的游戏也不断的变得火热.而对于游戏开发有兴趣的同学们,应该也想要学习开发游戏.虽说游戏开发的引擎较多 ...

最新文章

  1. 字符串 内存 函数的介绍与模拟实现
  2. java===java基础学习(11)---继承
  3. Gossip协议详解
  4. 使用 Inno Setup 快速打包你的应用程序
  5. printf 函数实现的深入剖析
  6. 杭州滨江工作方案:将区块链等产业与“数字滨江”、“数字经济”紧密相连
  7. 校园导游图的课程设计(三)
  8. 数论之神 HYSBZ - 2219 (原根+指标+bsgs+crt+exgcd+思维)
  9. ReactMotion Demo8 分析
  10. 计量芯片HLW8032在充电桩设备中的典型应用
  11. 蓄力一纪,可以远矣!十二年的百度地图和他的AI新征程
  12. 华哥倒酒(二分答案)
  13. 如何加密文件及文件夹
  14. 汉堡王什么汉堡好吃_汉堡王什么汉堡好吃?隐藏的点单攻略快来看
  15. ARM与DSP的区别
  16. caffe 报错 Aborted(core dumped
  17. 笔记本写代码 屏幕尺寸_为什么笔记本电脑的屏幕尺寸如此奇怪?
  18. 网易云课堂微专业--Java高级开发工程师
  19. 《月亮与六便士》读后
  20. 代码生成器-mybatis-plus-generator

热门文章

  1. c语言装b程序,C语言打造表白和装逼利器:亲爱的让我们相爱在一起,酷炫的梦幻...
  2. Android 手机横屏时,输入框铺满全屏的解决方案
  3. deepfacelab安卓版_deepfacelab
  4. 个人用游戏设计框架图
  5. 2016年5月27日更新后MSYS2的shell用法
  6. 学计算机猝死,北京邮电大学计算机系一名学生球场踢球时猝死
  7. ubuntu安装windows中的常用字体
  8. 【渗透安全】利用腾讯云CDN节点隐藏连接Webshell的真实IP
  9. 自媒体文章标题的大坑,难怪你写的文章没有推荐
  10. 七上八下猜数字_《脑筋急转弯》