我们将创建一个游戏我将他称之为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 。