3.1 引言
添加触摸控件以及根据设备调整游戏界面尺寸。
3.2 添加触摸控件
绘制控件:为了使在移动设备上玩游戏称为可能,一种常见的解决方案是在屏幕上添加一些可视触摸控件,这些控件可由位于页面底部的三个方形按钮组成。
游戏需要处理不同的屏幕分辨率,因此,游戏不会绘制固定大小的输入方块,而会基于游戏界面的宽度调整方块的尺寸。根据一些非正式测试可知,把游戏的界面宽度分成5个区就足以获得很好的效果了,按钮要大到能够方便地单击,但又不能过多占用屏幕的实际实际使用面积。
TouchControls对象:
响应触摸事件:touchstart、touchmove和touchend。
触摸事件的属性:
- event.touches:当前发生在设备上的所有触摸
- event.targetTouches:发生在作为触发事件对象的同一个DOM上的所有触摸
- event.changedTouches:在这一事件中发生了变化的所有触摸
targetTouches用于左边控制移动的两个按钮,你希望用户能够按下这两个按钮之一来进行向左或向右移动。所以,每发生一个触摸事件,游戏就会查看当前是否有触摸击中了这两个按钮中的任一个。若是,则标记该按钮被按下,即使触发事件的触摸并未击中任何按钮,游戏也会进行此番检查。
3.3 最大化游戏界面
设置视口:不想让用户缩放页面,可加入
<meta name="viewport" content="width=device-width, user-scalable=0,minimum-scale=1.0,maximum-scale=1.0"/>
调整画布尺寸:步骤如下
检查浏览器是否支持触摸事件
若屏幕大于最大尺寸或没有触摸支持,提前退出
检查用户是否处于横屏模式下
若是,要求他们旋转浏览器
将容器的尺寸调整为大于页面,以便删除地址栏
稍微滚动窗口,借此强制删除地址栏
将容器的尺寸设置成与窗口大小相吻合
检查游戏是否在(平板电脑一类的)较大设备上运行
若是,把视图的大小设置成性能像素大小的两倍
否则,将画布设置成与窗口大小相吻合
最后,将画布设置成使用绝对定位,把画布的绝对位置设置成窗口的左上角
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
3.4 添加得分
只需在游戏中添加一个新的游戏面板即可GamePoints对象
3.5 使之成为公平的战斗
赋予敌方飞船一点回击的火力
EnemyMissile对象
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 href='http://fonts.googleapis.com/css?family=Bangers' rel='stylesheet' type='text/css'>
<meta name="viewport" content="width=device-width, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0"/>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
</head>
<body>
<div id='container'>
<canvas id='game' width='320' height='480'></canvas>
</div>
<script src='engine.js'></script>
<script src='game.js'></script>
</body>
</html>
base.css
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
/* Center the container */
#container {
padding-top:50px;
margin:0 auto;
width:480px;
}
/* Give canvas a background */
canvas {
background-color:black;
}
engine.js
var Game = new function() {
var boards = [];
// Game Initialization
this.initialize = function(canvasElementId,sprite_data,callback) {
this.canvas = document.getElementById(canvasElementId);
this.playerOffset = 10;
this.canvasMultiplier= 1;
this.setupMobile();
this.width = this.canvas.width;
this.height= this.canvas.height;
this.ctx = this.canvas.getContext && this.canvas.getContext('2d');
if(!this.ctx) { return alert("Please upgrade your browser to play"); }
this.setupInput();
this.loop();
if(this.mobile) {
this.setBoard(4,new TouchControls());
}
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);
};
// Game Loop
this.loop = function() {
var dt = 30 / 1000;
setTimeout(Game.loop,30);
for(var i=0,len = boards.length;i<len;i++) {
if(boards[i]) {
boards[i].step(dt);
boards[i].draw(Game.ctx);
}
}
};
// Change an active game board
this.setBoard = function(num,board) { boards[num] = board; };
this.setupMobile = function() {
var container = document.getElementById("container"),
hasTouch = !!('ontouchstart' in window),
w = window.innerWidth, h = window.innerHeight;
if(hasTouch) { mobile = true; }
if(screen.width >= 1280 || !hasTouch) { return false; }
if(w > h) {
alert("Please rotate the device and then click OK");
w = window.innerWidth; h = window.innerHeight;
}
container.style.height = h*2 + "px";
window.scrollTo(0,1);
h = window.innerHeight + 2;
container.style.height = h + "px";
container.style.width = w + "px";
container.style.padding = 0;
if(h >= this.canvas.height * 1.75 || w >= this.canvas.height * 1.75) {
this.canvasMultiplier = 2;
this.canvas.width = w / 2;
this.canvas.height = h / 2;
this.canvas.style.width = w + "px";
this.canvas.style.height = h + "px";
} else {
this.canvas.width = w;
this.canvas.height = h;
}
this.canvas.style.position='absolute';
this.canvas.style.left="0px";
this.canvas.style.top="0px";
};
return this;
};
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;
ctx.drawImage(this.image,
s.sx + frame * s.w,
s.sy,
s.w, s.h,
Math.floor(x), Math.floor(y),
s.w, s.h);
};
return this;
};
var TitleScreen = function TitleScreen(title,subtitle,callback) {
var up = false;
this.step = function(dt) {
if(!Game.keys['fire']) up = true;
if(up && 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);
};
};
var GameBoard = function() {
var board = this;
// The current list of objects
this.objects = [];
this.cnt = {};
// Add a new object to the object list
this.add = function(obj) {
obj.board=this;
this.objects.push(obj);
this.cnt[obj.type] = (this.cnt[obj.type] || 0) + 1;
return obj;
};
// Mark an object for removal
this.remove = function(obj) {
var idx = this.removed.indexOf(obj);
if(idx == -1) {
this.removed.push(obj);
return true;
} else {
return false;
}
};
// Reset the list of removed objects
this.resetRemoved = function() { this.removed = []; };
// Removed an objects marked for removal from the list
this.finalizeRemoved = function() {
for(var i=0,len=this.removed.length;i<len;i++) {
var idx = this.objects.indexOf(this.removed[i]);
if(idx != -1) {
this.cnt[this.removed[i].type]--;
this.objects.splice(idx,1);
}
}
};
// Call the same method on all current objects
this.iterate = function(funcName) {
var args = Array.prototype.slice.call(arguments,1);
for(var i=0,len=this.objects.length;i<len;i++) {
var obj = this.objects[i];
obj[funcName].apply(obj,args);
}
};
// Find the first object for which func is true
this.detect = function(func) {
for(var i = 0,val=null, len=this.objects.length; i < len; i++) {
if(func.call(this.objects[i])) return this.objects[i];
}
return false;
};
// Call step on all objects and them delete
// any object that have been marked for removal
this.step = function(dt) {
this.resetRemoved();
this.iterate('step',dt);
this.finalizeRemoved();
};
// Draw all the objects
this.draw= function(ctx) {
this.iterate('draw',ctx);
};
// Check for a collision between the
// bounding rects of two objects
this.overlap = function(o1,o2) {
return !((o1.y+o1.h-1<o2.y) || (o1.y>o2.y+o2.h-1) ||
(o1.x+o1.w-1<o2.x) || (o1.x>o2.x+o2.w-1));
};
// Find the first object that collides with obj
// match against an optional type
this.collide = function(obj,type) {
return this.detect(function() {
if(obj != this) {
var col = (!type || this.type & type) && board.overlap(obj,this);
return col ? this : false;
}
});
};
};
var Sprite = function() { };
Sprite.prototype.setup = function(sprite,props) {
this.sprite = sprite;
this.merge(props);
this.frame = this.frame || 0;
this.w = SpriteSheet.map[sprite].w;
this.h = SpriteSheet.map[sprite].h;
};
Sprite.prototype.merge = function(props) {
if(props) {
for (var prop in props) {
this[prop] = props[prop];
}
}
};
Sprite.prototype.draw = function(ctx) {
SpriteSheet.draw(ctx,this.sprite,this.x,this.y,this.frame);
};
Sprite.prototype.hit = function(damage) {
this.board.remove(this);
};
var Level = function(levelData,callback) {
this.levelData = [];
for(var i =0; i<levelData.length; i++) {
this.levelData.push(Object.create(levelData[i]));
}
this.t = 0;
this.callback = callback;
};
Level.prototype.step = function(dt) {
var idx = 0, remove = [], curShip = null;
// Update the current time offset
this.t += dt * 1000;
// Start, End, Gap, Type, Override
// [ 0, 4000, 500, 'step', { x: 100 } ]
while((curShip = this.levelData[idx]) &&
(curShip[0] < this.t + 2000)) {
// Check if we've passed the end time
if(this.t > curShip[1]) {
remove.push(curShip);
} else if(curShip[0] < this.t) {
// Get the enemy definition blueprint
var enemy = enemies[curShip[3]],
override = curShip[4];
// Add a new enemy with the blueprint and override
this.board.add(new Enemy(enemy,override));
// Increment the start time by the gap
curShip[0] += curShip[2];
}
idx++;
}
// Remove any objects from the levelData that have passed
for(var i=0,len=remove.length;i<len;i++) {
var remIdx = this.levelData.indexOf(remove[i]);
if(remIdx != -1) this.levelData.splice(remIdx,1);
}
// If there are no more enemies on the board or in
// levelData, this level is done
if(this.levelData.length === 0 && this.board.cnt[OBJECT_ENEMY] === 0) {
if(this.callback) this.callback();
}
};
Level.prototype.draw = function(ctx) { };
var TouchControls = function() {
var gutterWidth = 10;
var unitWidth = Game.width/5;
var blockWidth = unitWidth-gutterWidth;
this.drawSquare = function(ctx,x,y,txt,on) {
ctx.globalAlpha = on ? 0.9 : 0.6;
ctx.fillStyle = "#CCC";
ctx.fillRect(x,y,blockWidth,blockWidth);
ctx.fillStyle = "#FFF";
ctx.textAlign = "center";
ctx.globalAlpha = 1.0;
ctx.font = "bold " + (3*unitWidth/4) + "px arial";
ctx.fillText(txt,
x+blockWidth/2,
y+3*blockWidth/4+5);
};
this.draw = function(ctx) {
ctx.save();
var yLoc = Game.height - unitWidth;
this.drawSquare(ctx,gutterWidth,yLoc,"\u25C0", Game.keys['left']);
this.drawSquare(ctx,unitWidth + gutterWidth,yLoc,"\u25B6", Game.keys['right']);
this.drawSquare(ctx,4*unitWidth,yLoc,"A",Game.keys['fire']);
ctx.restore();
};
this.step = function(dt) { };
this.trackTouch = function(e) {
var touch, x;
e.preventDefault();
Game.keys['left'] = false;
Game.keys['right'] = false;
for(var i=0;i<e.targetTouches.length;i++) {
touch = e.targetTouches[i];
x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft;
if(x < unitWidth) {
Game.keys['left'] = true;
}
if(x > unitWidth && x < 2*unitWidth) {
Game.keys['right'] = true;
}
}
if(e.type == 'touchstart' || e.type == 'touchend') {
for(i=0;i<e.changedTouches.length;i++) {
touch = e.changedTouches[i];
x = touch.pageX / Game.canvasMultiplier - Game.canvas.offsetLeft;
if(x > 4 * unitWidth) {
Game.keys['fire'] = (e.type == 'touchstart');
}
}
}
};
Game.canvas.addEventListener('touchstart',this.trackTouch,true);
Game.canvas.addEventListener('touchmove',this.trackTouch,true);
Game.canvas.addEventListener('touchend',this.trackTouch,true);
Game.playerOffset = unitWidth + 20;
};
var GamePoints = function() {
Game.points = 0;
var pointsLength = 8;
this.draw = function(ctx) {
ctx.save();
ctx.font = "bold 18px arial";
ctx.fillStyle= "#FFFFFF";
var txt = "" + Game.points;
var i = pointsLength - txt.length, zeros = "";
while(i-- > 0) { zeros += "0"; }
ctx.fillText(zeros + txt,10,20);
ctx.restore();
};
this.step = function(dt) { };
};
game.js
var sprites = {
ship: { sx: 0, sy: 0, w: 37, h: 42, frames: 1 },
missile: { sx: 0, sy: 30, w: 2, h: 10, frames: 1 },
enemy_purple: { sx: 37, sy: 0, w: 42, h: 43, frames: 1 },
enemy_bee: { sx: 79, sy: 0, w: 37, h: 43, frames: 1 },
enemy_ship: { sx: 116, sy: 0, w: 42, h: 43, frames: 1 },
enemy_circle: { sx: 158, sy: 0, w: 32, h: 33, frames: 1 },
explosion: { sx: 0, sy: 64, w: 64, h: 64, frames: 12 },
enemy_missile: { sx: 9, sy: 42, w: 3, h: 20, frame: 1 }
};
var enemies = {
straight: { x: 0, y: -50, sprite: 'enemy_ship', health: 10,
E: 100 },
ltr: { x: 0, y: -100, sprite: 'enemy_purple', health: 10,
B: 75, C: 1, E: 100, missiles: 2 },
circle: { x: 250, y: -50, sprite: 'enemy_circle', health: 10,
A: 0, B: -100, C: 1, E: 20, F: 100, G: 1, H: Math.PI/2 },
wiggle: { x: 100, y: -50, sprite: 'enemy_bee', health: 20,
B: 50, C: 4, E: 100, firePercentage: 0.001, missiles: 2 },
step: { x: 0, y: -50, sprite: 'enemy_circle', health: 10,
B: 150, C: 1.2, E: 75 }
};
var OBJECT_PLAYER = 1,
OBJECT_PLAYER_PROJECTILE = 2,
OBJECT_ENEMY = 4,
OBJECT_ENEMY_PROJECTILE = 8,
OBJECT_POWERUP = 16;
var startGame = function() {
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 fire to start playing",
playGame));
};
var level1 = [
// Start, End, Gap, Type, Override
[ 0, 4000, 500, 'step' ],
[ 6000, 13000, 800, 'ltr' ],
[ 10000, 16000, 400, 'circle' ],
[ 17800, 20000, 500, 'straight', { x: 50 } ],
[ 18200, 20000, 500, 'straight', { x: 90 } ],
[ 18200, 20000, 500, 'straight', { x: 10 } ],
[ 22000, 25000, 400, 'wiggle', { x: 150 }],
[ 22000, 25000, 400, 'wiggle', { x: 100 }]
];
var playGame = function() {
var board = new GameBoard();
board.add(new PlayerShip());
board.add(new Level(level1,winGame));
Game.setBoard(3,board);
Game.setBoard(5,new GamePoints(0));
};
var winGame = function() {
Game.setBoard(3,new TitleScreen("You win!",
"Press fire to play again",
playGame));
};
var loseGame = function() {
Game.setBoard(3,new TitleScreen("You lose!",
"Press fire to play again",
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 PlayerShip = function() {
this.setup('ship', { vx: 0, reloadTime: 0.25, maxVel: 200 });
this.reload = this.reloadTime;
this.x = Game.width/2 - this.w / 2;
this.y = Game.height - Game.playerOffset - this.h;
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.reload-=dt;
if(Game.keys['fire'] && this.reload < 0) {
Game.keys['fire'] = false;
this.reload = this.reloadTime;
this.board.add(new PlayerMissile(this.x,this.y+this.h/2));
this.board.add(new PlayerMissile(this.x+this.w,this.y+this.h/2));
}
};
};
PlayerShip.prototype = new Sprite();
PlayerShip.prototype.type = OBJECT_PLAYER;
PlayerShip.prototype.hit = function(damage) {
if(this.board.remove(this)) {
loseGame();
}
};
var PlayerMissile = function(x,y) {
this.setup('missile',{ vy: -700, damage: 10 });
this.x = x - this.w/2;
this.y = y - this.h;
};
PlayerMissile.prototype = new Sprite();
PlayerMissile.prototype.type = OBJECT_PLAYER_PROJECTILE;
PlayerMissile.prototype.step = function(dt) {
this.y += this.vy * dt;
var collision = this.board.collide(this,OBJECT_ENEMY);
if(collision) {
collision.hit(this.damage);
this.board.remove(this);
} else if(this.y < -this.h) {
this.board.remove(this);
}
};
var Enemy = function(blueprint,override) {
this.merge(this.baseParameters);
this.setup(blueprint.sprite,blueprint);
this.merge(override);
};
Enemy.prototype = new Sprite();
Enemy.prototype.type = OBJECT_ENEMY;
Enemy.prototype.baseParameters = { A: 0, B: 0, C: 0, D: 0,
E: 0, F: 0, G: 0, H: 0,
t: 0, reloadTime: 0.75,
reload: 0 };
Enemy.prototype.step = function(dt) {
this.t += dt;
this.vx = this.A + this.B * Math.sin(this.C * this.t + this.D);
this.vy = this.E + this.F * Math.sin(this.G * this.t + this.H);
this.x += this.vx * dt;
this.y += this.vy * dt;
var collision = this.board.collide(this,OBJECT_PLAYER);
if(collision) {
collision.hit(this.damage);
this.board.remove(this);
}
if(this.reload <= 0 && Math.random() < (this.firePercentage || 0.01) ) {
this.reload = this.reloadTime;
if(this.missiles == 2) {
this.board.add(new EnemyMissile(this.x+this.w-2,this.y+this.h/2));
this.board.add(new EnemyMissile(this.x+2,this.y+this.h/2));
} else {
this.board.add(new EnemyMissile(this.x+this.w/2,this.y+this.h));
}
}
this.reload-=dt;
if(this.y > Game.height ||
this.x < -this.w ||
this.x > Game.width) {
this.board.remove(this);
}
};
Enemy.prototype.hit = function(damage) {
this.health -= damage;
if(this.health <=0) {
if(this.board.remove(this)) {
Game.points += this.points || 100;
this.board.add(new Explosion(this.x + this.w/2,
this.y + this.h/2));
}
}
};
var EnemyMissile = function(x,y) {
this.setup('enemy_missile',{ vy: 200, damage: 10 });
this.x = x - this.w/2;
this.y = y;
};
EnemyMissile.prototype = new Sprite();
EnemyMissile.prototype.type = OBJECT_ENEMY_PROJECTILE;
EnemyMissile.prototype.step = function(dt) {
this.y += this.vy * dt;
var collision = this.board.collide(this,OBJECT_PLAYER)
if(collision) {
collision.hit(this.damage);
this.board.remove(this);
} else if(this.y > Game.height) {
this.board.remove(this);
}
};
var Explosion = function(centerX,centerY) {
this.setup('explosion', { frame: 0 });
this.x = centerX - this.w/2;
this.y = centerY - this.h/2;
};
Explosion.prototype = new Sprite();
Explosion.prototype.step = function(dt) {
this.frame++;
if(this.frame >= 12) {
this.board.remove(this);
}
};