HTML5 Canvas相机/视口-实际如何做? [英] HTML5 Canvas camera/viewport - how to actually do it?

查看:80
本文介绍了HTML5 Canvas相机/视口-实际如何做?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我确定这是在1000年前解决的:我得到了一个960 * 560大小的画布和一个5000 * 3000大小的房间,根据绘制位置的不同,总是只绘制960 * 560。玩家是。玩家应该始终处于中间位置,但是当靠近边界时-应该计算出最佳视角)。播放器可以使用WASD或箭头键完全自由移动。并且所有对象都应自行移动-而不是我移动玩家以外的所有其他物体,以创建玩家移动的错觉。



我现在找到了这两个问题:





演示中的工作方式:



我们有一个代表房间的大图像,我们只想在画布上显示视口中的零件。裁剪位置(sx,sy)与摄影机(xView,yView)相同,并且裁剪尺寸与视口(canvas)相同,因此 sWidth = canvas.width sHeight = canvas.height



我们需要注意裁切尺寸,因为 drawImage 如果裁切位置或裁切尺寸不会在画布上绘制任何内容根据排名无效。这就是为什么下面需要 if 节的原因。

  var sx,sy ,dx,dy; 
var sWidth,sHeight,dWidth,dHeight;

//裁剪图像的偏移点
sx = xView;
sy = yView;

//裁剪图像的尺寸
sWidth = context.canvas.width;
sHeight = context.canvas.height;

//如果裁剪后的图像小于画布,我们需要更改源尺寸
if(image.width-sx< sWidth){
sWidth = image.width- sx;
}
if(image.height-sy&sHeight){
sHeight = image.height-sy;
}

//在画布上绘制裁剪图像的位置
dx = 0;
dy = 0;
//将目标与源匹配,以不缩放图像
dWidth = sWidth;
dHeight = sHeight;

//绘制裁剪后的图像
context.drawImage(image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight);






绘制与视口相关的游戏对象



编写游戏时,最好将游戏中每个对象的逻辑和渲染分开。因此,在演示中,我们具有 update draw 函数。 update 方法更改对象状态,例如在游戏世界中的位置,应用物理,动画状态等。绘制方法实际上会渲染对象并考虑视口正确渲染它,该对象需要知道渲染上下文和视口属性。



请注意,考虑更新游戏对象游戏世界的位置。这意味着物体的(x,y)位置就是世界上的位置。尽管如此,由于视口在变化,因此需要正确渲染对象,并且渲染位置将与世界位置不同。



转换很简单:



世界(房间)中的对象位置:(x,y)

视口位置:(xView,yView)



渲染位置(x- xView,y-yView)



这适用于所有坐标,甚至是负坐标。






游戏摄像机



我们的游戏对象具有单独的更新方法。在演示实现中,相机被视为游戏对象,并且还具有单独的更新方法。



相机对象位于视口(xView,yView)的左上方,一个代表视口的矩形,一个代表游戏世界边界的矩形以及玩家在摄像机开始移动之前每个边界的最小距离(xDeadZone,yDeadZone)。我们还定义了相机的自由度(轴)。对于RPG等顶视图风格的游戏,允许摄像机在x(水平)轴和y(垂直)轴上移动。



将玩家保持在中间在视口中,我们将每个轴的deadZone设置为与画布的中心收敛。查看代码中的关注函数:


camera.follow(player,canvas.width / 2,canvas.height / 2)


注意:请参阅下面的更新部分,因为当地图的任何尺寸都不会产生预期的行为(房间)小于画布。






世界范围



由于每个对象(包括相机)都具有自己的更新功能,因此很容易检查游戏世界的边界。只记得在更新功能的最后放置阻塞移动的代码。






演示



查看完整代码并自己尝试。代码的大部分内容都带有注释,可以指导您完成操作。我假设您了解Javascript的基础知识以及如何使用原型(有时我将术语类用于原型对象,只是因为它在Java之类的语言中具有类的类似行为)。



实时演示

完整代码:

 <!DOCTYPE HTML> 
< html>
< body>
< canvas id = gameCanvas width = 400 height = 400 />
< script>
//我们的游戏类,方法和对象的包装器
window.Game = {};

//类的包装矩形
(function(){
function Rectangle(left,top,width,height){
this.left = left || 0;
this.top =顶部|| 0;
this.width =宽度|| 0;
this.height =高度|| 0;
this.right = this.left + this.width;
this.bottom = this.top + this.height;
}

Rectangle.prototype.set = function(left,top, / *可选* /宽度,/ *可选* /高度){
this.left = left;
this.top = top;
this.width = width || this.width;
this.height = height || this.height
this.right =(this.left + this.width);
this.bottom =(this.top + this.height);
}

Rectangle.prototype.within = function(r){
return(r.left< = this.left&
r.right > = this.right&&
r.top< = this.top&&
r.bottom> = this.bottom);
}

Rectangle.prototype.overlaps = function(r) {
return(this.left<右&&
r.left<这是正确的&&
this.top< r.bottom&&
r.top< this.bottom);
}

//将类矩形添加到我们的游戏对象
Game.Rectangle = Rectangle;
})();

//类相机的包装器(避免全局对象)
(function(){

//可能轴移动相机
var AXIS = {
NONE:1,
水平:2,
垂直:3,
两者:4
};

/ /相机构造函数
函数Camera(xView,yView,viewportWidth,viewportHeight,worldWidth,worldHeight){
//相机的位置(左上角坐标)
this.xView = xView || 0 ;
this.yView = yView || 0;

//摄像机开始移动之前从跟随对象到边界的距离
this.xDeadZone = 0; //到水平线的最小距离borders
this.yDeadZone = 0; //垂直边框的最小距离

//视口尺寸
this.wView = viewportWidth;
this.hView = viewportHeight;

//允许相机在垂直和水平轴上移动
this.axis = AXIS.BOTH;

//应该跟随的对象
this.followed = 空值;

//代表视口的矩形
this.viewportRect = new Game.Rectangle(this.xView,this.yView,this.wView,this.hView);

//代表世界边界(房间边界)的矩形
this.worldRect = new Game.Rectangle(0,0,worldWidth,worldHeight);

}

// gameObject需要具有 x和 y属性(作为世界(或房间)位置)
Camera.prototype.follow = function(gameObject,xDeadZone,yDeadZone){
this.followed = gameObject;
this.xDeadZone = xDeadZone;
this.yDeadZone = yDeadZone;
}

Camera.prototype.update = function(){
//继续跟随玩家(或其他想要的物体)
if(this.followed!= null){
if(this.axis == AXIS.HORIZONTAL || this.axis == AXIS.BOTH){
//根据跟随的对象位置
在水平轴上移动摄像机,如果(this.followed.x-this.xView + this.xDeadZone> this.wView)
this.xView = this.followed.x-(this.wView-this.xDeadZone);
else if(this.followed.x-this.xDeadZone&this.xView)
this.xView = this.followed.x-this.xDeadZone;

}
if(this.axis == AXIS.VERTICAL || this.axis == AXIS.BOTH){
//根据跟随的对象在垂直轴上移动相机
if(this.followed.y-this.yView + this.yDeadZone> this.hView)
this.yView = this.followed.y-(this.hView-this.yDeadZone);
else if(this.followed.y-this.yDeadZone&this.yView)
this.yView = this.followed.y-this.yDeadZone;
}

}

//更新viewportRect
this.viewportRect.set(this.xView,this.yView);

//不要让摄像机离开世界的边界
if(!this.viewportRect.within(this.worldRect)){
if(this.viewportRect.left< ; this.worldRect.left)
this.xView = this.worldRect.left;
if(this.viewportRect.top< this.worldRect.top)
this.yView = this.worldRect.top;
if(this.viewportRect.right> this.worldRect.right)
this.xView = this.worldRect.right-this.wView;
if(this.viewportRect.bottom> this.worldRect.bottom)
this.yView = this.worldRect.bottom-this.hView;
}

}

//将类相机添加到我们的游戏对象
Game.Camera =相机;

})();

//类播放器的包装器
(function(){
function Player(x,y){
//(x,y)= center对象
//注意:
//它表示玩家在世界(房间)上的位置,而不是画布位置
this.x = x;
this.y = y;

//移动速度以每秒像素为单位
this.speed = 200;

//渲染属性
this.width = 50;
this.height = 50;
}

Player.prototype.update = function(step,worldWidth,worldHeight){
//参数step是介于帧(以秒为单位)

//检查控件并相应地移动玩家
如果(Game.controls.left)
this.x-= this.speed * step;
if(Game.controls.up)
this.y-= this.speed *步骤;
if(Game.controls.right)
this.x + = this.speed * step;
if(Game.controls.down)
this.y + = this.speed * step;

//不要让玩家如果(this.x-this.width / 2< 0){
this.x = this.width / 2;
}
if(this.y-this.height / 2< 0){
this.y = this.height / 2;
}
if(this.x + this.width / 2> worldWidth){
this.x = worldWidth-this.width / 2;
}
if(this.y + this.height / 2> worldHeight){
this.y = worldHeight-this.height / 2;
}
}

Player.prototype.draw = function(context,xView,yView){
//绘制一个简单的矩形作为我们的播放器模型
context.save();
context.fillStyle =黑色;
// //在绘制之前,我们需要将玩家世界的位置转换为画布位置
context.fillRect((this.x-this.width / 2)-xView,(this.y-this.height / 2 )-yView,this.width,this.height);
context.restore();
}

//将类播放器添加到我们的游戏对象
Game.Player = Player;

})();

//类地图的包装器
(function(){
function Map(width,height){
//地图尺寸
这.width =宽度;
this.height =高度;

//地图纹理
this.image = null;
}

//创建一个程序生成的地图(您可以使用图像代替)
Map.prototype.generate = function(){
var ctx = document.createElement( canvas)。getContext( 2d );
ctx.canvas.width = this.width;
ctx.canvas.height = this.height;

var行= ~~(this.width / 44) + 1;
var列= ~~(this.height / 44)+1;

var color = red;
ctx.save();
ctx.fillStyle = red;
for(var x = 0,i = 0; i< rows; x + = 44,i ++){
ctx.beginPath();
for(var y = 0,j = 0; j<列; y + = 44,j ++){
ctx.rect(x,y,40,40);
}
color =(color == red? blue: red);
ctx.fillStyle =颜色;
ctx.fill();
ctx.closePath();
}
ctx.restore();

//将生成的地图存储为该图像纹理
this.image = new Image();
this.image.src = ctx.canvas.toDataURL( image / png);

//清除上下文
ctx = null;
}

//绘制调整为相机的地图
Map.prototype.draw = function(context,xView,yView){
//最简单的方法:绘制整个地图仅更改画布
//画布中的目标坐标将自行剔除图像(至少在硬件加速环境中没有性能差距->)
/*context.drawImage(this。图片,0、0,this.image.width,this.image.height,-xView,-yView,this.image.width,this.image.height); * /

// didactic方式(变量名中 s代表源, d代表目的地):

var sx,sy,dx,dy;
var sWidth,sHeight,dWidth,dHeight;

//裁剪图像的偏移点
sx = xView;
sy = yView;

//裁剪图像的尺寸
sWidth = context.canvas.width;
sHeight = context.canvas.height;

//如果裁剪后的图像小于画布,我们需要更改源尺寸
if(this.image.width-sx< sWidth){
sWidth = this。 image.width-sx;
}
if(this.image.height-sy&sHeight){
sHeight = this.image.height-sy;
}

//在画布上绘制裁剪图像的位置
dx = 0;
dy = 0;
//将目标与源匹配,以不缩放图像
dWidth = sWidth;
dHeight = sHeight;

context.drawImage(this.image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight);
}

//将类地图添加到我们的游戏对象
Game.Map =地图;

})();

//游戏脚本
(function(){
//对我们的游戏画布
进行预修复var canvas = document.getElementById( gameCanvas);
var context = canvas.getContext( 2d);

//游戏设置:
var FPS = 30;
var INTERVAL = 1000 / FPS; //毫秒
var STEP = INTERVAL / 1000 //秒

//设置代表房间的对象
var room = {
宽度:500,
高度:300,
地图:new Game.Map(500,300)
};

//为房间
room.map生成较大的图像纹理。 generate();

//设置玩家
var player = new Game.Player(50,50);

//旧的相机设置,不起作用
/ * var camera = new Game.Camera(0,0,canvas.width,canvas.height,room.width,room.height); * /
/ * camera.follow(player,canvas.width / 2,canvas.height / 2); * /

//设置正确的viewpor相机的t尺寸
var vWidth = Math.min(room.width,canvas.width);
var vHeight = Math.min(room.height,canvas.height);

//设置摄像头
var camera = new Game.Camera(0,0,vWidth,vHeight,room.width,room.height);
camera.follow(player,vWidth / 2,vHeight / 2);

//游戏更新功能
var update = function(){
player.update(STEP,room.width,room.height);
camera.update();
}

//游戏绘制函数
var draw = function(){
//清除整个画布
context.clearRect(0,0 ,canvas.width,canvas.height);

//重新绘制所有对象
room.map.draw(context,camera.xView,camera.yView);
player.draw(context,camera.xView,camera.yView);
}

//游戏循环
var gameLoop = function(){
update();
draw();
}

//<-配置播放/暂停功能:

//使用setInterval而不是requestAnimationFrame来更好地支持跨浏览器,
//,但更改为requestAnimationFrame polyfill很容易。

var runningId = -1;

Game.play = function(){
if(runningId == -1){
runningId = setInterval(function(){
gameLoop();
},间隔);
console.log( play);
}
}

Game.togglePause = function(){
if(runningId == -1){
Game.play();
} else {
clearInterval(runningId);
runningId = -1;
console.log( paused);
}
}

//->

})();

//<-配置游戏控件:

Game.controls = {剩余
:否,
向上:false,
正确:错误,
下来:错误,
};

window.addEventListener( keydown,function(e){
switch(e.keyCode){
case 37://向左箭头
Game.controls .left = true;
中断;
情况38://向上箭头
Game.controls.up = true;
中断;
情况39://右箭头
Game.controls.right = true;
中断;
case 40://向下箭头
Game.controls.down = true;
中断;
}
},否);

window.addEventListener( keyup,function(e){
switch(e.keyCode){
case 37://向左箭头
Game.controls .left = false;
中断;
情况38://向上箭头
Game.controls.up = false;
中断;
情况39://右箭头
Game.controls.right =否;
中断;
情况40://向下箭头
Game.controls.down = False;
中断;
case 80://键P暂停游戏
Game.togglePause();
休息;
}
},false);

//->

//加载页面后开始游戏
window.onload = function(){
Game.play();
}

< / script>
< / body>
< / html>



更新



如果地图(房间)的宽度和/或高度小于画布,则先前的代码将无法正常工作。要解决此问题,请在游戏脚本中按照以下步骤设置摄像头:

  //设置正确的视口大小相机
var vWidth = Math.min(room.width,canvas.width);
var vHeight = Math.min(room.height,canvas.height);

var camera = new Game.Camera(0,0,vWidth,vHeight,room.width,room.height);
camera.follow(player,vWidth / 2,vHeight / 2);

您只需要告诉相机构造函数,视口将是地图(房间)或帆布。并且由于我们希望播放器居中并绑定到该视口,因此 camera.follow 函数也必须进行更新。



< hr>

随时报告任何错误或添加建议。


I'm sure this was solven 1000 times before: I got a canvas in the size of 960*560 and a room in the size of 5000*3000 of which always only 960*560 should be drawn, depending on where the player is. The player should be always in the middle, but when near to borders - then the best view should be calculated). The player can move entirely free with WASD or the arrow keys. And all objects should move themselves - instead of that i move everything else but the player to create the illusion that the player moves.

I now found those two quesitons:

HTML5 - Creating a viewport for canvas works, but only for this type of game, i can't reproduce the code for mine.

Changing the view "center" of an html5 canvas seems to be more promising and also perfomant, but i only understand it for drawing all other objects correctly relative to the player and not how to scroll the canvas viewport relative to the player, which i want to achieve first of course.

My code (simplified - the game logic is seperately):

var canvas = document.getElementById("game");
canvas.tabIndex = 0;
canvas.focus();
var cc = canvas.getContext("2d");

// Define viewports for scrolling inside the canvas

/* Viewport x position */   view_xview = 0;
/* Viewport y position */   view_yview = 0;
/* Viewport width */        view_wview = 960;
/* Viewport height */       view_hview = 560;
/* Sector width */          room_width = 5000;
/* Sector height */         room_height = 3000;

canvas.width = view_wview;
canvas.height = view_hview;

function draw()
{
    clear();
    requestAnimFrame(draw);

    // World's end and viewport
    if (player.x < 20) player.x = 20;
    if (player.y < 20) player.y = 20;
    if (player.x > room_width-20) player.x = room_width-20;
    if (player.y > room_height-20) player.y = room_height-20;

    if (player.x > view_wview/2) ... ?
    if (player.y > view_hview/2) ... ?
}

The way i am trying to get it working feels totally wrong and i don't even know how i am trying it... Any ideas? What do you think about the context.transform-thing?

I hope you understand my description and that someone has an idea. Kind regards

解决方案

LIVE DEMO at jsfiddle.net

This demo illustrates the viewport usage in a real game scenario. Use arrows keys to move the player over the room. The large room is generated on the fly using rectangles and the result is saved into an image.

Notice that the player is always in the middle except when near to borders (as you desire).


Now I'll try to explain the main portions of the code, at least the parts that are more difficult to understand just looking at it.


Using drawImage to draw large images according to viewport position

A variant of the drawImage method has eight new parameters. We can use this method to slice parts of a source image and draw them to the canvas.

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

The first parameter image, just as with the other variants, is either a reference to an image object or a reference to a different canvas element. For the other eight parameters it's best to look at the image below. The first four parameters define the location and size of the slice on the source image. The last four parameters define the position and size on the destination canvas.

Font: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Using_images

How it works in demo:

We have a large image that represents the room and we want to show on canvas only the part within the viewport. The crop position (sx, sy) is the same position of the camera (xView, yView) and the crop dimensions are the same as the viewport(canvas) so sWidth=canvas.width and sHeight=canvas.height.

We need to take care about the crop dimensions because drawImage draws nothing on canvas if the crop position or crop dimensions based on position are invalid. That's why we need the if sections bellow.

var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;

// offset point to crop the image
sx = xView;
sy = yView;

// dimensions of cropped image          
sWidth =  context.canvas.width;
sHeight = context.canvas.height;

// if cropped image is smaller than canvas we need to change the source dimensions
if(image.width - sx < sWidth){
    sWidth = image.width - sx;
}
if(image.height - sy < sHeight){
    sHeight = image.height - sy; 
}

// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;          

// draw the cropped image
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);


Drawing game objects related to viewport

When writing a game it's a good practice separate the logic and the rendering for each object in game. So in demo we have update and draw functions. The update method changes object status like position on the "game world", apply physics, animation state, etc. The draw method actually render the object and to render it properly considering the viewport, the object need to know the render context and the viewport properties.

Notice that game objects are updated considering the game world's position. That means the (x,y) position of the object is the position in world. Despite of that, since the viewport is changing, objects need to be rendered properly and the render position will be different than world's position.

The conversion is simple:

object position in world(room): (x, y)
viewport position: (xView, yView)

render position: (x-xView, y-yView)

This works for all kind of coordinates, even the negative ones.


Game Camera

Our game objects have a separated update method. In Demo implementation, the camera is treated as a game object and also have a separated update method.

The camera object holds the left top position of viewport (xView, yView), an object to be followed, a rectangle representing the viewport, a rectangle that represents the game world's boundary and the minimal distance of each border that player could be before camera starts move (xDeadZone, yDeadZone). Also we defined the camera's degrees of freedom (axis). For top view style games, like RPG, the camera is allowed to move in both x(horizontal) and y(vertical) axis.

To keep player in the middle of viewport we set the deadZone of each axis to converge with the center of canvas. Look at the follow function in the code:

camera.follow(player, canvas.width/2, canvas.height/2)

Note: See the UPDATE section below as this will not produce the expected behavior when any dimension of the map (room) is smaller than canvas.


World's limits

Since each object, including camera, have its own update function, its easy to check the game world's boundary. Only remember to put the code that block the movement at the final of the update function.


Demonstration

See the full code and try it yourself. Most parts of the code have comments that guide you through. I'll assume that you know the basics of Javascript and how to work with prototypes (sometimes I use the term "class" for a prototype object just because it have a similar behavior of a Class in languages like Java).

LIVE DEMO

Full code:

<!DOCTYPE HTML>
<html>
<body>
<canvas id="gameCanvas" width=400 height=400 />
<script>
// wrapper for our game "classes", "methods" and "objects"
window.Game = {};

// wrapper for "class" Rectangle
(function() {
  function Rectangle(left, top, width, height) {
    this.left = left || 0;
    this.top = top || 0;
    this.width = width || 0;
    this.height = height || 0;
    this.right = this.left + this.width;
    this.bottom = this.top + this.height;
  }

  Rectangle.prototype.set = function(left, top, /*optional*/ width, /*optional*/ height) {
    this.left = left;
    this.top = top;
    this.width = width || this.width;
    this.height = height || this.height
    this.right = (this.left + this.width);
    this.bottom = (this.top + this.height);
  }

  Rectangle.prototype.within = function(r) {
    return (r.left <= this.left &&
      r.right >= this.right &&
      r.top <= this.top &&
      r.bottom >= this.bottom);
  }

  Rectangle.prototype.overlaps = function(r) {
    return (this.left < r.right &&
      r.left < this.right &&
      this.top < r.bottom &&
      r.top < this.bottom);
  }

  // add "class" Rectangle to our Game object
  Game.Rectangle = Rectangle;
})();

// wrapper for "class" Camera (avoid global objects)
(function() {

  // possibles axis to move the camera
  var AXIS = {
    NONE: 1,
    HORIZONTAL: 2,
    VERTICAL: 3,
    BOTH: 4
  };

  // Camera constructor
  function Camera(xView, yView, viewportWidth, viewportHeight, worldWidth, worldHeight) {
    // position of camera (left-top coordinate)
    this.xView = xView || 0;
    this.yView = yView || 0;

    // distance from followed object to border before camera starts move
    this.xDeadZone = 0; // min distance to horizontal borders
    this.yDeadZone = 0; // min distance to vertical borders

    // viewport dimensions
    this.wView = viewportWidth;
    this.hView = viewportHeight;

    // allow camera to move in vertical and horizontal axis
    this.axis = AXIS.BOTH;

    // object that should be followed
    this.followed = null;

    // rectangle that represents the viewport
    this.viewportRect = new Game.Rectangle(this.xView, this.yView, this.wView, this.hView);

    // rectangle that represents the world's boundary (room's boundary)
    this.worldRect = new Game.Rectangle(0, 0, worldWidth, worldHeight);

  }

  // gameObject needs to have "x" and "y" properties (as world(or room) position)
  Camera.prototype.follow = function(gameObject, xDeadZone, yDeadZone) {
    this.followed = gameObject;
    this.xDeadZone = xDeadZone;
    this.yDeadZone = yDeadZone;
  }

  Camera.prototype.update = function() {
    // keep following the player (or other desired object)
    if (this.followed != null) {
      if (this.axis == AXIS.HORIZONTAL || this.axis == AXIS.BOTH) {
        // moves camera on horizontal axis based on followed object position
        if (this.followed.x - this.xView + this.xDeadZone > this.wView)
          this.xView = this.followed.x - (this.wView - this.xDeadZone);
        else if (this.followed.x - this.xDeadZone < this.xView)
          this.xView = this.followed.x - this.xDeadZone;

      }
      if (this.axis == AXIS.VERTICAL || this.axis == AXIS.BOTH) {
        // moves camera on vertical axis based on followed object position
        if (this.followed.y - this.yView + this.yDeadZone > this.hView)
          this.yView = this.followed.y - (this.hView - this.yDeadZone);
        else if (this.followed.y - this.yDeadZone < this.yView)
          this.yView = this.followed.y - this.yDeadZone;
      }

    }

    // update viewportRect
    this.viewportRect.set(this.xView, this.yView);

    // don't let camera leaves the world's boundary
    if (!this.viewportRect.within(this.worldRect)) {
      if (this.viewportRect.left < this.worldRect.left)
        this.xView = this.worldRect.left;
      if (this.viewportRect.top < this.worldRect.top)
        this.yView = this.worldRect.top;
      if (this.viewportRect.right > this.worldRect.right)
        this.xView = this.worldRect.right - this.wView;
      if (this.viewportRect.bottom > this.worldRect.bottom)
        this.yView = this.worldRect.bottom - this.hView;
    }

  }

  // add "class" Camera to our Game object
  Game.Camera = Camera;

})();

// wrapper for "class" Player
(function() {
  function Player(x, y) {
    // (x, y) = center of object
    // ATTENTION:
    // it represents the player position on the world(room), not the canvas position
    this.x = x;
    this.y = y;

    // move speed in pixels per second
    this.speed = 200;

    // render properties
    this.width = 50;
    this.height = 50;
  }

  Player.prototype.update = function(step, worldWidth, worldHeight) {
    // parameter step is the time between frames ( in seconds )

    // check controls and move the player accordingly
    if (Game.controls.left)
      this.x -= this.speed * step;
    if (Game.controls.up)
      this.y -= this.speed * step;
    if (Game.controls.right)
      this.x += this.speed * step;
    if (Game.controls.down)
      this.y += this.speed * step;

    // don't let player leaves the world's boundary
    if (this.x - this.width / 2 < 0) {
      this.x = this.width / 2;
    }
    if (this.y - this.height / 2 < 0) {
      this.y = this.height / 2;
    }
    if (this.x + this.width / 2 > worldWidth) {
      this.x = worldWidth - this.width / 2;
    }
    if (this.y + this.height / 2 > worldHeight) {
      this.y = worldHeight - this.height / 2;
    }
  }

  Player.prototype.draw = function(context, xView, yView) {
    // draw a simple rectangle shape as our player model
    context.save();
    context.fillStyle = "black";
    // before draw we need to convert player world's position to canvas position            
    context.fillRect((this.x - this.width / 2) - xView, (this.y - this.height / 2) - yView, this.width, this.height);
    context.restore();
  }

  // add "class" Player to our Game object
  Game.Player = Player;

})();

// wrapper for "class" Map
(function() {
  function Map(width, height) {
    // map dimensions
    this.width = width;
    this.height = height;

    // map texture
    this.image = null;
  }

  // creates a prodedural generated map (you can use an image instead)
  Map.prototype.generate = function() {
    var ctx = document.createElement("canvas").getContext("2d");
    ctx.canvas.width = this.width;
    ctx.canvas.height = this.height;

    var rows = ~~(this.width / 44) + 1;
    var columns = ~~(this.height / 44) + 1;

    var color = "red";
    ctx.save();
    ctx.fillStyle = "red";
    for (var x = 0, i = 0; i < rows; x += 44, i++) {
      ctx.beginPath();
      for (var y = 0, j = 0; j < columns; y += 44, j++) {
        ctx.rect(x, y, 40, 40);
      }
      color = (color == "red" ? "blue" : "red");
      ctx.fillStyle = color;
      ctx.fill();
      ctx.closePath();
    }
    ctx.restore();

    // store the generate map as this image texture
    this.image = new Image();
    this.image.src = ctx.canvas.toDataURL("image/png");

    // clear context
    ctx = null;
  }

  // draw the map adjusted to camera
  Map.prototype.draw = function(context, xView, yView) {
    // easiest way: draw the entire map changing only the destination coordinate in canvas
    // canvas will cull the image by itself (no performance gaps -> in hardware accelerated environments, at least)
    /*context.drawImage(this.image, 0, 0, this.image.width, this.image.height, -xView, -yView, this.image.width, this.image.height);*/

    // didactic way ( "s" is for "source" and "d" is for "destination" in the variable names):

    var sx, sy, dx, dy;
    var sWidth, sHeight, dWidth, dHeight;

    // offset point to crop the image
    sx = xView;
    sy = yView;

    // dimensions of cropped image          
    sWidth = context.canvas.width;
    sHeight = context.canvas.height;

    // if cropped image is smaller than canvas we need to change the source dimensions
    if (this.image.width - sx < sWidth) {
      sWidth = this.image.width - sx;
    }
    if (this.image.height - sy < sHeight) {
      sHeight = this.image.height - sy;
    }

    // location on canvas to draw the croped image
    dx = 0;
    dy = 0;
    // match destination with source to not scale the image
    dWidth = sWidth;
    dHeight = sHeight;

    context.drawImage(this.image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
  }

  // add "class" Map to our Game object
  Game.Map = Map;

})();

// Game Script
(function() {
  // prepaire our game canvas
  var canvas = document.getElementById("gameCanvas");
  var context = canvas.getContext("2d");

  // game settings: 
  var FPS = 30;
  var INTERVAL = 1000 / FPS; // milliseconds
  var STEP = INTERVAL / 1000 // seconds

  // setup an object that represents the room
  var room = {
    width: 500,
    height: 300,
    map: new Game.Map(500, 300)
  };

  // generate a large image texture for the room
  room.map.generate();

  // setup player
  var player = new Game.Player(50, 50);

  // Old camera setup. It not works with maps smaller than canvas. Keeping the code deactivated here as reference.
  /* var camera = new Game.Camera(0, 0, canvas.width, canvas.height, room.width, room.height);*/
  /* camera.follow(player, canvas.width / 2, canvas.height / 2); */

  // Set the right viewport size for the camera
  var vWidth = Math.min(room.width, canvas.width);
  var vHeight = Math.min(room.height, canvas.height);

  // Setup the camera
  var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
  camera.follow(player, vWidth / 2, vHeight / 2);

  // Game update function
  var update = function() {
    player.update(STEP, room.width, room.height);
    camera.update();
  }

  // Game draw function
  var draw = function() {
    // clear the entire canvas
    context.clearRect(0, 0, canvas.width, canvas.height);

    // redraw all objects
    room.map.draw(context, camera.xView, camera.yView);
    player.draw(context, camera.xView, camera.yView);
  }

  // Game Loop
  var gameLoop = function() {
    update();
    draw();
  }

  // <-- configure play/pause capabilities:

  // Using setInterval instead of requestAnimationFrame for better cross browser support,
  // but it's easy to change to a requestAnimationFrame polyfill.

  var runningId = -1;

  Game.play = function() {
    if (runningId == -1) {
      runningId = setInterval(function() {
        gameLoop();
      }, INTERVAL);
      console.log("play");
    }
  }

  Game.togglePause = function() {
    if (runningId == -1) {
      Game.play();
    } else {
      clearInterval(runningId);
      runningId = -1;
      console.log("paused");
    }
  }

  // -->

})();

// <-- configure Game controls:

Game.controls = {
  left: false,
  up: false,
  right: false,
  down: false,
};

window.addEventListener("keydown", function(e) {
  switch (e.keyCode) {
    case 37: // left arrow
      Game.controls.left = true;
      break;
    case 38: // up arrow
      Game.controls.up = true;
      break;
    case 39: // right arrow
      Game.controls.right = true;
      break;
    case 40: // down arrow
      Game.controls.down = true;
      break;
  }
}, false);

window.addEventListener("keyup", function(e) {
  switch (e.keyCode) {
    case 37: // left arrow
      Game.controls.left = false;
      break;
    case 38: // up arrow
      Game.controls.up = false;
      break;
    case 39: // right arrow
      Game.controls.right = false;
      break;
    case 40: // down arrow
      Game.controls.down = false;
      break;
    case 80: // key P pauses the game
      Game.togglePause();
      break;
  }
}, false);

// -->

// start the game when page is loaded
window.onload = function() {
  Game.play();
}

</script>
</body>
</html>


UPDATE

If width and/or height of the map (room) is smaller than canvas the previous code will not work properly. To resolve this, in the Game Script make the setup of the camera as followed:

// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);

var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);

You just need to tell the camera constructor that viewport will be the smallest value between map (room) or canvas. And since we want the player centered and bonded to that viewport, the camera.follow function must be update as well.


Feel free to report any errors or to add suggestions.

这篇关于HTML5 Canvas相机/视口-实际如何做?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆