SpriteSheet
提供精灵图的加载和底层绘制
//精灵类,load初化图片,draw负责进行绘制
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 = '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, x, y, s.w, s.h);
}
}
Sprite
游戏中活动对象父类,setup方法相当于构造函数,merge可以用来初始化参数,hit为碰撞事件,draw为通用顶层绘制
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.hit = function() {
this.board.remove(this);
}
Sprite.prototype.draw = function (ctx) {
SpriteSheet.draw(ctx, this.sprite, this.x, this.y, this.frame);
}
Game
游戏类, initialize,融合canvas属性,setupInput检测输入,loop对Game中的boards组件进行循环运行step和draw方法,initialize中初始了loop循环,其中调用step和draw方法,setBoard用来加入板块
//游戏类, initialize,融合canvas属性,setupInput检测输入,loop对Game中的boards组件进行循环运行step和draw方法
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;
this.ctx = this.canvas.getContext && this.canvas.getContext('2d');
if (!this.ctx) {return alert('请升级浏览器');}
this.setupInput();
this.loop();
SpriteSheet.load(sprite_data, callback);
}
var KEY_CODES = {37:'left', 39:'right', 32:'fire'};
this.keys = {};
this.setupInput = function(){
window.addEventListener('keydown', function(e){
if (KEY_CODES[e.keyCode]) {
Game.keys[KEY_CODES[e.keyCode]] = true;
e.preventDefault();
}
},false);
window.addEventListener('keyup', function(e){
if (KEY_CODES[e.keyCode]) {
Game.keys[KEY_CODES[e.keyCode]] = false;
e.preventDefault();
}
},false);
};
var boards = [];
this.loop = function(){
var dt = 30/1000;
for (var i = 0;i < boards.length; i ++) {
if (boards[i]) {
boards[i].step(dt);
boards[i] && boards[i].draw(Game.ctx);
}
}
setTimeout(Game.loop, 30);
};
this.setBoard = function(num, board) {
boards[num] = board;
}
}
Starfield
星空类,组件式,用一个canvas在作为背景来绘制,板块之一
var Starfield = function(speed, opacity, numStarts, clear){
var stars = document.createElement("canvas");
stars.width = Game.width;
stars.height = Game.height;
var starCtx = stars.getContext('2d');
var offset = 0;
//绘制黑色背景
if (clear) {
starCtx.fillStyle = "#000";
starCtx.fillRect(0,0, stars.width, stars.height);
}
starCtx.fillStyle = "#FFF";
starCtx.globalAlpha = opacity;
for (var i = 0; i < numStarts; i ++) {
starCtx.fillRect(Math.floor(Math.random()*stars.width), Math.floor(Math.random()*stars.height), 2, 2);
}
this.draw = function(ctx){
var intOffset = Math.floor(offset);
var remaining = stars.height - intOffset;
if (intOffset > 0) {
ctx.drawImage(stars,0, remaining, stars.width, intOffset, 0, 0, stars.width, intOffset);
}
if (remaining > 0 ) {
ctx.drawImage(stars, 0, 0, stars.width, remaining, 0, intOffset, stars.width, remaining);
}
}
//console.log(offset)
this.step = function(dt) {
offset += dt * speed;
//console.log(dt)
offset = offset % stars.height;
}
}
TitleScreen
标题板块
var TitleScreen = function (title, subtitle, callback) {
this.step = function (dt) {
if (Game.keys['fire'] && callback) callback();
};
this.draw = function (ctx) {
ctx.fillStyle = "#fff";
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);
}
}
GameBoard
add,remove,resetRemoved,finalizeRemoved增,删(模拟),重置,销毁(实际),collide碰撞检测,overlap检测,detect方法遍历方法,但是会在函数返回真时停止,iterate方法全部遍历,GameBoard为一个大板块,然后管理着其它的活动对象
PlayerShip
游戏人对象,继承Sprite
var PlayerShip = function() {
this.setup('ship', {vx: 0, frame: 0, reloadTime: 0.25, maxVel: 200});
// 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.reloadTime = 0.1;
this.reload = this.reloadTime;
// 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.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));
}
}
// this.draw = function(ctx) {
// SpriteSheet.draw(ctx, 'ship', this.x, this.y, 0);
// }
}
PlayerShip.prototype = new Sprite();
PlayerShip.prototype.type = OBJECT_PLAYER;
PlayerShip.prototype.hit = function() {
this.board.remove(this);
this.board.add(new Explosion(this.x +this.w/2 , this.y + this.h/2,loseGame))
;}
PlayerMissile
游戏人炮弹类
var PlayerMissile = function(x, y) {
this.setup('missile', {vy: -700, damage: 10})
// this.w = SpriteSheet.map['missile'].w;
// this.h = SpriteSheet.map['missile'].h;
this.x = x - this.w / 2;
this.y = y -this.h;
// this.vy = -700;
};
PlayerMissile.prototype = new Sprite();
PlayerMissile.prototype.type = OBJECT_ENEMY_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);
}
if (this.y < - this.h) {
this.board.remove(this);
}
};
Enemy
敌人类
var Enemy = function (blueprint, override) {
this.merge(this.baseParameters);
this.setup(blueprint.sprite, blueprint);
this.merge(override);
var baseParameters = {A: 0, B: 0, C: 0, D: 0, E: 0, F: 0, G: 0, H: 0};
// for (var prop in baseParameters) {
// this[prop] = baseParameters[prop];
// }
// for (var prop in blueprint) {
// this[prop] = blueprint[prop];
// }
// if (override) {
// for (prop in override) {
// this[prop] = override[prop];
// }
// }
// console.log(this.sprite)
// this.w = SpriteSheet.map[this.sprite].w;
// this.h = SpriteSheet.map[this.sprite].h;
// this.t = 0;
}
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};
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.hit(20);
}
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)) {
this.board.add(new Explosion(this.x + this.w/2, this.y +this.h/2));
}
}
}
Explosion
爆炸类
var Explosion = function (centerX, centerY, callback) {
this.setup('explosion', {frame: 0});
this.x = centerX - this.w / 2;
this.y = centerY - this.h / 2;
this.subFrame = 0;
this.callback = callback;
}
Explosion.prototype = new Sprite();
Explosion.prototype.step = function (dt) {
this.frame = Math.floor((this.subFrame++) / 3);
if (this.subFrame >= 36) {
this.board.remove(this);
if (this.callback) this.callback();
}
}
GameBoard对象:
//object[],cnt[],removed[]
var GameBoard = function() {
var board = this;
this.objects = [];
this.cnt = [];
this.add = function(obj){
obj.board = this;
this.objects.push(obj);
this.cnt[obj.type] = (this.cnt[obj.type] || 0) + 1;
return obj;
};
this.remove = function(obj) {
var wasStillAlive = this.removed.indexOf(obj) == -1;
//console.log(wasStillAlive)
if (wasStillAlive) {
this.removed.push(obj);
}
return wasStillAlive;
}
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);
}
};
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;
}
this.step = function(dt) {
//console.log('sdf')
this.resetRemoved();
this.iterate('step', dt);
this.finalizeRemoved();
}
this.draw = function(ctx) {
this.iterate('draw', ctx);
}
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));
}
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;
}
});
};
this.resetRemoved = function(){this.removed = [];}
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);
}
}
}
}
Level
关卡类
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) {
//console.log(this.levelData)
var idx = 0, remove = [], curShip = null;
this.t += dt * 1000;
while ((curShip = this.levelData[idx]) && (curShip[0] < this.t + 2000)) {
console.log(this.t)
if (this.t > curShip[1]) {
remove.push(curShip);
} else if (curShip[0] < this.t) {
var enemy = enemies[curShip[3]], override = curShip[4];
this.board.add(new Enemy(enemy, override));
curShip[0] += curShip[2];
}
//console.log(curShip)
idx++;
}
for (var i = 0,len = remove.length; i< len; i++) {
var idx = this.levelData.indexOf(remove[i]);
if (idx != -1) this.levelData.splice(idx,1);
}
if (this.levelData.length === 0 && this.board.cnt[OBJECT_ENEMY] === 0) {
if (this.callback) this.callback();
}
}
Level.prototype.draw = function(ctx) {}
关于js函数中的arguments参数的巧用
三次法则
一个简单高效的碰撞检测
this.collide = function(obj, type) { return this.detect(function(){ if (obj != this) { if ((!type || this.type & type)){ console.log(type,this.type) console.log((!type || this.type & type)) } var col = (!type || this.type & type) && board.overlap(obj, this) return col ? this : false; } }); };
利用按位与过滤和指定不符的对象
延缓动画时长小技巧
Explosion.prototype.step = function (dt) { this.frame = Math.floor(this.subFrame++ / 3); console.log(this.subFrame); if (this.subFrame >= 36) { this.board.remove(this); } }
js深度复制
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; }
游戏控制函数
var playGame = function () {
//Game.setBoard(3, new PlayerShip());
var board = new GameBoard();
board.add(new PlayerShip());
//board.add(new Player)
board.add(new Level(level1, winGame));
// board.add(new Enemy(ememies.basic));
// board.add(new Enemy(ememies.basic, {x: 200}));
Game.setBoard(3, board);
}
var winGame = function() {
Game.setBoard(3, new TitleScreen('你赢了','点击重新游戏', playGame));
}
var loseGame = function () {
Game.setBoard(3, new TitleScreen("你输了",'点击重新游戏',playGame))
}
// var ememies = {basic: {x:100, y: -50, sprite: 'enemy_purple',B: 100, C: 2, E: 100, health: 20}};
// 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: 200, C: 1, E: 200},
// circle: {x: 400, y:-50, sprite: 'enemy_circle', health: 10, A: 0, B: -200, C: 1, E: 20, F: 200, G: 1, H: Math.PI/2},
// wiggle: {x: 0, y: -50, sprite: 'enemy_bee', health: 20, B: 100, C: 4, E: 100},
// step: {x: 0, y:-50, sprite: 'enemy_circle', health: 10, B:300, C: 1.5, E: 60}
// }
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 },
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 },
step: { x: 0, y: -50, sprite: 'enemy_circle', health: 10,
B: 150, C: 1.2, E: 75 }
};
var startGame = function() {
//SpriteSheet.draw(Game.ctx, "ship", 100, 100, 0);
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, 50));
Game.setBoard(3, new TitleScreen("打飞机", "单击开始", playGame))
}
window.addEventListener("load", function(){
Game.initialize("game", sprites, startGame);
});
游戏所需数据,配置
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}
};
var level1 = [
[0, 4000, 500, 'step'],
[6000, 13000, 800, 'ltr'],
[12000, 16000,400, 'circle'],
[18200, 20000, 500, 'straight', {x: 150}],
[18200, 20000, 500, 'straight', {x: 100}],
[18400, 20000, 500, 'straight', {x: 200}],
[22000, 25000, 400, 'wiggle', {x: 300}],
[22000, 25000, 400, 'wiggle', {x: 200}],
]
结果图