制作canvas游戏


发布者 1518409521  发布时间 1411479797380
关键字 JS学习  Html5 
在我上一篇创造<canvas>艺术的帖子中,我介绍了使用HTML5< canvas>API来创建一个随机生成的“北极光”动画。产生动画的效果完全是审美。除了生成颜色和形状它没有提供交互性。在这篇文章中,我们将使用相同的技术介绍原文,但扩展我们的例子介绍键盘输入和操作以创建一个完整的互动游戏。    
我们将创建一个游戏我将他称之为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/ 全屏玩游戏。

56日发布,2014年由凯尔发表在 deployement