我们将创建一个游戏我将他称之为Space。这是一个简单的游戏,你可以使用键盘操作一个二维地图的恒星和行星。为了简便起见,完整的脚本将在这篇文章的底部,我将从脚本片段解释这些到底是什么。
游戏“循环”
游戏开发的最重要的元素是游戏”循环。 “从本质上讲,只要游戏一直持续,这是一个将不断重复的函数。 我们的游戏循环就像我们在前面的帖子里使用的动画循环,附加一些关键的补充。 这里没有太多的细节,游戏循环脚本看起来是这样的:
<canvas id="space" width="400" height="300"></canvas>
// game function game(){ // configuration var game = this; game.canvas = document.getElementById('space'); game.ctx = game.canvas.getContext('2d'); game.time = false; // initialize game.init = function(){ // start the game loop game.loop(); }; // game loop game.loop = function(){ // timing var now = new Date().getTime(); var d = now - (game.time || now); game.time = now; // update positions, view, etc. game.update(d); // render game.render(d); // request next frame requestAnimationFrame(game.loop); }; // update game game.update = function(d){ // 1. update player position // 2. update view }; // render game game.render = function(d){ // 1. clear the canvas // 2. draw background // 3. draw stars, planets // 4. draw player }; } // begin game var space = new game(); space.init();
有了这个基本结构之后,我们开始添加更具体的功能到脚本来处理动作,动画和玩家互动。
游戏视图
第一个挑战是创建一个可滚动的地图,玩家可以在上面移动。 我们的地图将包含对象比如行星和恒星,但并不是每个星星都将在任何给定的时间可见。 只有那些接近的玩家的星星将在在canvas上绘制。 这是通过创建一个游戏视图来实现的 。 因为这是一个二维游戏,我们可以考虑根据游戏视图的 x
和 Y
轴。 它看上去是这样的:
正如你所看到的,星星1和2在游戏视图是可见的,但是星星3不可见。 当玩家在地图上 x
和 Y
轴移动,视图将基于球员的位置显示新地图更新内容。在我们的脚本中,我们可以通过将下面的代码增加到game()函数来创建游戏试图。
// configuration game.width = 0; game.height = 0; game.view = { x: 0, y: 0 }; // resize canvas game.resize = function(){ game.ctx.canvas.width = game.canvas.width; game.ctx.canvas.height = game.canvas.width; game.width = game.canvas.width; game.height = game.canvas.width; };
结合使用的 game.width
, game.height
, game.view.x
, game.view.y
,我们可以跟踪我们的试图,总是在游戏canvas上绘制正确的对象。 当我们将运动和对象添加到我们的地图, 每次渲染一个对象我们将会参考我们的游戏视图。
恒星、行星,飞船,哦我的天
基本动画和视图结构,是时候用恒星,行星,和飞船只来填充我们的游戏世界,最后构成一个游戏。 最好是创建一个通用的 实体 对象,我们可以使用它来将所有对象放在我们的地图。 我们的实体将为每个映射对象存储信息,包括它的位置( x
和 Y
),大小( 宽度
和 高度
)、方位和运动矢量。
// translate coordinates
game.translate = function(x, y){ return { x: (game.width / 2) - game.view.x + x, y: (game.height / 2) - game.view.y + y } }; // entity game.entity = function(options){ // settings var entity = this; entity.settings = { x: 0, // x position y: 0, // y position w: 20, // width h: 20, // height o: 0, // orientation v: { x: 0, y: 0 }, // vector f: 0.0004, // friction speed: 1, color: { red: 0, green: 0, blue: 0, alpha: 0 } }; entity.settings = merge(entity.settings, options); // update entity.update = function(d){ // update position entity.settings.x += (entity.settings.v.x / 10) * d; entity.settings.y += (entity.settings.v.y / 10) * d; // friction entity.settings.v.x -= entity.settings.v.x * entity.settings.f * d; entity.settings.v.y -= entity.settings.v.y * entity.settings.f * d; }; // draw entity.draw = function(d){ // only draw when in view if(entity.settings.x - (entity.settings.w / 2) <= game.view.x + (game.width / 2) && entity.settings.x + (entity.settings.w / 2) >= game.view.x - (game.width / 2) && entity.settings.y - (entity.settings.h / 2) <= game.view.y + (game.height / 2) && entity.settings.y + (entity.settings.h / 2) >= game.view.y - (game.height / 2)){ // get translated coordinates var t = game.translate(entity.settings.x, entity.settings.y); // orientation game.ctx.save(); game.ctx.translate(t.x, t.y); game.ctx.rotate(entity.settings.o * Math.PI / 180); // color game.ctx.fillStyle = 'rgba(' + entity.settings.color.red + ', ' + entity.settings.color.green + ', ' + entity.settings.color.blue + ', ' + entity.settings.color.alpha + ')'; // draw entity game.ctx.beginPath(); game.ctx.rect(0 - (entity.settings.w / 2), 0 - (entity.settings.h / 2), entity.settings.w, entity.settings.h); game.ctx.fill(); // reset orientation game.ctx.restore(); } }; }; // create player game.player = new game.entity({ x: 0, y: 0, w: 32, h: 40 }); // create a star at a random location var star = new space.entity({ x: Math.floor(Math.random()*5000)*(Math.round(Math.random())*2-1), y: Math.floor(Math.random()*5000)*(Math.round(Math.random())*2-1), w: 5, h: 5 });
实体
对象将存储所有我们需要在游戏地图上绘制对象的信息。 实体将使用 V
(向量)属性支持位置,方向,甚至运动。 我也引进了 friction
属性他会随着时间慢慢减少运动矢量,所以当玩家在地图上移动他们不会无限制的漂走下去。
在正确的位置画游戏实体,我们使用 translate()
函数
转换我们的map-relative的 x
和 Y
坐标到view-relative坐标。 使用这种结合 if
声明,检查实体是否在当前的“视图”,我们可以在只有当他们出现在球员附近时绘制我们的游戏对象。
图像和动画
目前,我们的游戏实体只能在我们的游戏地图渲染成简单的矩形。取而代之, 我们想用图片甚至动画给我们的游戏内容带来活力。 我们可以使用 动画精灵将这种支持添加到游戏实体 ,它是用于游戏开发的一种普遍的技术。 我们的游戏对象将使用如下图的图片渲染。
这个单一映像都包含 组成对象不同状态的帧 。 从左到右,第一帧是我们飞船的 静止状态,紧随其后的是两个 向前的推力 帧,然后是两个 逆冲断层 帧,最后两个紧随其后 涡轮增压 帧。 我们可以通过添加支持这些精灵动画改善我们的脚本。
// load images game.resources = []; game.load = function(images){ // load image from url var loadFromUrl = function(url){ var img = new Image(); img.src = '/path/to/images/' + url + '.png'; game.resources[url] = { image: img, loaded: false }; img.onload = function(){ game.resources[url].loaded = true; }; }; // accept array or single resource if(images instanceof Array){ for(var i = 0; i < images.length; i++){ loadFromUrl(images[i]); } } else{ loadFromUrl(images); } }; // sprites game.sprite = function(options){ // settings var sprite = this; sprite.settings = { image: false, alpha: 1, x: 0, y: 0, w: 0, h: 0, speed: 0.02, // .001 = 1 frame/second frames: [], index: 0, dir: 'horizontal', loop: true }; sprite.settings = merge(sprite.settings, options); // update sprite.update = function(d){ sprite.settings.index += sprite.settings.speed * d; }; // draw sprite.draw = function(x, y, w, h){ // determine which frame to draw var frame = 0; if(sprite.settings.speed > 0){ var max = sprite.settings.frames.length; var idx = Math.floor(sprite.settings.index); frame = sprite.settings.frames[idx % max]; if(!sprite.settings.loop && idx > max){ var frame = sprite.settings.frames[sprite.settings.frames.length - 1]; } } // set new position if(sprite.settings.dir == 'vertical'){ sprite.settings.y = frame * sprite.settings.h; } else{ sprite.settings.x = frame * sprite.settings.w; } // render game.ctx.drawImage(sprite.settings.image, sprite.settings.x, sprite.settings.y, sprite.settings.w, sprite.settings.h, x, y, w, h); }; }; // load images game.load([ 'ship', 'star-small', 'star-large' ]); // add sprites to player game.player.settings.sprites['rest'] = new game.sprite({ image: game.resources['ship'].image, w: 32, h: 40, frames: [0], speed: 0 }); game.player.settings.sprites['forward'] = new game.sprite({ image: game.resources['ship'].image, w: 32, h: 40, frames: [1, 2] }); game.player.settings.sprites['reverse'] = new game.sprite({ image: game.resources['ship'].image, w: 32, h: 40, frames: [3, 4] }); game.player.settings.sprites['boost'] = new game.sprite({ image: game.resources['ship'].image, w: 32, h: 40, frames: [5, 6] });
game.sprite()
对象允许我们快速指定sprite图像的宽度、高度、速度和方向。 使用这个函数,我们可以扩展我们的 实体
()
对象包括支持sprite图像。 game.load()
函数处理图像加载,这样我们可以在游戏开始之前预加载图片。
键盘交互
最后给我们的游戏添加键盘交互脚本。 随着游戏开发, 独立跟踪keyup
和 keyDown
事件变得非常容易。 我更喜欢创建键码数组,可以在任何时间检查我们的脚本以查看特定的键是否在当前按下。 我们可以添加一点脚本做到这一点:
// keyboard game.keys = []; game.keydown = function(e){ game.keys[e.keyCode] = true; }; game.keyup = function(e){ game.keys[e.keyCode] = false; }; // listen window.addEventListener('keydown', game.keydown, false); window.addEventListener('keyup', game.keyup, false);
这个函数允许我们跟踪每一个键盘事件。 例如,如果我们想知道在任何时候left-arrow-key是否松开,我们可以检查 game.keys[37]
,当松开这个键这将返回 true
, 在其他情况返回undefined
。 我们可以在每次game.update()执行时通过运行一个新的函数好好利用这个功能。
game.keypress = function(d){ // boost var boost = 1; if(game.keys[16]){ boost = 3; } // thrust if(game.keys[40]){ game.player.settings.v.x += Math.cos((game.player.settings.o - 270) * Math.PI / 180) * 0.002 * game.player.settings.speed * d; game.player.settings.v.y += Math.sin((game.player.settings.o - 270) * Math.PI / 180) * 0.002 * game.player.settings.speed * d; game.player.settings.status = 'reverse'; } else if(game.keys[38]){ game.player.settings.v.x += Math.cos((game.player.settings.o - 90) * Math.PI / 180) * 0.004 * (game.player.settings.speed * boost) * d; game.player.settings.v.y += Math.sin((game.player.settings.o - 90) * Math.PI / 180) * 0.004 * (game.player.settings.speed * boost) * d; game.player.settings.status = 'forward'; if(game.keys[16]){ game.player.settings.status = 'boost'; } } // rotate if(game.keys[37]){ game.player.settings.o -= (0.15 / boost) * d; if(game.player.settings.o < 0){ game.player.settings.o = 360 - game.player.settings.o; } else if(game.player.settings.o > 360){ game.player.settings.o = 0 + game.player.settings.o; } } if(game.keys[39]){ game.player.settings.o += (0.15 / boost) * d; if(game.player.settings.o < 0){ game.player.settings.o = 360 - game.player.settings.o; } else if(game.player.settings.o > 360){ game.player.settings.o = 0 + game.player.settings.o; } } };
我们脚本的最后一个片段允许用户操纵 player
对象的方向、运动向量和使用方向键的速度。 这允许用户旋转他们的飞船并在游戏世界移动。 我们甚至添加了一个当shift键和向上箭头键同时按下时速度猛增的功能。
最终的产品
做了一点调整,我们已经在我们的游戏中添加了数以百计的随机放置的恒星和行星,创造一个小二维的世界,我们可以在我们的飞船探索。使用键盘上的箭头键试试。
您可以下载源代码并在 http://oldrivercreative.com/space/ 全屏玩游戏。
5月6日发布,2014年由凯尔发表在 deployement 。