javascript – HTML5 Canvas相机/视口 – 如何实际操作?

我确定之前已经解决了1000次:我得到了一个960 * 560大小的画布和一个大小为5000 * 3000的房间,其中应该只画出960 * 560,这取决于玩家的位置.玩家应该始终位于中间,但是当接近边界时 – 应该计算最佳视图).玩家可以使用WASD或箭头键完全免费移动.并且所有物体都应该自己移动 – 而不是我移动除了玩家之外的所有其他东西以创造玩家移动的幻觉.

我现在发现了这两个问题:

HTML5 – Creating a viewport for canvas工作,但只有这种类型的游戏,我无法重现我的代码.

Changing the view “center” of an html5 canvas似乎更有前途,也更有说服力,但我只是理解它相对于玩家正确地绘制所有其他对象而不是如何相对于玩家滚动画布视口,我想首先实现它.

我的代码(简化 – 游戏逻辑是单独的):

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) ... ?
}

我试图让它工作的方式感觉完全错误,我甚至不知道我是如何尝试它…任何想法?你怎么看context.transform-thing?

我希望你理解我的描述,并且有人有一个想法.亲切的问候

解决方法:

LIVE DEMO在jsfiddle.net

此演示说明了真实游戏场景中的视口使用情况.使用箭头键将播放器移动到房间上方.使用矩形在运行中生成大房间,并将结果保存到图像中.

请注意,除非靠近边界(如您所愿),否则播放器始终位于中间位置.

现在我将尝试解释代码的主要部分,至少只是看到它时更难理解的部分.

使用drawImage根据视口位置绘制大图像

drawImage方法的一个变体有八个新参数.我们可以使用此方法切片源图像的部分并将它们绘制到画布上.

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

与其他变体一样,第一个参数图像是对图像对象的引用或对不同画布元素的引用.对于其他八个参数,最好查看下图.前四个参数定义源图像上切片的位置和大小.最后四个参数定义目标画布上的位置和大小.

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

它在演示中的工作原理:

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

我们需要注意裁剪尺寸,因为如果基于位置的裁剪位置或裁剪尺寸无效,drawImage不会在画布上绘制任何内容.这就是我们需要if部分的原因.

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);

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

在编写游戏时,将游戏中每个对象的逻辑和渲染分开是一个很好的做法.所以在演示中我们有更新和绘制功能.更新方法更改对象状态,如“游戏世界”上的位置,应用物理,动画状态等.绘制方法实际渲染对象并在考虑视口时正确渲染,对象需要知道渲染上下文和视口属性.

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

转换很简单:

世界(房间)中的物体位置:(x,y)
视口位置:(xView,yView)

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

这适用于所有类型的坐标,甚至是负坐标.

游戏相机

我们的游戏对象有一个单独的更新方法.在Demo实现中,摄像机被视为游戏对象,并且还具有单独的更新方法.

相机对象保持视口的左上角位置(xView,yView),要跟踪的对象,表示视口的矩形,表示游戏世界边界的矩形以及玩家在相机启动前可能出现的每个边界的最小距离move(xDeadZone,yDeadZone).我们还定义了相机的*度(轴).对于像RPG这样的俯视式游戏,允许摄像机在x(水平)和y(垂直)轴上移动.

为了将玩家保持在视口的中间,我们将每个轴的deadZone设置为与画布的中心会聚.查看代码中的follow函数:

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

世界的极限

由于每个对象(包括摄像头)都有自己的更新功能,因此很容易检查游戏世界的边界.只记得在更新函数的最后添加阻止移动的代码.

示范

查看完整代码并亲自尝试.比用文字解释要好得多.也许在阅读完代码后,这些信息将被澄清.

LIVE DEMO

完整代码:

<!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: "none", 
        HORIZONTAL: "horizontal", 
        VERTICAL: "vertical", 
        BOTH: "both"
    };

    // Camera constructor
    function Camera(xView, yView, canvasWidth, canvasHeight, 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 = canvasWidth;
        this.hView = canvasHeight;          

        // 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;
    }

    // generate an example of a large map
    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);

        // didatic way:

        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: 5000,
        height: 3000,
        map: new Game.Map(5000, 3000)
    };

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

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

    // setup the magic camera !!!
    var camera = new Game.Camera(0, 0, canvas.width, canvas.height, room.width, room.height);       
    camera.follow(player, canvas.width/2, canvas.height/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:

    // I'll use setInterval instead of requestAnimationFrame for compatibility reason,
    // but it's easy to change that.

    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>

随意报告任何错误或添加建议.

上一篇:javascript-如何在移动浏览器的屏幕顶部显示自定义警报?


下一篇:javascript – jQuery对话框不以viewport为中心,而是父元素