在庞大的游戏世界中,玩家不能一览地图全貌,而是只能看到其中一部分,并一步步探索,这时就要用到一种技术来显示局部的地图,游戏术语称为摄像机(Camera)。下面两张图中的白色矩形框表示了Camera的作用,玩家控制的角色总是在该矩形内。

可以想像成一个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
);
}
}
}
}

以下是演示效果:

input{
position: relative;
z-index: 2;
}
#debugTools ul{
transition: all .3s;
opacity: 1;
left: 70px;
top: 0;
position: absolute;
}
#debugTools ul li{
float: left;
margin-right: 15px;
}
#debugTools.active ul {
left: 0;
opacity: 0;
}
-->

 
  • 网格
  • 精灵框
  • 碰撞框
  • 坐标

// {
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(['https://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  更新摄像机、长距离冲刺

最新文章

  1. 反编译apk
  2. 阿里中间件——diamond
  3. Django 模版语言详解
  4. Apache 优雅重启 Xampp开机自启 - 【环境变量】用DOS命令在任意目录下启动服务
  5. Entity Framework Fluent API
  6. ubuntu timezone
  7. Ruby与Python开发的环境IDE配置(附软件的百度云链接)
  8. 一个最简html5文档来说明html5的新特性和写法
  9. 编译工程时报illegal character:\65279--转
  10. Careercup - Google面试题 - 4807591515389952
  11. POJ1276Cash Machine
  12. HDU-4611 Balls Rearrangement 循环节,模拟
  13. linux常用命令搜索
  14. Day 1 @ RSA Conference Asia Pacific &amp; Japan 2016
  15. CLR via C# - Char_String - Format
  16. JavaSE_ 反射 目录(27)
  17. (C#)Windows Shell 外壳编程系列6 - 执行
  18. C++获取文件大小常用技巧
  19. js--数组去重3种方法
  20. SpringMvc+thymeleaf+HTML5中文乱码问题

热门文章

  1. Educational Codeforces Round 31 B. Japanese Crosswords Strike Back【暴力】
  2. spoj 913 Query on a tree II (倍增lca)
  3. Cookie和Session在Node.JS中的实践(三)
  4. Loj #6019. 「from CommonAnts」寻找 LCM
  5. POJ 3368 Frequent values(线段树区间合并)
  6. ios禁用多按钮同时按下操作
  7. iOS UI、Xcode、调试、代码等常见问题总汇(持续更新中)
  8. centos iptables关于ping
  9. Scala各种符号含义;scala =&gt;符号含义总结
  10. log4j教程 2、安装