在庞大的游戏世界中,玩家不能一览地图全貌,而是只能看到其中一部分,并一步步探索,这时就要用到一种技术来显示局部的地图,游戏术语称为摄像机(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  更新摄像机、长距离冲刺

HTML5 2D平台游戏开发#7Camera的更多相关文章

  1. HTML5 2D平台游戏开发#4状态机

    在实现了<HTML5 2D平台游戏开发——角色动作篇之冲刺>之后,我发现随着角色动作的增加,代码中的逻辑判断越来越多,铺天盖地的if() else()语句实在让我捉襟见肘: 这还仅仅是角色 ...

  2. HTML5 2D平台游戏开发#6地图绘制

    此前已经完成了一部分角色的动作,现在还缺少可以交互的地图让游戏看起来能玩.不过在开始之前应当考虑清楚使用什么类型的地图,就2D平台游戏来说,一般有两种类型的地图,Tile-based和Art-base ...

  3. HTML5 2D平台游戏开发#11斜坡物理

    在游戏中会经常遇到斜坡地形,比如众所周知的魂斗罗,角色可以在坡上移动和跳跃: 斜坡在2D游戏中很常见,处理起来也较为棘手.最初我打算用分离轴定律来实现,在建立了一个物理模型之后: 发现上坡时没什么问题 ...

  4. HTML5 2D平台游戏开发#8指令技

    一般在动作游戏中,玩家可以通过对输入设备输入一系列的指令让角色完成某个或多个特定的动作.以格斗游戏<拳皇>为例,键入↓↘→↘↓↙← + A or C可以触发IORI的必杀技八稚女: 通过一 ...

  5. HTML5 2D平台游戏开发#5攻击

    目前为止,角色除了基本的移动外还什么都不能做,于是我打算先实现角色的攻击动画.角色的普通攻击一共可以分为三个阶段: 一段斩 二段斩 三段斩 移动攻击 跳跃攻击 触发方式为角色站立时按下J(攻击)键,角 ...

  6. HTML5 2D平台游戏开发#10Wall Jump

    这个术语不知道怎么翻译比较贴切,但并不妨碍对字面意思的理解,大概就是飞檐走壁.比如: 这是游戏<忍者龙剑传>中的场景,玩家可以通过操纵角色在墙面上移动并跳跃. 首先需要实现角色抓墙这一动作 ...

  7. HTML5 2D平台游戏开发#9蓄力技

    在很多动作游戏中,玩家操控的角色可以施放出比普通攻击更强力的蓄力技,一般操作为按住攻击键一段时间然后松开,具体效果像下面这张图: 要实现这个操作首先要记录下按键被按住的时间,初始是0: this.sa ...

  8. HTML5 2D平台游戏开发#2跳跃与二段跳

    在上一篇<Canvas制作时间与行为可控的sprite动画>中已经实现了角色的左右移动,本篇继续实现角色的一系列动作之一:跳跃.先来看看最终效果: 要实现跳跃,必须模拟垂直方向的速度和重力 ...

  9. HTML5 2D平台游戏开发#1

    在Web领域通常会用到一组sprite来展示动画,这类动画从开始到结束往往不会有用户参与,即用户很少会用控制器(例如鼠标.键盘.手柄.操作杆等输入设备)进行操作.但在游戏领域,sprite动画与控制器 ...

随机推荐

  1. mysql里的知识

    1.mysql基础 (1)mysql存储结构:数据库->表-> 数据   sql语句 (2)管理数据库: 增加: create database 数据库 default character ...

  2. Unity3d之MonoBehavior的各个函数的执行顺序,回调,顺序,次数等

    Update 当MonoBehaviour启用时,其Update在每一帧被调用.仅调用一次(每帧) LateUpdate 当Behaviour启用时,  每帧调用一次: FixedUpdate 当Mo ...

  3. HDU 2586 How far away? LCA 转化成RMQ

    链接:http://acm.hdu.edu.cn/showproblem.php?pid=2586 [题意] 给出一个N 个和N-1条边的连通图,询问任意两点间的距离.N<=40000 . [分 ...

  4. linux-配置字符串-grep

    grep -rn "hello,world!" * * : 表示当前目录所有文件,也可以是某个文件名 -r 是递归查找 -n 是显示行号 -R 查找所有文件包含子目录 -i 忽略大 ...

  5. Linux下Shell的复制粘贴快捷键

    [Shift]+[Insert]:复制 [Ctrl]+[Insert]:粘贴

  6. po_文件格式[转]

    原文: http://cpp.ezbty.org/content/science_doc/po_%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F 摘要:PO 是一种 GNU 定 ...

  7. linux下打包,压缩,解压缩

    Linux下最常用的打包程序就是tar了,使用tar程序打出来的包我们常称为tar包,tar包文件的命令通常都是以.tar结尾的.生成tar包后,就可以用其它的程序来进 行压缩了,所以首先就来讲讲ta ...

  8. What is Mocking?

    Mocking is primarily used in unit testing. An object under test may have dependencies on other (comp ...

  9. Python中使用XMLRPC(入门)

    一.简介 RPC是Remote Procedure Call的缩写,翻译成中文为:远程方法调用. 它是一种在本地机器上调用远端机器上的一个过程(方法)的技术,这个过程也被大家称为“分布式计算”,是为了 ...

  10. django验证码django-simple-captha

    搭建网站很经常要用到验证码,django中就有这样的中间件django-simple-captha githup地址https://github.com/mbi/django-simple-captc ...