12、开源游戏-“胡子”地图绘制和游戏主循环设计

﹥>﹥吖頭↗ 提交于 2020-03-01 14:05:17

在前面中我们初始化了游戏的资源,这次我们来说下地图的绘制和游戏主循环设计。

地图绘制

    以前说过地图是用tiled画好,导出为图片形式的,所以地图的绘制,就是把这个图片绘制到canvas的过程。这样绘制地图就简单了,使用drawImage方法绘制即可。

    这里有个2问题,1是地图的大小一般肯定是大于canvas的,所以我们只是把地图的一部分绘制到了canvas上,2是地图的移动。1中的地图的复制位置是根据2中地图的移动距离来确定的。我们的思路如下:记录鼠标移动的xy坐标值,然后根据xy值和canvas边缘做比较,当靠近边缘时,我们就移动地图一段距离,重复这个过程,直到地图绘制完。

    其实我们上面的思路中,就是在改变drawImage方法的参数过程,那么来看下drawImage方法:

定义和用法

drawImage() 方法绘制一幅图像。

语法

drawImage(image, x, y)
drawImage(image, x, y, width, height)
drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight)
image

所要绘制的图像。这必须是表示 <img> 标记或者屏幕外图像的 Image 对象,或者是 Canvas 元素。

x, y
要绘制的图像的左上角的位置。
width, height
图像所应该绘制的尺寸。指定这些参数使得图像可以缩放。
sourceX, sourceY
图像将要被绘制的区域的左上角。这些整数参数用图像像素来度量。
sourceWidth, sourceHeight
图像所要绘制区域的大小,用图像像素表示。
destX, destY
所要绘制的图像区域的左上角的画布坐标。
destWidth, destHeight
图像区域所要绘制的画布大小。

描述

drawImage() 方法有 3 个变形。第一个变形把整个图像复制到画布,将其放置到指定点的左上角,并且将每个图像像素映射成画布坐标系统的一个单元。第二个变形也把整个图像复制到画布,但是允许您用画布单位来指定想要的图像的宽度和高度。第三个变形则是完全通用的,它允许您指定图像的任何矩形区域并复制它,对画布中的任何位置都可进行任何的缩放。

传递给 drawImage() 方法的图像必须是 Image 对象Canvas 元素。一个 Image 对象能够表示文档中的一个 <img> 标记或者使用 Image() 构造函数所创建的一个屏幕外图像。

再看一下这个图:

我们使用该方法的最后一种变形,代码如下:

game.bgContext.drawImage(game.taskMapImage,game.offsetX,game.offsetY,game.canvasWidth,game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);


我仔细说明一下:

  • bgContext是我们背景canvas的绘图环境,同样我们在初始化时将它保存了起来,这样我们就可以使用2d的绘图环境。代码如下:


game.bgCanvas = $('#bgcanvas');
game.bgContext = game.bgCanvas.getContext('2d');
  • game.taskMapImage为当前任务的地图,现在所有任务都是一个地图,而且也没有障碍物,我打算在做胡子第二版时用一个框架来实现这个障碍物碰撞检测
  • offsetX和offsetY,是指地图移动的偏移量,默认未移动时为0。当地图向左侧移动了20像素后,offsetX变为20,同理offsetY也是如此计算。这个20就是我们说的移动一段距离的值。这2个值决定了,我们从地图图片哪开始复制,结合他后面的2个参数,就是我从哪开始复制,复制多宽、多高的图片。
  • game.canvasWidth和game.canvasHeight为背景canvas的宽高,即复制地图中canvas宽高的地图。
  • 0,0为背景canvas左上角的坐标,即将地图图片,从这开始绘制。
  • game.canvasWidth,game.canvasHeight 绘制地图的大小,没说的肯定是背景canvas的宽高了。



那我们怎么判断是否需要移动地图呢?我们根据鼠标的xy位置和一个阀值做比较,即鼠标x值距离背景canvas的左右边界小于阀值像素时,我们移动一次地图,鼠标y同理。

阀值我们为10像素,每次移动20像素的地图距离。代码如下:

if(mouse.x<=10){
 if (game.offsetX>=20){
 game.offsetX -= 20; 
 }else if (game.offsetX>0){
 game.offsetX = 0; 
 
 }
} else if (mouse.x>= game.canvasWidth - 10){
 if (game.offsetX + game.canvasWidth + 20 <= game.currentMapImage.width){
 game.offsetX += 20;
 }else{
 game.offsetX += game.currentMapImage.width-(game.offsetX + game.canvasWidth);
 }
}

下面代码中,我们判断鼠标的x值,如果x距离左侧边界小于等于10时,判断是否已经移动了超过20像素,是我们就减去20,不足时我们恢复成0,同理我们判断鼠标x距离右侧边界时的情况。类似完成鼠标y的判断。

通过上面的代码,当面的条件满足时,我们改变offsetX和offsetY的值,然后即调用drawImage方法绘制新的地图,来实现地图的移动。


game.bgContext.drawImage(game.taskMapImage,game.offsetX,game.offsetY,game.canvasWidth,game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);


小节

我们使用鼠标的xy和背景canvas边界做比较,来判断是否需要移动地图,是的话就根据移动的距离,重新绘制地图。这里对鼠标移动的监听没有说明,主要使用jquery事件:


$(document).mousemove(function(e){
		mouse.x = e.pageX;
		mouse.y = e.pageY;
	});

这里还需要减去背景canvas相对左和顶部的距离(如果有的话),这个都不细说。在上面中我们还有一个问题,就是谁来触发这个判断动作,由鼠标事件来做?如果这样这个鼠标事件js就有点不单存了,我想用游戏的主循环触发这个判断,当然不是直接去做,那样更不好。


设计游戏主循环

    在前面时,我们提到过游戏的基本原理就是“绘制 擦除 绘制”循环它。我感觉这个很重要!这个循环控制着或说触发着游戏的一系列事件的产生执行。这个和flash好像啊,如果了解过flash的动画,应该知道弄flash动画,都弄很多小的影片剪辑(movieclip),然后将它们组合或放在主时间轴上,当主时间轴播放时,这些小的影片剪辑也在同时播放着自己的循环(这里可能不太恰当,因为主时间轴可以是停止的,呵呵)

    那我们可不可以这样想,所谓的游戏开发其实和开发网站、mis系统、bi软件(为啥说他呢,因为我是开发这个的,呵呵)这些系统是一样的,都是把系统分解成许多个小功能,然后组合其中一部分,或根本不用组合,就成了一个系统了。

    游戏也是,分解成许多个小功能,尽量模块话(一个稍复杂的游戏不这样弄,真难想象如何控制代码和维护),然后用主循环去组合或执行他们, 不论游戏还是系统,触发动作的一般都是人,如用户点击按钮登录,玩家使用技能,只不过系统中用户触发后一般就是立马执行,而游戏却不同,一般是由循环去执行他,或者说是在下一次循环中执行他。

    不知道我说的乱不乱,呵呵。我整理下我的思路:开发一个游戏时,把游戏分解成许多个小功能,然后分别实现他们,我想这里最难的就是如何分解和他们的关系了。(等胡子游戏第一版开发完时,我会发一下代码的类图,到时欢迎大家指出我们的错误和不足呵呵,)。然后主循环中调用它们或调用几个,然后这几个中又调用其他的(他们的关系)。

    以前觉得开发游戏时,不知道从何下手,现在我把它和我熟悉的bi开发做了对号或找不同,感觉不再像当初那么迷糊了。下面说主循环的代码设计。

代码设计

    按照前面的废话,主循环只有“绘制 擦除 绘制”就可以了,那就是绘制所有的游戏单元(建筑、车辆、人员),如果是一个没有动画的游戏,我想应该是的,但是胡子有动画,就是精灵图(其实这么说是不准确的,游戏里的一切都精灵),就是每个单元都有自己的一个小的动画循环,如车辆行走,转向和建筑的生产等,如下图的坦克和天电的发电图:

坦克行走图


天电 发电图

就目前来说我们的主循环要干2建事,1绘制所有的游戏单元,2绘制游戏单元的动画。代码如下:

$(window).load(function() {
    game.init();
});

var game = {
    init: function(){
        mouse.init();
	commandbar.init();
	sounds.init();
	data.init();

	...
    },
    start:function(){
	 commandbar.init();
        
	 ...
    },
    spiritLoop:function(){
		//调用所有游戏单元的动画方法
		for (var i = 0;i<game.items.length;i++){
			game.items[i].spiritAnimate();
		};
		setInterval(game.spiritLoop,100);
    },	
    drawLoop:function(){
		if (判断是否需要移动地图){
			game.bgContext.drawImage(game.taskMapImage,game.offsetX,game.offsetY,game.canvasWidth,game.canvasHeight, 0,0,game.canvasWidth,game.canvasHeight);
		}
		//调用所有游戏单元的绘制方法
		for (var i = 0;i<game.items.length;i++){
			game.items[i].draw();
		};
		requestAnimationFrame(game.drawLoop);							
    }
}

这里我们主要设计,不细说代码。

总结

我们按照游戏的原理,来设计代码的实现,就像我们开发web系统时按照系统设计来开发系统一样。下一次我们来说游戏单元的绘制,再上一次的总结(11中)中我们提到,使用继承的方式实现绘制。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!