1.1 引言
在HTML5上从头开始构建一个一次性游戏---一个名为Alien Invasion的纵向卷轴2D太空射击类游戏。
1.2 用500行代码构建一个完整游戏
Alien Invasion秉承了游戏“1942”的精髓(但是在太空中),或可把它看成Galaga的一个简化版本。玩家控制出现在屏幕底部的飞船,操作飞船垂直飞过无边无际的太空领域,同时保卫地球,抵抗成群入侵的外星人。
在移动设备上玩游戏时,用户是通过显示在屏幕左下角的左右箭头进行控制的,发射按钮在右侧。在桌面上玩游戏时,用户可以使用键盘的箭头键来控制飞行和使用空格键进行射击。
为弥补移动设备屏幕大小各有不同这一不足,游戏会调整游戏区域,始终按照设备大小来运行游戏。在桌面上,游戏会被放在浏览器页面中间的一个矩形区域中运行。
结构化游戏:几乎每个这种类型的游戏都包含了几块相同的内容:一些资产的加载、一个标题画面、一些精灵、用户输入、碰撞检测以及一个把这几块内容整合在一起的游戏循环。
http://cykod.github.com/AlienInvasion/
1.3 添加HTML和CSS样板代码
<canvas>元素
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Alien Invasion</title>
<link rel="stylesheet" href="base.css" type="text/css" />
</head>
<body>
<div id="container">
<canvas id='game' width='320' height="480"></canvas>
</div>
<script src="game.js"></script>
</body>
</html>
base.css
#container {
padding-top: 50px;
margin: 0 auto;
width: 480px;
}
canvas {
background-color: black;
}
1.4 画布入门
drawImage有着几种不同的调用形式,这取决于你是想绘制完整图像还是仅绘制部分图像。
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
这种形式使得你能使用参数sx、sy、sWidth和sHeight在图像中指定源矩形,以及使用参数dx、dy、dWidth和dHeight在画布上指定目标矩形。要从精灵表的某个精灵中抽取单独的帧,这就是你应用使用的格式
game.js
var canvas = document.getElementById('game');
var ctx = canvas.getContext && canvas.getContext('2d');
if (!ctx) {
// No 2d
alert('Please upgrade your browser');
} else {
startGame();
}
function startGame() {
ctx.fillStyle = "#FFFF00";
ctx.fillRect(50, 100, 380, 400);
ctx.fillStyle = "rgba(0,0,128,0.5);";
ctx.fillRect(0, 50, 380, 400);
//加载图像
var img = new Image();
img.onload = function() {
// 异步调用
ctx.drawImage(img,18,0,18,25,100,100,18,25);
console.log("onload");
}
img.src = 'images/sprites.png';
}
1.5 创建游戏的结构
利用鸭子类型:基于对象的外部接口(而非它们的类型)来使用对象的概念称为鸭子类型(duck typing)
Alien Invasion在游戏界面和精灵这两个地方用到了这一概念,该游戏把任何响应step()和draw()方法调用的事物都当成游戏界面对象或有效的精灵对待。把鸭子类型用于游戏界面,这使得Alien Invasion能够把标题画面和游戏中的界面当成同样的对象类型看待,简化了关卡和标题画面之间的切换。同样鸭子类型用于精灵意味着游戏能够灵活决定往游戏面板中添加的内容,其中包括玩家、敌人、炮弹和HUD元素等。HUD是抬头显示设备的简称,这个术语常指位于游戏屏幕上方的元素,如剩余的生命条数和玩家得分等。
创建三个基本对象:Game对象(把所有东西捆绑在一起)、SpriteSheet对象(加载和绘制精灵)以及GameBoard(显示、更新精灵元素和处理精灵元素碰撞)。该游戏还需要一大群不同的精灵,如玩家、敌方飞船、导弹及诸如得分和剩余生命条数一类的HUD对象等。
1.6 加载精灵表
SpriteSheet类
1.7 创建Game对象
主要目的是初始化游戏引擎并运行游戏循环,以及提供一种机制来改变所显示的主场景。
1.8 添加滚动背景
每帧绘制太多精灵会降低游戏在移动设备上的运行速度。一种解决方法是创建画布的离屏缓冲区,在缓冲区中随机绘制一堆星星,然后简单地绘制慢慢向下移过画布的星空。
http://jsperf.com/prerendered-starfield测试HTML5性能
StarField类需要完成的事情主要有三项,第一项是创建离屏画布。
1.9 插入标题画面
一个文本标题和一个副标题
Bangers字体赋予游戏一种很好的复古风格
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Bangers" type="text/css" />
1.10 添加主角
第一步是添加一艘由玩家控制的飞船
创建PlayerShip对象:
index.html
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Alien Invasion</title>
<link rel="stylesheet" href="base.css" type="text/css" />
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Bangers" type="text/css" />
</head>
<body>
<div id="container">
<canvas id='game' width='480' height="600"></canvas>
</div>
<script src="engine.js"></script>
<script src="game.js"></script>
</body>
</html>
base.css
#container {
padding-top: 50px;
margin: 0 auto;
width: 480px;
}
canvas {
background-color: black;
}
engine.js
var Game = new function() {
this.initialize = function(canvasElementId,sprite_data,callback) {
this.canvas = document.getElementById(canvasElementId);
this.width = this.canvas.width;
this.height = this.canvas.height;
// Setu up the rendering context
this.ctx = this.canvas.getContext && this.canvas.getContext('2d');
if (!this.ctx) {
return alert("Please upgrade your browser to play");
}
this.setupInput();
this.loop();
SpriteSheet.load(sprite_data, callback);
};
// Handle Input
var KEY_CODES = { 37:'left', 39:'right', 32:'fire' };
this.keys = {};
this.setupInput = function() {
window.addEventListener('keydown', function(e) {
if (KEY_CODES[event.keyCode]) {
Game.keys[KEY_CODES[event.keyCode]] = true;
e.preventDefault();
}
}, false);
window.addEventListener('keyup', function(e) {
if (KEY_CODES[event.keyCode]) {
Game.keys[KEY_CODES[event.keyCode]] = false;
e.preventDefault();
}
}, false);
}
// 存放的是已更新并已绘制到画布上的游戏的各块内容,一个可能的面板例子是背景或标题画面
var boards = [];
this.loop = function() {
var dt = 30/1000;
for (var i=0, len=boards.length; i<len; i++) {
if (boards[i]) {
boards[i].step(dt);
boards[i] && boards[i].draw(Game.ctx);
}
}
setTimeout(Game.loop, 30);
};
// Change an active game board
this.setBoard = function(num, board) {
boards[num] = board;
};
};
var SpriteSheet = new function() {
this.map = { };
this.load = function(spriteData, callback) {
this.map = spriteData;
this.image = new Image();
this.image.onload = callback;
this.image.src = 'images/sprites.png';
};
this.draw = function(ctx,sprite,x,y,frame) {
var s = this.map[sprite];
if (!frame) {
frame = 0;
}
//console.log(s);
ctx.drawImage(this.image,
s.sx + frame * s.w,
s.sy,
s.w, s.h,
x, y,
s.w, s.h);
};
};
var TitleScreen = function TitleScreen(title, subtitle, callback) {
this.step = function(dt) {
if (Game.keys['fire'] && callback)
callback();
};
this.draw = function(ctx) {
ctx.fillStyle = "#FFFFFF";
ctx.textAlign = "center";
ctx.font = "bold 40px bangers";
ctx.fillText(title, Game.width/2, Game.height/2);
ctx.font = "bold 20px bangers";
ctx.fillText(subtitle, Game.width/2, Game.height/2 + 40);
}
};
game.js
var sprites = {
ship: { sx: 0, sy: 0, w: 38, h: 42, frames: 1 }
};
function startGame() {
//SpriteSheet.draw(Game.ctx, "ship", 100, 100, 1);
Game.setBoard(0, new Starfield(20,0.4,100,true))
Game.setBoard(1, new Starfield(50,0.6,100))
Game.setBoard(2, new Starfield(100,1.0,50));
Game.setBoard(3, new TitleScreen("Alien Invasion",
"Press space to start playing",
playGame));
}
window.addEventListener("load", function() {
Game.initialize("game", sprites, startGame);
});
var Starfield = function(speed,opacity,numStars,clear){
// Set up the offscreen canvas
var stars = document.createElement("canvas");
stars.width = Game.width;
stars.height = Game.height;
var starCtx = stars.getContext("2d");
var offset = 0;
// If the clear option is set,
// make the background black instead of transparent
if (clear) {
starCtx.fillStyle = "#000";
starCtx.fillRect(0,0,stars.width,stars.height);
}
// Now draw a bunch of random 2 pixel
// rectangles onto the offscreen canvas
starCtx.fillStyle = "#FFF";
starCtx.globalAlpha = opacity;
for (var i=0; i<numStars; i++) {
starCtx.fillRect(Math.floor(Math.random()*stars.width),
Math.floor(Math.random()*stars.height),
2,
2);
}
// This method is called every frame
// to draw the starfield onto the canvas
this.draw = function(ctx) {
var intOffset = Math.floor(offset);
var remaining = stars.height - intOffset;
// Draw the top half of the starfield
if (intOffset > 0) {
ctx.drawImage(stars,
0, remaining,
stars.width, intOffset,
0, 0,
stars.width, intOffset);
}
// Draw the bottom half of the starfield
if (remaining > 0) {
ctx.drawImage(stars,
0, 0,
stars.width, remaining,
0, intOffset,
stars.width, remaining);
}
}
// This method is called to update
// the starfield
this.step = function(dt) {
offset += dt * speed;
offset = offset % stars.height;
}
}
var playGame = function() {
Game.setBoard(3, new PlayerShip());
}
var PlayerShip = function() {
this.w = SpriteSheet.map['ship'].w;
this.h = SpriteSheet.map['ship'].h;
this.x = Game.width/2 - this.w/2;
this.y = Game.height - 10 - this.h;
this.vx = 0;
this.step = function(dt) {
//
this.maxVel = 200;
this.step = function(dt) {
if (Game.keys['left']) {
this.vx = -this.maxVel;
} else if (Game.keys['right']) {
this.vx = this.maxVel;
} else {
this.vx = 0;
}
this.x += this.vx * dt;
if (this.x < 0) {
this.x = 0;
} else if (this.x > Game.width - this.w) {
this.x = Game.width - this.w;
}
}
};
this.draw = function(ctx) {
SpriteSheet.draw(ctx, 'ship', this.x, this.y, 1);
};
};