从Chrome小恐龙游戏学习2D游戏制作

2023-10-07 07:10

本文主要是介绍从Chrome小恐龙游戏学习2D游戏制作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在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 = nowthis.clearCanvas()  // 清除画布// 处理逻辑...window.requestAnimationFrame(this.mainloop.bind(this))}

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

scheduleNextUpdate: function () {if (!this.updatePending) {this.updatePending = truethis.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 = imgresolve(img)}img.onerror = () => {reject()}})
}// 绘制画布
initCanvas() {const canvas = document.createElement('canvas')canvas.width = CANVAS_WIDTHcanvas.height = CANVAS_HEIGHTdocument.body.appendChild(canvas)this.canvas = canvasthis.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 = ctxthis.currentAnimFrames = Trex.animFrames['RUNNING'].framesthis.msPerFrame = Trex.animFrames['RUNNING'].msPerFramethis.currentFrame = 0this.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.xPosthis.ctx.drawImage(img,sx,sy,swidth,sheight,x,y,width,height)
}

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

run

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

用户交互和运动状态

小恐龙游戏中的用户交互主要是跳和下蹲,监听用户按键事件,根据键码去切换小恐龙的状态和处理位置信息。这里有两个小逻辑,在蹲的时候因为帧图的大小有变化需要做宽高的切换;在跳的时候因为游戏是变速运动,所以也根据游戏的当前速度做了一个关联我们把仙人掌加上之后,游戏的核心交互流程就已经实现出来了:

碰撞检测

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

collision_boxs

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

collision

碰撞盒子以及恐龙的碰撞盒子定义:矩形重合判断在mainloop中进行碰撞检测:

结尾

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


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

NDK 学习进阶免费视频来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

这篇关于从Chrome小恐龙游戏学习2D游戏制作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/156865

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

线性代数|机器学习-P36在图中找聚类

文章目录 1. 常见图结构2. 谱聚类 感觉后面几节课的内容跨越太大,需要补充太多的知识点,教授讲得内容跨越较大,一般一节课的内容是书本上的一章节内容,所以看视频比较吃力,需要先预习课本内容后才能够很好的理解教授讲解的知识点。 1. 常见图结构 假设我们有如下图结构: Adjacency Matrix:行和列表示的是节点的位置,A[i,j]表示的第 i 个节点和第 j 个

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件