12、开源游戏-“胡子”地图绘制和游戏主循环设计
在前面中我们初始化了游戏的资源,这次我们来说下地图的绘制和游戏主循环设计。地图绘制
以前说过地图是用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.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.draw();
};
requestAnimationFrame(game.drawLoop);
}
}这里我们主要设计,不细说代码。
总结
我们按照游戏的原理,来设计代码的实现,就像我们开发web系统时按照系统设计来开发系统一样。下一次我们来说游戏单元的绘制,再上一次的总结(11中)中我们提到,使用继承的方式实现绘制。
文档来源:开源中国社区https://my.oschina.net/eternal/blog/220110
页:
[1]