HTML5 2D平台游戏开发#7Camera
2017-06-22 16:33
441 查看
在庞大的游戏世界中,玩家不能一览地图全貌,而是只能看到其中一部分,并一步步探索,这时就要用到一种技术来显示局部的地图,游戏术语称为摄像机(Camera)。下面两张图中的白色矩形框表示了Camera的作用,玩家控制的角色总是在该矩形内。
可以想像成一个200X100宽高的相框固定在坐标(0,0)处,然后移动下面的蓝纸,蓝纸的不同位置就会显示在相框中。
同时,也只需绘制出现在相框中的地图即可,这样可以提升一部分程序的性能。为实现Camera功能,需要添加一些辅助方法。
改造一下原来的
同时新增
在游戏开始时初始化Camera:
渲染地图时只绘制Camera部分:
以下是演示效果:
网格
精灵框
碰撞框
坐标
// let gCachedAssets = {},
loadAssets = (assetList, callback) => {
let loadBatch = {
count: 0,
total: assetList.length,
cb: callback
},next;
(function loadAsset(src) {
if(gCachedAssets[src] === undefined) {
let assetType = getAssetTypeFromExtension(src);
if(assetType === 0) {
let img = new Image();
img.onload = () => {
onLoadedCallback(img,loadBatch);
next = assetList.shift();
if(next) {
loadAsset(next);
}
};
img.src = src;
gCachedAssets[src] = img;
} else if(assetType === 1) {
let script = document.createElement('script');
script.addEventListener('load', () => {
onLoadedCallback(script, loadBatch);
next = assetList.shift();
if(next) {
loadAsset(next);
}
});
script.src = src;
gCachedAssets[src] = script;
document.getElementsByTagName('head')[0].appendChild(script);
}
} else {
onLoadedCallback(gCachedAssets[src], loadBatch);
}
})(assetList.shift());
},
onLoadedCallback = (asset, batch) => {
batch.count++;
if (batch.count === batch.total) {
batch.cb(asset);
}
},
getAssetTypeFromExtension = (assetName) => {
if(assetName.indexOf('.jpg') !== -1 || assetName.indexOf('.jpeg') !== -1 || assetName.indexOf('.png') !== -1) {
return 0;
}
if(assetName.indexOf('.js') !== -1 || assetName.indexOf('.json') !== -1) {
return 1;
}
return -1;
};
(function() {
var canvas = document.createElement('canvas'),
a = document.getElementById('a');
canvas.id = 'c1';
canvas.width = 640;
canvas.height = 506;
a.appendChild(canvas);
var c = document.getElementById('c1'),
ctx = c.getContext('2d'),
lastTime = 0,
elapsed,
paused = false,
raqId,
playerSpriteSheet = new Image(),
levelAssets = new Image(),
canvasBG = new Image(),
now;
let camera;
loadAssets(['http://files.cnblogs.com/files/undefined000/game.min-v2.js?v=10'],function() {
playerSpriteSheet.src = imageData;
levelAssets.src = levelSpriteSheet;
canvasBG.src = background;
camera = new Camera(levels.stage1,0,0,c.width / MAPCONFIG.TILESIZE,c.height / MAPCONFIG.TILESIZE);
let level = new MapManager(levels.stage1,ctx,{
image:levelAssets,
w:416,
h:96
},camera);
let player = new Player(new Vector(5,2),ctx,level,playerSpriteSheet,camera);
camera.follow(player,c.width / 2 / MAPCONFIG.TILESIZE,c.height / 2 / MAPCONFIG.TILESIZE);
function loop() {
draw();
}
function stop() {
cancelAnimationFrame(raqId)
}
function draw() {
ctx.clearRect(0,0,c.width,c.height);
now = +new Date;
if(lastTime !== 0) {
elapsed = Math.min(now - lastTime,16);
} else elapsed = 16;
level.render();
player.update(elapsed);
camera.update();
lastTime = now;
raqId = requestAnimationFrame(draw);
}
loop();
window.addEventListener('keyup',(e) => {
if(e.keyCode === 80) {
paused = !paused;
if(paused) {
stop();
let txt = 'Pause';
ctx.font = '50px Source Han Serif';
ctx.fillStyle = '#f00';
ctx.fillText(txt, (c.width - ctx.measureText(txt).width) / 2, c.height / 2);
} else {
loop();
}
}
});
});
})();
// ]]>
2017/04/09 更新角色跳跃
2017/04/21 更新角色冲刺
2017/05/01 更新角色状态机
2017/05/16 更新角色攻击动画
2017/05/22 更新角色移动攻击动画
2017/05/24 更新角色跳跃攻击动画
2017/06/04 更新地图绘制
2017/06/22 更新摄像机、长距离冲刺
可以想像成一个200X100宽高的相框固定在坐标(0,0)处,然后移动下面的蓝纸,蓝纸的不同位置就会显示在相框中。
同时,也只需绘制出现在相框中的地图即可,这样可以提升一部分程序的性能。为实现Camera功能,需要添加一些辅助方法。
改造一下原来的
AABB函数:
class AABB { /** * 碰撞盒子 * @param x {number} 盒子x坐标 * @param y {number} 盒子y坐标 * @param w {number} 盒子宽度 * @param h {number} 盒子高度 */ constructor(x,y,w,h) { this.pos = new Vector(x,y); this.size = new Vector(w,h); this.center = new Vector(this.pos.x + w / 2,this.pos.y + h / 2); this.halfSize = new Vector(this.size.x / 2,this.size.y / 2); this.init(); } set(x, y, /*optional*/w, /*optional*/h) { this.pos = new Vector(x, y); this.size = new Vector(w || this.width, h || this.height); this.init(); } init() { this.left = this.pos.x; this.top = this.pos.y; this.width = this.size.x; this.height = this.size.y; this.right = this.left + this.width; this.bottom = this.top + this.height; } within(r) { return r.left <= this.left && r.right >= this.right && r.top <= this.top && r.bottom >= this.bottom; } }
同时新增
Camera构造函数:
let AXIS = {}; Object.defineProperties(AXIS,{ 'NONE':{ value:"none" }, 'HORIZONTAL':{ value:"horizontal" }, 'VERTICAL':{ value:"vertical" }, 'BOTH':{ value:"both" } }); class Camera { /** * 摄像机构造函数 * @param level {map} 地图 * @param x {Number} camera的x坐标 * @param y {Number} camera的y坐标 * @param canvasWidth {Number} camera视口宽度 * @param canvasHeight {Number} camera视口高度 * @param maxX {Number} camera的最大x坐标 * @param maxY {Number} camera的最大y坐标 */ constructor(level,x,y,canvasWidth,canvasHeight,maxX,maxY) { //摄像机左上角的x,y坐标 this.x = x; this.y = y; //摄像机的大小 this.w = canvasWidth; this.h = canvasHeight; //摄像机开始移动的临界点 //跟踪对象到摄像机边界的距离 this.xDeadZone = 0; //距离水平边界的距离 this.yDeadZone = 0; //距离垂直边界的距离 //摄像机能够移动的最大范围 this.maxX = maxX || level.cols - this.w; this.maxY = maxY || level.rows - this.h; //摄像机移动的方向 this.axis = AXIS.BOTH; //镜头跟随的对象 this.followed = null; //表示camera视口 this.viewportRect = new AABB(this.x,this.y,this.w,this.h); //表示整个地图范围 this.worldRect = new AABB(0,0,level.cols,level.rows); } follow(gameObject,xDeadZone,yDeadZone) { this.followed = gameObject; this.xDeadZone = xDeadZone; this.yDeadZone = yDeadZone; } update() { //仅在有跟随对象时更新摄像机位置 if(this.followed !== null) { if(this.axis === AXIS.HORIZONTAL || this.axis === AXIS.BOTH) { //根据跟随对象位置更新摄像机的x坐标 if(this.followed.pos.x - this.x + this.xDeadZone > this.w) { this.x = this.followed.pos.x - (this.w - this.xDeadZone); } else if(this.followed.pos.x - this.xDeadZone < this.x) { this.x = this.followed.pos.x - this.xDeadZone; } } if(this.axis === AXIS.VERTICAL || this.axis === AXIS.BOTH) { //根据跟随对象位置更新摄像机的y坐标 if(this.followed.pos.y - this.y + this.yDeadZone > this.h) { this.y = this.followed.pos.y - (this.h - this.yDeadZone); } else if(this.followed.pos.y - this.yDeadZone < this.y) { this.y = this.followed.pos.y - this.yDeadZone; } } } //重新设置camera视口的x坐标和y坐标 this.viewportRect.set(this.x,this.y); //保证camera不会超出地图范围 if(!this.viewportRect.within(this.worldRect)) { if(this.viewportRect.left < this.worldRect.left) this.x = this.worldRect.left; if(this.viewportRect.top < this.worldRect.top) this.y = this.worldRect.top; if(this.viewportRect.right > this.worldRect.right) this.x = this.worldRect.right - this.w; if(this.viewportRect.bottom > this.worldRect.bottom) this.y = this.worldRect.bottom - this.h; } } }
在游戏开始时初始化Camera:
camera = new Camera(levels,0,0,c.width / MAPCONFIG.TILESIZE,c.height / MAPCONFIG.TILESIZE); camera.follow(player,c.width / 2 / MAPCONFIG.TILESIZE,c.height / 2 / MAPCONFIG.TILESIZE);
渲染地图时只绘制Camera部分:
_drawLayer(layerIndex) { let tileSize = MAPCONFIG.TILESIZE, startCol = camera.x >> 0, //起始列 endCol = Math.floor(startCol + camera.w) + 1, //结束列 startRow = camera.y >> 0, //开始行 endRow = Math.floor(startRow + camera.h) + 1, //结束行 offsetX = -camera.x + startCol, offsetY = -camera.y + startRow; for (let r = startRow; r < endRow; r++) { for (let c = startCol; c < endCol; c++) { let tile = this.getTile(layerIndex, c, r), x = (c - startCol + offsetX) * tileSize, //瓦片的x坐标 y = (r - startRow + offsetY) * tileSize; //瓦片的y坐标 if (tile !== -1) { this.ctx.drawImage( this.spriteSheet, tile * tileSize % this.dimensions.w, //瓦片精灵图上的x坐标 Math.floor(tile * tileSize / this.dimensions.w) * tileSize, //瓦片精灵图上的y坐标 tileSize, tileSize, Math.round(x), Math.round(y), tileSize, tileSize ); } } } }
以下是演示效果:
网格
精灵框
碰撞框
坐标
// let gCachedAssets = {},
loadAssets = (assetList, callback) => {
let loadBatch = {
count: 0,
total: assetList.length,
cb: callback
},next;
(function loadAsset(src) {
if(gCachedAssets[src] === undefined) {
let assetType = getAssetTypeFromExtension(src);
if(assetType === 0) {
let img = new Image();
img.onload = () => {
onLoadedCallback(img,loadBatch);
next = assetList.shift();
if(next) {
loadAsset(next);
}
};
img.src = src;
gCachedAssets[src] = img;
} else if(assetType === 1) {
let script = document.createElement('script');
script.addEventListener('load', () => {
onLoadedCallback(script, loadBatch);
next = assetList.shift();
if(next) {
loadAsset(next);
}
});
script.src = src;
gCachedAssets[src] = script;
document.getElementsByTagName('head')[0].appendChild(script);
}
} else {
onLoadedCallback(gCachedAssets[src], loadBatch);
}
})(assetList.shift());
},
onLoadedCallback = (asset, batch) => {
batch.count++;
if (batch.count === batch.total) {
batch.cb(asset);
}
},
getAssetTypeFromExtension = (assetName) => {
if(assetName.indexOf('.jpg') !== -1 || assetName.indexOf('.jpeg') !== -1 || assetName.indexOf('.png') !== -1) {
return 0;
}
if(assetName.indexOf('.js') !== -1 || assetName.indexOf('.json') !== -1) {
return 1;
}
return -1;
};
(function() {
var canvas = document.createElement('canvas'),
a = document.getElementById('a');
canvas.id = 'c1';
canvas.width = 640;
canvas.height = 506;
a.appendChild(canvas);
var c = document.getElementById('c1'),
ctx = c.getContext('2d'),
lastTime = 0,
elapsed,
paused = false,
raqId,
playerSpriteSheet = new Image(),
levelAssets = new Image(),
canvasBG = new Image(),
now;
let camera;
loadAssets(['http://files.cnblogs.com/files/undefined000/game.min-v2.js?v=10'],function() {
playerSpriteSheet.src = imageData;
levelAssets.src = levelSpriteSheet;
canvasBG.src = background;
camera = new Camera(levels.stage1,0,0,c.width / MAPCONFIG.TILESIZE,c.height / MAPCONFIG.TILESIZE);
let level = new MapManager(levels.stage1,ctx,{
image:levelAssets,
w:416,
h:96
},camera);
let player = new Player(new Vector(5,2),ctx,level,playerSpriteSheet,camera);
camera.follow(player,c.width / 2 / MAPCONFIG.TILESIZE,c.height / 2 / MAPCONFIG.TILESIZE);
function loop() {
draw();
}
function stop() {
cancelAnimationFrame(raqId)
}
function draw() {
ctx.clearRect(0,0,c.width,c.height);
now = +new Date;
if(lastTime !== 0) {
elapsed = Math.min(now - lastTime,16);
} else elapsed = 16;
level.render();
player.update(elapsed);
camera.update();
lastTime = now;
raqId = requestAnimationFrame(draw);
}
loop();
window.addEventListener('keyup',(e) => {
if(e.keyCode === 80) {
paused = !paused;
if(paused) {
stop();
let txt = 'Pause';
ctx.font = '50px Source Han Serif';
ctx.fillStyle = '#f00';
ctx.fillText(txt, (c.width - ctx.measureText(txt).width) / 2, c.height / 2);
} else {
loop();
}
}
});
});
})();
// ]]>
更新日志
2017/04/09 更新角色跳跃
2017/04/21 更新角色冲刺
2017/05/01 更新角色状态机
2017/05/16 更新角色攻击动画
2017/05/22 更新角色移动攻击动画
2017/05/24 更新角色跳跃攻击动画
2017/06/04 更新地图绘制
2017/06/22 更新摄像机、长距离冲刺
相关文章推荐
- HTML5 2D平台游戏开发#9蓄力技
- HTML5 2D平台游戏开发#4状态机
- HTML5 2D平台游戏开发#8指令技
- HTML5 2D平台游戏开发#5攻击
- HTML5 2D平台游戏开发#10Wall Jump
- HTML5 2D平台游戏开发#3冲刺
- HTML5 2D平台游戏开发#6地图绘制
- HTML5 2D平台游戏开发#11斜坡物理
- HTML5 2D平台游戏开发#2跳跃与二段跳
- 在Android平台下搭建PhoneGap开发环境--用HTML5开发游戏
- HTML5 2D 游戏开发: 图形和动画
- Phaser是一款专门用于桌面及移动HTML5 2D游戏开发的开源免费框架
- 七巧板平台html5游戏开发之初识物理引擎
- 【coco2d-html5简单游戏开发】一、连锁反应小游戏
- HTML5吃豆豆游戏开发实战(四)2d碰撞检测、重构-第二篇
- HTML5 2D 游戏开发(一): Snail Bait 简介
- 【2Dhtml5游戏框架phaser介绍2】使用在线云IDE开发phaser
- HTML5 2D Canvas手机游戏开发经验谈
- CutJS – 用于 HTML5 游戏开发的 2D 渲染引擎