Подтвердить что ты не робот

HTML5 Canvas Camera/ViewPort - как на самом деле это сделать?

Я уверен, что это было решено 1000 раз раньше: я получил холст размером 960 * 560 и комнату размером 5000 * 3000, из которых всегда нужно рисовать только 960 * 560, в зависимости от того, где игрок есть. Игрок должен быть всегда посередине, но когда он близок к границам - тогда должен быть рассчитан лучший вид). Игрок может свободно перемещаться с помощью WASD или клавиш со стрелками. И все объекты должны двигаться сами - вместо этого я перемещаю все остальное, кроме игрока, чтобы создать иллюзию, что игрок движется.

Теперь я нашел эти два quesitons:

HTML5 - Создание видового экрана для холста работает, но только для этого типа игры я не могу воспроизвести код для моего.

Изменение вида "center" холста html5, кажется, более перспективны и перфоманты, но я понимаю это только для правильного рисования всех других объектов относительно игрока, а не для того, чтобы прокрутить видовой экран холста относительно игрока, который я хочу достичь сначала, конечно.

Мой код (упрощенный - логика игры отдельно):

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 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?

Надеюсь, вы понимаете мое описание и у кого-то есть идея. С уважением

4b9b3361

Ответ 1

ЖИВОЙ ДЕМО на jsfiddle.net

Эта демонстрация иллюстрирует использование области просмотра в реальном игровом сценарии. Используйте клавиши со стрелками, чтобы переместить игрока по комнате. Большая комната создается на лету с помощью прямоangularьников, а результат сохраняется в изображении.

Обратите внимание, что игрок всегда находится посередине, за исключением тех случаев, когда он находится близко к границам (по вашему желанию).


Теперь я попытаюсь объяснить основные части кода, по крайней мере, те части, которые сложнее понять, просто взглянув на него.


Использование drawImage для рисования больших изображений в соответствии с положением области просмотра

Вариант метода drawImage имеет восемь новых параметров. Мы можем использовать этот метод, чтобы разрезать части исходного изображения и нарисовать их на холсте.

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

Первый параметр image, как и в других вариантах, является либо ссылкой на объект изображения, либо ссылкой на другой элемент canvas. Для остальных восьми параметров лучше всего посмотреть на изображение ниже. Первые четыре параметра определяют местоположение и размер среза на исходном изображении. Последние четыре параметра определяют положение и размер на целевом холсте.

Canvas drawImage

Шрифт: 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);

Рисование игровых объектов, связанных с окном просмотра

При написании игры рекомендуется разделять логику и рендеринг для каждого объекта в игре. Так что в демоверсии у нас есть функции update и draw. Метод update изменяет состояние объекта, например положение в "игровом мире", применяет физику, состояние анимации и т.д. Метод draw фактически отображает объект, и для его правильной визуализации с учетом области просмотра объект должен знать визуализацию. контекст и свойства области просмотра.

Обратите внимание, что игровые объекты обновляются с учетом положения игрового мира. Это означает, что (x, y) позиция объекта - это позиция в мире. Несмотря на это, поскольку видовой экран изменяется, объекты должны отображаться правильно, а позиция рендеринга будет отличаться от мировой позиции.

Преобразование просто:

положение объекта в мире (комнате): (x, y)
положение окна просмотра: (xView, yView)

позиция рендеринга: (x-xView, y-yView)

Это работает для всех видов координат, даже отрицательных.


Игровая камера

Наши игровые объекты имеют отдельный метод обновления. В демонстрационной реализации камера рассматривается как игровой объект и также имеет отдельный метод обновления.

Объект камеры удерживает левую верхнюю позицию области просмотра (xView, yView), объект, которому нужно следовать, прямоangularьник, представляющий область просмотра, прямоangularьник, который представляет границу игрового мира, и минимальное расстояние каждой границы, которое игрок мог пройти, прежде чем камера начнет двигаться ( xDeadZone, yDeadZone). Также мы определили степени свободы камеры (ось). Для игр в стиле сверху, таких как RPG, камера может перемещаться по оси x (по горизонтали) и по оси y (по вертикали).

Чтобы держать игрока в середине области просмотра, мы устанавливаем deadZone каждой оси, чтобы сходиться с центром холста. Посмотрите на следующую функцию в коде:

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

Примечание. См. раздел ОБНОВЛЕНИЕ ниже, так как это не приведет к ожидаемому поведению, если какое-либо измерение карты (комнаты) меньше холста.


Мировые ограничения

Поскольку каждый объект, включая камеру, имеет собственную функцию обновления, легко проверить границы игрового мира. Только не забудьте поместить код, который блокирует движение в конце функции обновления.


Демонстрация

Посмотрите полный код и попробуйте сами. Большинство частей кода содержат комментарии, которые проведут вас через. Я предполагаю, что вы знаете основы Javascript и как работать с прототипами (иногда я использую термин "класс" для объекта-прототипа только потому, что у него есть похожее поведение класса в таких языках, как Java).

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: 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 boundary (room 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 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 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 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 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

Если ширина и/или высота карты (комнаты) меньше, чем холст, предыдущий код не будет работать должным образом. Чтобы решить эту проблему, в игровом скрипте настройте камеру следующим образом:

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

Вам просто нужно сообщить конструктору камеры, что область просмотра будет наименьшим значением между картой (комнатой) или холстом. И так как мы хотим, чтобы игрок был центрирован и привязан к этому окну просмотра, функция camera.follow также должна быть обновлена.


Не стесняйтесь сообщать о любых ошибках или добавлять предложения.

Ответ 2

Код в принятом ответе немного. Это просто:

function draw() {
    ctx.setTransform(1,0,0,1,0,0);//reset the transform matrix as it is cumulative
    ctx.clearRect(0, 0, canvas.width, canvas.height);//clear the viewport AFTER the matrix is reset

    //Clamp the camera position to the world bounds while centering the camera around the player                                             
    var camX = clamp(-player.x + canvas.width/2, yourWorld.minX, yourWorld.maxX - canvas.width);
    var camY = clamp(-player.y + canvas.height/2, yourWorld.minY, yourWorld.maxY - canvas.height);

    ctx.translate( camX, camY );    

    //Draw everything
}

И зажим выглядит следующим образом:

function clamp(value, min, max){
    if(value < min) return min;
    else if(value > max) return max;
    return value;
}

Ответ 3

Вот как использовать холст для просмотра на другом изображении большего размера.

Видовой экран - это просто обрезанная часть большего изображения, которое отображается пользователю.

В этом случае окно просмотра будет отображаться пользователю на холсте (холст - это область просмотра).

Сначала введите код функции перемещения, который перемещает область просмотра вокруг большего изображения.

Эта функция перемещает верхний/левый угол окна просмотра на 5 пикселей в указанном направлении:

function move(direction){
    switch (direction){
        case "left":
            left-=5;
            break;
        case "up":
            top-=5;
            break;
        case "right":
            left+=5;
            break;
        case "down":
            top+=5
            break;
    }
    draw(top,left);
}

Функция перемещения вызывает функцию рисования.

В draw() функция drawImage обрезает указанную часть большего изображения.

drawImage также отобразит этот "обрезанный фон" для пользователя на холсте.

context.clearRect(0,0,game.width,game.height);
context.drawImage(background,cropLeft,cropTop,cropWidth,cropHeight,
                     0,0,viewWidth,viewHeight);

В этом примере

Фон - это полное фоновое изображение (обычно не отображаемое, а скорее источник для обрезки)

cropLeft и cropTop определяют, где на фоновом изображении начнется обрезка.

cropWidth и cropHeight определяют, как большой прямоугольник будет обрезаться из фонового изображения.

0,0 говорят, что суб-изображение, которое было обрезано из фона, будет нарисовано на 0,0 на холсте видового экрана.

viewWidth и viewHeight - это ширина и высота холста области просмотра

Итак, вот пример drawImage с использованием чисел.

Допустим, наш viewport (= наш холст дисплея) имеет ширину 150 пикселей и высоту 100 пикселей.

context.drawImage(background,75,50,150,100,0,0,150,100);

75 и 50 говорят, что обрезка начнется в положении x = 75/y = 50 на фоновом изображении.

150 100 говорят, что прямоугольник, который нужно обрезать, будет иметь ширину 150 и 100.

0,0,150,100 говорят, что изображение с обрезанным прямоугольником будет отображаться с использованием полного размера холста видовой области.

Это для механики рисования видового экрана... просто добавьте ключевые элементы управления!

Вот код и скрипт: http://jsfiddle.net/m1erickson/vXqyc/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>

<style>
    body{ background-color: ivory; }
    canvas{border:1px solid red;}
</style>

<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var game=document.getElementById("game");
    var gameCtx=game.getContext("2d");

    var left=20;
    var top=20;

    var background=new Image();
    background.onload=function(){
        canvas.width=background.width/2;
        canvas.height=background.height/2;
        gameCtx.fillStyle="red";
        gameCtx.strokeStyle="blue";
        gameCtx.lineWidth=3;
        ctx.fillStyle="red";
        ctx.strokeStyle="blue";
        ctx.lineWidth=3;
        move(top,left);
    }
    background.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/game.jpg";


    function move(direction){
        switch (direction){
            case "left":
                left-=5;
                break;
            case "up":
                top-=5;
                break;
            case "right":
                left+=5;
                break;
            case "down":
                top+=5
                break;
        }
        draw(top,left);
    }

    function draw(top,left){
        ctx.clearRect(0,0,canvas.width,canvas.height);
        ctx.drawImage(background,0,0,background.width,background.height,0,0,canvas.width,canvas.height);
        gameCtx.clearRect(0,0,game.width,game.height);
        gameCtx.drawImage(background,left,top,250,150,0,0,250,150);
        gameCtx.beginPath();
        gameCtx.arc(125,75,10,0,Math.PI*2,false);
        gameCtx.closePath();
        gameCtx.fill();
        gameCtx.stroke();
        ctx.beginPath();
        ctx.rect(left/2,top/2,125,75);
        ctx.stroke();
        ctx.beginPath();
        ctx.arc(left/2+125/2,top/2+75/2,5,0,Math.PI*2,false);
        ctx.stroke();
        ctx.fill();
    }

    $("#moveLeft").click(function(){move("left");}); 
    $("#moveRight").click(function(){move("right");}); 
    $("#moveUp").click(function(){move("up");}); 
    $("#moveDown").click(function(){move("down");}); 

}); // end $(function(){});
</script>

</head>

<body>
    <canvas id="game" width=250 height=150></canvas><br>
    <canvas id="canvas" width=500 height=300></canvas><br>
    <button id="moveLeft">Left</button>
    <button id="moveRight">Right</button>
    <button id="moveUp">Up</button>
    <button id="moveDown">Down</button>
</body>
</html>

Ответ 4

То, как вы сейчас это делаете, кажется мне правильным. Я бы изменил границы "20" на переменную, поэтому вы можете легко изменить границы уровня или всей игры, если это вам понадобится.

Вы можете абстрагировать эту логику в конкретном методе "Viewport", который просто обрабатывал бы вычисления, необходимые для определения того, где ваша "камера" должна быть на карте, а затем убедитесь, что координаты X и Y вашего символа совпадают центр вашей камеры.

Вы также можете перевернуть этот метод и определить местоположение камеры на основе положения символов (например, (position.x - (desired_camera_size.width / 2))) и вывести камеру из нее.

Когда вы определили положение своей камеры, вы можете начать беспокоиться о том, чтобы рисовать комнату как первый слой вашего холста.

Ответ 5

Это простой вопрос установки в окне просмотра целевых координат x и y, как утверждает Колтон, в каждом кадре. Преобразования не нужны, но могут использоваться по желанию. Формула, которая работала для меня, была:

function update() {

  // Assign the viewport to follow a target for this frame
  viewport.x = -target.x + canvas.width / 2;
  viewport.y = -target.y + canvas.height / 2;

  // Draw each entity, including the target, relative to the viewport
  ctx.fillRect(
    entity.x + viewport.x, 
    entity.y + viewport.y,
    entity.size,
    entity.size
  );
}

Прикрепление к карте является необязательным вторым шагом:

function update() {

  // Assign the viewport to follow a target for this frame
  viewport.x = -target.x + canvas.width / 2;
  viewport.y = -target.y + canvas.height / 2;

  // Keep viewport in map bounds
  viewport.x = clamp(viewport.x, canvas.width - map.width, 0);
  viewport.y = clamp(viewport.y, canvas.height - map.height, 0);

  // Draw each entity, including the target, relative to the viewport
  ctx.fillRect(
    entity.x + viewport.x, 
    entity.y + viewport.y,
    entity.size,
    entity.size
  );
}

// Restrict n to a range between lo and hi
const clamp = (n, lo, hi) => n < lo ? lo : n > hi ? hi : n;

Вот пример: https://jsfiddle.net/ggorlen/7yv7u572/

Ответ 6

Решение @gustavo-carvalho является феноменальным, но оно требует обширных вычислений и когнитивных издержек. Подход @Colton - шаг в правильном направлении; Жаль, что в его ответе недостаточно подробностей. Я взял его идею и побежал с ней, чтобы создать этот CodePen. Он достигает именно того, что @user2337969 просит использовать context.translate. Прелесть в том, что для этого не требуется смещать какие-либо карты или координаты игрока, поэтому рисовать их так же просто, как напрямую использовать их x и y, что гораздо проще.

Представьте, что 2D-камера представляет собой прямоугольник, который перемещается внутри большой карты. Его верхний левый угол находится в координатах (x, y) на карте, а его размер соответствует размеру холста, то есть canvas.width и canvas.height. Это означает, что x может варьироваться от 0 до map.width - canvas.width, а y от 0 до map.height - canvas.height (включительно). Это min и max которые мы вводим в метод clamp @Colton.

Однако, чтобы это работало, мне пришлось перевернуть знак x и y поскольку с помощью context.translate положительные значения смещают холст вправо (создавая иллюзию, будто камера перемещается влево), а отрицательные - влево ( как будто камера смещается вправо).