上期我们只实现了小鸟飞行以及障碍物地图随机生成、绘制和滚动,这期我们要完成整个游戏的大体框架,即游戏运行的主体部分,而其他的比如计分,重新开始等不会去实现。文章最后会讲一些个人心得。

本期内容依旧是在微信小游戏上进行实现的。由于内容以及代码都承接以前文章,如果你没有阅读过,可以从这里开始。

本文不允许任何形式的转载!

阅读提示

本系列文章不适合以下人群阅读,如果你无意点开此文,请对号入座,以免浪费你宝贵的时间。

  • 想要学习利用游戏引擎开发游戏的朋友。本文不会涉及任何第三方游戏引擎。
  • 不具备面向对象编程经验的朋友。本文的语言主要是Javascript(ECMA 2016),如果你不具备JS编程经验倒也无妨,但没有面向对象程序语言经验就不好办了。

关注我的微信公众号,回复“源代码4”可获得本文示例代码下载地址,谢谢各位了!

最基础的碰撞判定

为什么要加上一个“最基础的”呢,因为二维图形碰撞(Collision)一般分为Bounds接触判定(可能会碰撞)和最终碰撞判定(多边形到底是否有接触),比如下图:

首先说下Bounds是什么。我们可以认为Bounds就是一个区域,通常我们都以一个矩形作为Bounds,但有时候可能会是其他形状。就上图而言,我们的Bounds是一个矩形,并且是和XY轴平行的,这种Bounds我们成为Axis-aligned bounding boxes,简称AABB(下文提到的Bounds都是这个AABB)。一般只需要知道这个矩形的左上角和右下角两个点的位置,就能通过计算得知两个Bounds是否接触。

通常来说我们称左上角为min,右下角为max,意为该Bounds最小的点和最大的点。而我们接下来用的是[left,top],[right,bottom]分别来表示最小点和最大点的坐标。

怎么判断两个Bounds没有接触呢:
设两个不同的Bounds A和B,如果其中一个的left比另外一个的right大,或者它的top比另一个的bottom大,则我们认为 A没有和B接触。反之,判断两个Bounds是否接触的代码如下:

function overlaps(boundsA,boundsB){if(boundsA.left > boundsB.right || boundsA.top > boundsB.bottom){return false;}if(boundsB.left > boundsA.right || boundsB.top > boundsA.bottom){return false;}return true;
}


简化一下上面代码:

function overlaps(boundsA, boundsB) {return (boundsA.left <= boundsB.right && boundsA.right >= boundsB.left&& boundsA.bottom >= boundsB.top && boundsA.top <= boundsB.bottom);}

我就问一下,你觉得哪段代码好?我选第二个。

回到我们的游戏中,为了简化,我们认为只要树干和小鸟的Bounds接触上,就算碰撞。
那小鸟和树干的Bounds怎么得到呢。

Figure类的第1次迭代

综上,我们知道了什么是Bounds,而且注意到,要判断两个Bounds是否接触,必要条件是这两个Bounds必须在同一个坐标系下。
我们设计的Figure类具有left,top,width和height,可以用这几个属性来表示所在区域,所以我们的Figure的Bounds的最小坐标点就是[left,top],最大坐标点就是[left+width,top+height],为了便于代码阅读和简化编码,我们给Figure加上两个属性right,bottom以及一个getBounds方法:

Figure.js : ...getBounds() {return {left: this.left, top: this.top,right: this.right, bottom: this.bottom};}get right() {return this.left + this.width;}get bottom() {return this.top + this.height;}...

如果你对Figure类第0次迭代这篇文章还有印象的话,就应该知道Figure的left和top是相对其父节点的坐标系的,所以Figure的bounds不应该这么简单地计算!比如一个Figure A的子Figure C和另一个Figure B中的子Figure D进行bounds碰撞测试,如果按照上面的代码得到的bounds来判断的话肯定是错了,坐标系都不同比较个卵。

所以我们说bounds首先是有一个相对性的,就我们的游戏而言,要判断小鸟和树桩是否碰撞,就应该获得它们相对于顶层Graph的bounds值,然后再进行判断。幸运的是小鸟和树桩(应该说是障碍地图)正好是Graph的子节点,所以上述代码正好得到它们相对于Graph的bounds,直接判断就好了。

除了相对坐标系的问题,Figure的bounds还应该要考虑到自身的旋转和拉伸,这都会影响到Bounds值,更幸运的是,我们目前这个游戏中小鸟和树桩都没有旋转和拉伸。如何计算旋转和拉伸后的Bounds我会在以后讲到变换矩阵的时候再说,先提示一下各位免得产生误会。

小鸟的Bounds还好说,就刚才代码即可获得,但是TileMap的怎么办?它的Bounds是整个地图的区域,跟树干没关系呀,这里我们就只能是特例特办了,通过TileMap的地图数据来计算地图中所有树干的Bounds。
先看下之前TileMap的drawSelf方法:

drawSelf(ctx) {let imageManager = ImageManager.getInstance();let trunk1 = imageManager.getImage('trunk1');let trunk2 = imageManager.getImage('trunk2');let trunk3 = imageManager.getImage('trunk3');let startX = 0;let startY = 0;// 一列一列画:for (let i = 0; i < this.mapData.mapData.length; i++) {startY = 0;//每一列的y轴坐标都是从0开始// 先计算出每一列绘制的起始x轴的值:if (i != 0) {// 从第二列开始就要计算了,第一列是0startX += this.trunkWidth;// 第n列比第n-1列绝对多出一个宽度// 再加上它们之间的间隔:startX += this.mapData.spaceData[i-1] * this.trunkWidth;}// 某一列的地图数据:let datas = this.mapData.mapData[i];for (let j = 0; j < datas.length; j++) {let data = datas[j];let image = undefined;switch (data) {case 1:image = trunk2;break;case 2:image = trunk3;break;case 3:image = trunk1;break;}// 如果image为空,即地图数据为0,此处是个空白处就不需要绘制if (image) {ctx.drawImage(image, startX, startY, this.trunkWidth, this.trunkHeight);}// 画好一个就往下y坐标往下移动一个树桩高度startY += this.trunkHeight;}}}

它是通过地图数据在drawSelf中计算出树桩的位置和类型,然后绘制上去。正好,我们的树桩bounds计算跟这段代码几乎一样,那我们就可以利用这段代码来计算出树桩bounds了。
为了不重复计算,我们可以在生成地图数据的时候就计算出树桩的bounds数据,然后在绘制的时候直接使用即可,无需二次计算。所以我更改了TileMap的代码,你可以拿去和以前的比较一下:

import Figure from "./Figure";
import MapGenerator from "./MapGenerator";
import ImageManager from "./utils/ImageManager";export default class TileMap extends Figure {constructor(p) {super(p);this.mapData = undefined;this.trunkHeight = 0; // 单个树桩的高度this.trunkWidth = 0; // 单个树桩的宽度this.column = 0;this.row = 0;this.boundsArray = []; // 地图中树桩的Bounds数据数组}initMap() {if (this.mapData != undefined) {MapGenerator.generateMapData(this.mapData);} else {this.mapData = MapGenerator.generateMapData(this.mapData, this.column, this.row);}// 根据map数据来设置Map的高度和宽度:this.height = this.mapData.row * this.trunkHeight;this.width = this.mapData.column * this.trunkWidth;// 还要加上每列树桩之间的间隔:for (let i = 0; i < this.mapData.spaceData.length; i++) {this.width += this.mapData.spaceData[i] * this.trunkWidth;}// 生成树桩的bounds数据:this.calculateTrunkBounds();}calculateTrunkBounds() {let boundsArray = this.boundsArray;boundsArray.length = 0;// 先清空let startX = 0;let startY = 0;for (let i = 0; i < this.mapData.mapData.length; i++) {startY = 0;//每一列的y轴坐标都是从0开始// 先计算出每一列绘制的起始x轴的值:if (i != 0) {// 从第二列开始就要计算了,第一列是0startX += this.trunkWidth;// 第n列比第n-1列绝对多出一个宽度// 再加上它们之间的间隔:startX += this.mapData.spaceData[i - 1] * this.trunkWidth;}// 某一列的地图数据:let datas = this.mapData.mapData[i];for (let j = 0; j < datas.length; j++) {let data = datas[j];if (data != 0) {// 跳过缺口区域, bounds里还多了一个data属性// 这是为了绘制的时候不必再从mapData里取数据判断树桩image了boundsArray.push({left: startX,top: startY,right: startX + this.trunkWidth,bottom: startY + this.trunkHeight,data: data});}// y坐标往下移动一个树桩高度startY += this.trunkHeight;}}}getTrunkBoundsArray() {return this.boundsArray;}drawSelf(ctx) {let imageManager = ImageManager.getInstance();let trunk1 = imageManager.getImage('trunk1');let trunk2 = imageManager.getImage('trunk2');let trunk3 = imageManager.getImage('trunk3');let trunkDatas = this.getTrunkBoundsArray();for(let i = 0 ; i < trunkDatas.length;i++){let trunkData = trunkDatas[i];let image = undefined;switch (trunkData.data) {case 1:image = trunk2;break;case 2:image = trunk3;break;case 3:image = trunk1;break;}if (image) {ctx.drawImage(image, trunkData.left, trunkData.top, this.trunkWidth, this.trunkHeight);}}}
}

新的TileMap类中增加了一个calculateTrunkBounds方法,和一个getTrunkBoundsArray方法。在我们生成地图的时候就开始计算Bounds,然后绘制的时候直接就使用了。

请注意!!,树桩的Bounds目前并不是相对Graph的,而是相对于其所在地图的,这是因为计算bounds的方法只在生成地图的时候被调用,而这时候的地图坐标和需要计算碰撞时候的坐标是不同的,所以只能先算出树桩相对地图的bounds,然后再在需要进行碰撞测试的时候加上地图的left和top来获取其相对Graph的bounds。

便于测试,我把小鸟和树桩的bounds都绘制成了红色边框,得到如下结果:

注意看,小鸟的bounds实际上有点过大了,因为整个小鸟并没有占满整个Figure的区域,所以在测试碰撞的时候我们要可以缩小一下他的Bounds区域,不然小鸟背部根本没接触到树干还判定为碰撞就不好了。比如这么大,我觉得就挺合适了。

有一种碰撞判定叫做 像素碰撞,即查看两个图片数据中是否有不全透明的像素点重合,我觉得好麻烦啊,这种判断难道不会很慢吗

最后我们就需要在BirdFlyGame里加入碰撞判定代码,一旦碰撞上游戏就结束:

BirdFlyGame.js部分代码:
....afterRefresh(refreshCount) {if(!this.gameOver){this.collisionTest();}}collisionTest() {// 小鸟bounds:let birdBounds = this.bird.getBounds();let scaleW = this.bird.width * 0.2;let scaleH = this.bird.height * 0.2;// 让bounds缩小点let a = {left: birdBounds.left + scaleW,top: birdBounds.top + scaleH,right: birdBounds.left + (this.bird.width - scaleW),bottom: birdBounds.top + (this.bird.height - scaleH)};let that = this;collisionTrunkTest(this.firstMap);collisionTrunkTest(this.secondMap);function collisionTrunkTest(map){let firstBounds = map.getTrunkBoundsArray();for (let i = 0; i < firstBounds.length; i++) {let bounds = firstBounds[i];// bounds是相对地图的,需要加上它在Graph的位置// 以此来获得树桩相对于Graph的Boundslet b = {left: bounds.left + map.left,top: bounds.top + map.top,right: bounds.right + map.left,bottom: bounds.bottom + map.top};if(BirdFlyGame.overlaps(a,b)){that.processGameOver();return;}}}}processGameOver(){this.gameOver = true;this.gameStop();console.log('game over');}static overlaps(bounds1, bounds2) {let a = bounds1;let b = bounds2;return (a.left <= b.right && a.right >= b.left&& a.bottom >= b.top && a.top <= b.bottom);}

下面是测试结果,一旦小鸟的红色框碰到树干的,游戏就停止了。

我们就可以在processGameOver里做一些操作,比如显示飞行距离、时间、重来一次按钮等。

性能优化

1.去掉不必要的绘制

如果Figure没有在Graph的显示范围内(即屏幕里面),其实是没必要绘制的。
而我们的地图却不是。第一张地图和第二张地图同时往左移动,在第二张地图实际上还没有进入到Graph的区域的时候,我们却依然在绘制它,并且TileMap类还是一个要绘制大量图片的类,这就让效率降低了不少。

如果我们提前判断出Figure不在父节点的区域内的话,就不要绘制它。所以我们可以在Figure类的drawChildren方法中进行一次Bounds碰撞判断,发现子Figure没有和父Figure所在显示区域接触,就不绘制:

   drawChildren(ctx) {for (let i = 0; i < this.children.length; i++) {let childFigure = this.children[i];// bounds碰撞判定基于Figure坐标系// 所以Figure显示区域的bounds左上角就是[0,0]let displayBounds = {left:0,top:0,right:this.width,bottom:this.height};if(!Utils.overlaps(displayBounds,childFigure.getBounds())){continue;}childFigure.draw(ctx);}}

这个解决了整个Figure在父节点区域外会进行绘制的问题。不过TileMap类却要绘制大量树桩,而且有很多树桩都是在显示区域外的,就如上图所示,蓝色背景的地图中,有一部分还是在屏幕外的。
这个也好解决,我们在TileMap的drawSelf中,通过树桩的Bounds来跟Graph的显示区域Bounds做碰撞测试,没有接触的就不绘制:

drawSelf(ctx) {let imageManager = ImageManager.getInstance();let trunk1 = imageManager.getImage('trunk1');let trunk2 = imageManager.getImage('trunk2');let trunk3 = imageManager.getImage('trunk3');let trunkDatas = this.getTrunkBoundsArray();let graph = this.getGraph();let graphDisplayBounds = {left: 0, top: 0,right: graph.width,bottom: graph.height};for (let i = 0; i < trunkDatas.length; i++) {let trunkData = trunkDatas[i];let image = undefined;// 找到树桩对应graph坐标系的Boundslet absoluteBounds = {left: trunkData.left + this.left, top: trunkData.top + this.top,right: trunkData.right + this.left, bottom: trunkData.bottom + this.top};// 不在显示区域就跳过这一个(如果像之前是个二维数组就好了,直接可以跳过一列)if(!Utils.overlaps(absoluteBounds,graphDisplayBounds)){continue;}switch (trunkData.data) {case 1:image = trunk2;break;case 2:image = trunk3;break;case 3:image = trunk1;break;}if (image) {ctx.drawImage(image, trunkData.left, trunkData.top,this.trunkWidth, this.trunkHeight);}}}

不要小看只是少画了一些图片,性能会提升很多的。再次提醒各位,用CanvasRenderingContext2D绘图是很慢的,能改进的地方就改进,提升一点是一点,否则一旦把程序搬到移动设备上就会卡得出奇。

下图是性能测试的结果,CPU 2 x slowdown,大约录值了6秒,不太准。

2.取消不必要的碰撞测试

在测试小鸟和树干碰撞的时候,我们的代码如下:

collisionTrunkTest(this.firstMap);
collisionTrunkTest(this.secondMap);

是将两张地图的所有树桩Bounds和小鸟的Bounds进行测试,就如上面优化绘制遇到的问题一样,很多树桩位置根本就没进入屏幕,这种碰撞测试就是无意义的。
更进一步来说,如果地图没有进入到小鸟可能在的位置区域,就没必要进行测试,如下图所示:

如果还要细分的话就需要改造之前生成的树桩Bounds数据结构,目前我把所有树桩的Bounds放到了一个数组中,可以把它改成一个二维数据结构:

之前存放的数据是这样的:
一维数组,每个数据表示树桩的Bounds(data是之前为了判断树桩类型保留下来的一个属性):
[{left:数字,top:数字,right:数字,bottom:数字,data:数字} , ........]
可以改造成:
[..........// 某一列树桩bounds数据:{trunkBounds : [{left,top,right,bottom,data} , ........]}..........
]

目前测试方法是遍历一维数组。改造后虽然也要遍历一遍,但是由于分成了“列”,如果某列第一个不在测试范围,则整列就跳过。这里我只提一下,就不改造原来的数据结构了。
此外,如果某个树桩没有进入小鸟碰撞区域,并且在该区域右侧,那么这个树桩往后的所有树桩都没有进入该区域,不需要测试碰撞:

BirdFlyGame.js ,修改碰撞测试方法:
collisionTest() {// 小鸟bounds:let birdBounds = this.bird.getBounds();let scaleW = this.bird.width * 0.2;let scaleH = this.bird.height * 0.2;// 让bounds缩小点let a = {left: birdBounds.left + scaleW,top: birdBounds.top + scaleH,right: birdBounds.left + (this.bird.width - scaleW),bottom: birdBounds.top + (this.bird.height - scaleH)};let that = this;// 小鸟可能出现的区域boundslet birdDisplayBounds = {left : a.left,right:a.right,top:0,bottom:this.height};collisionTrunkTest(this.firstMap);collisionTrunkTest(this.secondMap);function collisionTrunkTest(map){if(!Utils.overlaps(map.getBounds(),birdDisplayBounds)){// 如果地图没有进入小鸟出现区域,则不测试return;}let firstBounds = map.getTrunkBoundsArray();for (let i = 0; i < firstBounds.length; i++) {let bounds = firstBounds[i];// bounds是相对地图的,需要加上它在Graph的位置// 以此来获得树桩相对于Graph的Boundslet b = {left: bounds.left + map.left,top: bounds.top + map.top,right: bounds.right + map.left,bottom: bounds.bottom + map.top};if(Utils.overlaps(a,b)){that.processGameOver();return;}else{// 树桩和小鸟的bounds没有接触,看一下该bounds是否在小鸟bounds的右侧// 如果是,则跳过剩余树桩的碰撞测试if( b.left > a.right ) {break;}}}}}

上面我提到,如果树桩bounds在小鸟右侧并没接触上则跳过剩余的树桩。那如果树桩bounds如果在小鸟左侧也没接触上,那该树桩之前的树桩也是没必要测试碰撞的,但是我们是遍历一个数组,逐个进行比较,且数组里的Bounds数据的left是从左至右的(从小到大),所以刚才说的那个就不好实现。

小结

我们的游戏主体框架就已经全部完成了,即小鸟可以控制飞行,地图可能随机生成并能滚轴移动,可以进行碰撞测试等,并且我们还进行了一些优化,游戏其余的部分:加入音效和背景音乐,游戏结束后计分,游戏开始的界面,游戏结束的界面,这些的工作量其实不比之前做游戏主框架少,甚至更多,所以我就不一一讲述了,毕竟我很懒。
至此《老脸教你做游戏》就告一段落了(实际上我想写的是《老脸教你做游戏引擎》,后来我想了想,还是要点脸吧)。
如果有下期,那应该会是讲质点运动模拟,有缘的话我们会再见的。

再次感谢您的阅读!

如果你已经强打精神看到此处,说明我们有缘。我怕以后不更新blog没机会写,所以在最后我讲讲一些我觉得挺有用的东西。

图形区域划分

用过数据库吗?没用过没关系。数据库可以建立索引,便于快速定位到某个数据。
我们在未优化测试碰撞的方法中,采用了全局遍历,即用图形A和所有其他图形进行碰撞测试,这是很慢的。如果我们能快速定位到图形A和它附近的图形的话,就没必要进行全局测试了。
所以几乎所有游戏引擎都会对图形进行区域划分,就拿目前的游戏为例:

我们可以把整个屏幕划分成n个区域,每当有新的图形加入就计算出该图形的Bounds在哪个区域,并把该图形放到区域里图形数组里,如果图形发生变换(移动、缩放、旋转),重新计算图形所在区域并更新,这就相当于数据库里建立索引。写段伪代码:


function onNewFigureAdded(figure){updateFigureArea(figure);
}function onFigureTransformChanged(figure){updateFigureArea(figure);
}function updateFigureArea(figure){// 得到figure对应区域数组let areas = findFigureAreas(figure);// 清空figure维护的area数组while(figure.areaArray.length !=0){let areaId = figure.areaArray.pop();let area = getArea(areaId);area.remove(figure.id);}figure.areaArray.length = 0;// 清空figure所在区域id数组// 重新设置一遍while(areas.length !=0){let area = areas.pop();figure.areaArray.push(area.id);area.add(figure.id);}
}

伪代码,不要较真,是这个意思就可以了

这样一来,如果我想知道小鸟所在区域有哪些些的图形,就可以这样:

let otherFigures = [];
for(let i = 0 ; i < bird.areaArray.length; i++){let area = getArea(bird.areaArray[i]);copyAll(otherFigures,area.figures);
}

比如想要进行碰撞测试,就不再需要全局比较了,从小鸟所在区域内取出其他图形和小鸟Bounds进行测试即可。
上面例子里的区域是一个固定网格形状的,这种区域划分叫做全局网格(Uniform Grid),是二维中较为简单的区域划分方式,二维游戏引擎常用区域划分还有四叉树(Quadtrees)等,三维的就有BSD等。不管怎么去划分吧,目的就一个,快速定位图形,减少不必要的操作,我们所讲的这个游戏碰撞测试还是很简单的,如果是复杂的碰撞测试,那就会花费很多时间。一帧16毫秒是特别宝贵的,不要浪费在一些无谓的操作上。
当然,选择哪种划分方式还要看区域更新复不复杂,太复杂了也不行,本末倒置了。
下图是我做的刚体运动模拟的网格区域划分测试:

devicePixelRatio

devicePixelRatio顾名思义,设备分辨比。
这个是DOM中windows对象的一个属性,微信小游戏里是通过SystemInfo获取的:

let systemInfo = wx.getSystemInfoSync();
let scale = systemInfo.pixelRatio;

比如你绘制一个图片在canvas上,你可能会发现在手机上比PC上模糊(像素化),这是因为devicePixelRatio的值不为1。
这个属性值的意思是说,当前显示设备的物理像素分辨率和CSS像素分辨率的比值,可以这么理解:一个CSS像素等于多少个物理像素大小,即需要多少个当前设备屏幕像素绘制一个CSS像素。
假设devicePixedlRatio为3.5的话,你绘制出来的图片在设备上显示跟实际屏幕像素就差了3.5倍,所以看着就模糊了。
所以在你用canvas绘制之前需要通过devicePixelRatio的值重新设置canvas在内存里大小:

let scale = windows.devicePixelRatio;
canvas.width *= scale;
cavans.height *= scale;

让canvas的内存大小变成之前的devicePixeloRatio倍,而且canvas的显示大小不要改变:

let scale = windows.devicePixelRatio;
canvas.style.width = canvas.width;// 微信小游戏没有style
cavnas.style.height = canvas.height;
canvas.width *= scale;
cavans.height *= scale;

而通常我们写代码的时候图形的大小都是根据canvas大小来设定的,而且是在canvas内存大小改变之前,所以如果你在代码中一直沿用了为改变内存大小的canvas长宽,那就还需要将context放大相应的倍数:

// 之前这么写的
let myFigure.width = canvas.width;
myFigure.draw();
.....
// 后来才改的内存大小
let scale = windows.devicePixelRatio;
canvas.style.width = canvas.width; // 微信小游戏没有style
cavnas.style.height = canvas.height;
canvas.width *= scale;
cavans.height *= scale;
// 那就让ctx放大scale倍
let ctx = canvas.getContext('2d');
ctx.scale(scale,scale);

这样就不会出现模糊的情况了。

图片尽量放在一起

我们绘制的图片经常都是一张一张的,实际上这样不好,尽量把他们都合到一张图片上,指定好source bounds然后绘制。
这样做一方面是为了节约空间,另一方面是为了方便加载,另外我在做webgl的时候,为了统一绘制,会将多个图片合并到一个texture中,但是不管怎么做都无法让这些图片铺满一个大的空间的,往往会生成多个texture(这也跟我的算法有关)。如果一开始要绘制的图片就是一个大的图片,就不需要来回切换了,一次性就能绘制成功。

建立对象池

如果游戏程序中会频繁生成新的图形对象,请建一个对象池,便于复用。
好多人说对象池的目的是为了减少创建对象时的开销,个人认为这是一方面。另外一方面是为了给GC省时间,太多的对象需要清扫的话GC花的时间很长,以至于出现游戏玩着玩着突然卡一下。

context 2d的clip方法少用为妙

这个方法我再web上倒没有发现问题,可是在微信小游戏里出现了性能爆降的情况。我觉得是微信小游戏的bug,于是在他们论坛提交了,不过过了很久都无人回复,我觉得很有可能是我自己代码烂造成的,于是就默默把提交的bug删了。
这是代码片段导入链接,试试在你的手机上卡不卡:
https://developers.weixin.qq.com/s/AQNhhimW7A4s

context 2d在移动端只适合简单的图形和游戏

我之前做了个打泡泡的微信小游戏,不包括背景图片,里面至少要绘制50个泡泡,而且还会动,起初在模拟器上运行得还好,而且在我的手机上运行也没问题;后来我把这个游戏移植到了web端,一到手机端FPS只有20,我忽略了一点,微信小游戏不是运行在浏览器上的,跟普通的web页面不是一回事。于是我不得不把整个游戏的绘制部分改成了webgl,花了一个月的时候才调试好。

在此提醒一下各位,如果你的程序主要是在移动端运行,开发的时候在模拟器(我是在Chrome上运行)上先将CPU降低到手机的标准,不然会误导各位,还以为程序运行得挺流畅,实际上卡成鬼。

所以说,如果只会画个报表啦,做个Diagram程序啦,canvas2d就够用了,但要想在手机端做出渲染操作比较多而且复杂的游戏(比如3d效果的),用canvas2d是不行的,请转投webgl。

游戏不分贵贱

游戏就是一个程序而已,有的凸出的是画面,有的是在音乐上下了大工夫,而有的却是在游戏剧情安排上很吸引人,各有各的受众。
有些人就喜欢绝地求生,我就不喜欢,而我很爱玩连连看,却被别人笑话幼稚。玩家即是如此,何况开发者:你用的Unreal引擎开发的3d游戏,我用的原生js开发的2d游戏,你的就要高一档次?
游戏不分贵贱的,只有喜欢和不喜欢

【老脸教你做游戏】小鸟飞过障碍物的游戏(下)相关推荐

  1. 【老脸教你做游戏】小鸟飞过障碍物的游戏(上)

    摘要 我们已经从最基础的画线填充.cavans2d context状态变换,做出了绘制封装类(Figure)以及动画类(Animation),就目前而言,这几个简单的类已经可以做简单的游戏了.这期就做 ...

  2. 手把手教你做一个Java贪吃蛇小游戏

    大家好,我是孙不坚1208,这篇博客给大家分享一下:如何做一个贪吃蛇小游戏(Java版)的exe应用程序,希望能给需要帮助的朋友带来方便. 手把手教你做一个Java贪吃蛇小游戏的exe应用程序 一.J ...

  3. C语言跳过障碍物小游戏,html5飞翔的小鸟越过障碍物小游戏代码

    一款简单好玩的html5 canvas飞翔的小鸟越过障碍物小游戏代码,玩法:点击鼠标控制小鸟飞行. 查看演示 下载资源: 11 次 下载资源 下载积分: 30 积分 js代码 var cvs = do ...

  4. unity2D小鸟飞过柱子小游戏

    (1)启动unity应用程序,在弹出的对话框里单击New project 按钮 (2)我们要得到的效果 (3)首先用户在玩这个游戏的时候小鸟是不断向前飞的,我们知道unity中用户眼睛看到的界面就是摄 ...

  5. 【老脸教你做游戏】从Canvas开始

    本文不允许任何形式的转载! 阅读提示 本系列文章不适合以下人群阅读,如果你无意点开此文,请对号入座,以免浪费你宝贵的时间. 想要学习利用游戏引擎开发游戏的朋友.本文不会涉及任何第三方游戏引擎. 不具备 ...

  6. JavaScript应用——手把手教你做一个页面化猜数字游戏

    一听到猜数字游戏,想必大家都不太陌生吧?是的没错,很多人都用C语言或者Java写过猜数字游戏小程序,博主也不例外,之前写过C语言版本的猜数字游戏,感兴趣的同学可以看看C语言版本猜数字游戏.本篇博客主要 ...

  7. python手机版做小游戏代码大全-Python大牛手把手教你做一个小游戏,萌新福利!...

    原标题:Python大牛手把手教你做一个小游戏,萌新福利! 引言 最近python语言大火,除了在科学计算领域python有用武之地之外,在游戏.后台等方面,python也大放异彩,本篇博文将按照正规 ...

  8. (完结)Unity游戏开发——新发教你做游戏(七):Animator控制角色动画播放

    文章目录 一.前言 二.Animator状态机组织 三.过渡条件 四.用代码设置过渡条件,触发动画播放 五.CharacterAniLogic.cs完整代码 一.前言 文章列表 Unity游戏开发-- ...

  9. Unity游戏开发——新发教你做游戏(三):3种资源加载方式

    文章目录 一.前言 二.Unity的目录结构规范 1.Resources(不是很推荐把资源放这个目录) 2.RawAssets(存放生资源) 3.GameRes(存放熟资源) 4.StreamingA ...

最新文章

  1. 并发编程之 锁的优化有哪些
  2. linux vnc的小黑点和鼠标不同步_vnc连接windows,推荐三款非常好用的vnc连接windows软件...
  3. 请求时的编码问题 Use body.encode(‘utf-8‘) if you want to send it encoded in UTF-8
  4. prooerties mysql_MySQL_第八章
  5. UbuntuServer16.04LTS中安装Mysql并配置远程访问
  6. 英语语法---主语详解
  7. Linux(CentOS 7)——阿里云 云服务器 ECS上Apache服务器安装与配置
  8. 说说C#的async和await
  9. S3C6410 时钟初始化
  10. java string 数组 个数,Java - 定义一个接收String的方法,并返回一个整数数组,其中包含每个元音的数量...
  11. Android架构MVC,MVP与MVVM及MVPVM对比分析
  12. STM32 rtc唤醒 低功耗待机模式 避免iwdog看门狗方案
  13. 企业高薪招人,近5成岗位月薪过万
  14. 被问:这个BUG为什么没测出来?该如何回答
  15. 基于单片机的超市储物柜设计_基于单片机的新型智能储物柜设计
  16. 吃货联盟订餐系统项目实践
  17. 阿尔茨海默最新研究进展(2022年12月)
  18. 每日一课 | range()函数用法
  19. C/C++数据结构课程设计安排
  20. python 英语分词_NLTK(一):英文分词分句

热门文章

  1. Calling a method in the system process without a qualified user
  2. 微软鼠标测试软件,第一款win8鼠标:微软Sculpt全球首测
  3. 关键词布局排名优化方法
  4. 永洪Bi架构部署与集群部署
  5. YUV420P 和 YUV420SP 理论与画框
  6. [Linux]工作中常用的Unix命令
  7. python从键盘上输入10个整数_从键盘为一维整型数组输入10个整数
  8. 视频弹窗播放html,利用jQuery实现在线视频弹出播放代码
  9. ubantu下相关的应用软件的集合
  10. 2023年湖北建设厅七大员建筑八大员考试什么时候报名?甘建二