Переместить игрока на большой холст javascript игра - PullRequest
0 голосов
/ 21 февраля 2020

Я написал этот код. Это простой игровой движок. Несколько вещей все еще отсутствуют (столкновения, звуки, текстовые описания), но это только начало :-) Игрок может управляться с помощью стрелок. Но у меня есть 2 проблемы. Может быть, кто-то скажет мне что-то, как это сделать.

  1. Перемещение игрока сейчас, когда игрок движется, экран движется в другом темпе. Эффект заключается в том, что через короткое время игрок закрывается краем экрана.

Я подозреваю, что проблема заключается где-то здесь: ctx.translate (player.x, player.y); но я понятия не имею, что я написал неправильно в этом коде.

Вид с камеры. По умолчанию холст будет очень большим, до 10 000 x 10 000 точек. Несколько сотен дорожек и комнат (зеленые и песчаные прямоугольники) будут нарисованы на холсте. Я полагаю, что весь холст визуализируется в данный момент, даже за пределами экрана. И это, вероятно, значительная трата компьютерных ресурсов. Но я понятия не имею, как это сделать.

Конечно, если здесь есть что-то еще, я с благодарностью приму любые предложения.

window.addEventListener('load', function(event) {
  initCanvas();
});



let ctx;
let cW = 3000; // canvas width
let cH = 3000; // canvas height
let playerImgTop;
let playerImgBottom;
let playerImgLeft;
let playerImgRight;
let playerSpeed = 20;
let playerDir = 0;



function initCanvas() {
  ctx = document.getElementById('mycanvas').getContext('2d');
  ctx.canvas.width = cW;
  ctx.canvas.height = cH;
  let animateInterval = setInterval(render, 1000/30);

  playerImgTop = new Image();
  playerImgTop.src = "http://www.itbvega.pl/io/img/player-top.png";
  playerImgBottom = new Image();
  playerImgBottom.src = "http://www.itbvega.pl/io/img/player-bottom.png";
  playerImgLeft = new Image();
  playerImgLeft.src = "http://www.itbvega.pl/io/img/player-left.png";
  playerImgRight = new Image();
  playerImgRight.src = "http://www.itbvega.pl/io/img/player-right.png";

  let gameLocations = [
      {"id": "room0", "x": 180, "y": 180, "rw": 60, "rh": 60, "type": "room"},
      {"id": "room1", "x": 160, "y": 380, "rw": 100, "rh": 100, "type": "room"},
      {"id": "path0", "x": 200, "y": 240, "rw": 20, "rh": 140, "type": "path"}
  ];

  function renderGameLocations() {
    for (let i = 0; i < gameLocations.length; i++) {
      let loc = gameLocations[i];
      if (loc.type === "path") {
        ctx.fillStyle = "#62d299";
      } else if (loc.type === "room") {
        ctx.fillStyle = "#e4b65e";
      }
      ctx.fillRect(loc.x, loc.y, loc.rw, loc.rh);

    }
  }


  function render() {
    ctx.save();
    ctx.clearRect(0,0, cW, cH);

    renderGameLocations();

    player.render();

    ctx.translate(player.x, player.y);

    ctx.restore();
  }
}



function Player() {
  this.x = 200;
  this.y = 200;
  this.render = function() {
    if (playerDir === 0) {
      ctx.drawImage(playerImgTop, this.x, this.y);
    } else if (playerDir === 1) {
      ctx.drawImage(playerImgRight, this.x, this.y);
    } else if (playerDir === 2) {
      ctx.drawImage(playerImgBottom, this.x, this.y);
    } else if (playerDir === 3) {
      ctx.drawImage(playerImgLeft, this.x, this.y);
    }
  }
}

let player = new Player();


document.addEventListener('keydown', function(event) {
  let key_press = event.keyCode;
   //alert(event.keyCode + " | " + key_press);
  if (key_press === 38 ) { // top
    player.y -= playerSpeed;
    playerDir = 0;
  } else if (key_press === 40) { // bottom
    player.y += playerSpeed;
    playerDir = 2;
  } else if (key_press === 37) { // left
    player.x -= playerSpeed;
    playerDir = 3;
  } else if (key_press === 39) { // right
    player.x += playerSpeed;
    playerDir = 1;
  }
});
<canvas id="mycanvas"></canvas>

1 Ответ

0 голосов
/ 23 февраля 2020

Ваш вопрос охватывает слишком много вопросов для подробного ответа. Я разделил его на две темы: игровое поле и представление и Четырехъядерные деревья , которые кратко охватят два ваших вопроса.

Я добавил пример, который реализует оба предмета на самом базовом c уровне. Используйте пример, чтобы изучить более тонкие детали. Если у вас есть вопросы, задавайте их в комментариях или в качестве новых вопросов SO.

Игровое поле и вид

Игровое поле - это вся карта. (см. пример playfield). Он не содержит пикселей, он содержит только элементы для отображения и взаимодействия. (см. пример playfield.quadMap playfield.visibleItems и mapItems)

Представление - это холст.

Он не больше дисплея и содержит только пиксели.

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

Вид установлен относительно чего-то интересного в игровом поле. Например плеер. (см. пример playfield.setView)

Вы устанавливаете вид (top left), занимая положение игроков и вычитая половину ширины и высоты видов.

Чтобы убедиться, что вид не выходит за пределы игрового поля go, убедитесь, что верхний левый угол не меньше 0, а верхний левый плюс ширина и высота вида не больше ширины и высоты игрового поля. (см. пример playfield.setView)

Чтобы использовать вид, установите преобразование холста таким образом, чтобы его начало составляло -top и -left, а затем нарисуйте элементы в этом нормальном положении.

Quad деревья

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

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

В 2D это можно сделать с помощью специального типа связанного списка, называемого четырехугольным деревом.

  • Четырехугольное дерево состоит из квадратов. У каждого квадрата есть топор, позиция y, ширина и высота. Например, Quad
  • Первый квад имеет тот же размер, что и игровое поле. В примере это квадрат 16000 пикселей.
  • Каждый квад также может иметь 4 субквада, представляющих квад, разделенный на 4. См. Пример Quad.prototype.divide
  • Вы повторяете для каждого субквада, дайте каждому 4 больше суб квадров. Вы устанавливаете максимальную глубину, то есть количество раз, когда вы делите субквадр на большее количество субквадов. (в данном примере maxDepth равно 5)
  • Таким образом, наименьший квад в примере - это игровое поле size / (2 ** maxDepth) === 16000 / 32 === 500, означающее, что оно охватывает 500 на 500 пикселей игрового поля.
  • Внизу из дерева квадов вы храните элементы, которые перекрывают этот квад. Элемент может перекрывать несколько квадов, каждый квад должен перекрывать этот элемент. (в данном случае элемент представляет собой просто прямоугольник с x, y, w, h и col или)
  • Вы можете проверить, перекрывает ли какой-либо четырехугольник текущий вид. См. Пример (Quad.prototype.isInView). Если этого не происходит, он и все его подваги и элементы, которые они содержат, не видны. Это очень быстро устраняет необходимость проверять до 3/4 элементов.

Объект игрового поля содержит самый верхний элемент Quad. Когда вы устанавливаете представление playfield.setView, оно создает преобразование вида и создает карту всех элементов во всех видимых квадратах. Когда setView возвращает карту, playfield.visibleItem содержит все элементы в четырехугольниках, которые перекрывают вид.

Поскольку элементы карты могут быть в нескольких четырехугольниках одновременно, вам необходимо быстро создать список элементов без повторяя тот же пункт. Вы можете сделать это с помощью Map или Set (они встроены в JavaScript объекты) и позволяют избежать необходимости проверять, есть ли элемент в списке.

Пример

Используйте клавиши СТРЕЛКА для перемещения. Вы должны щелкнуть холст , чтобы начать, так как фрагменты SO не будут автоматически фокусировать клавиатуру.

Карта playfield очень велика 16 000 на 16 000 пикселей, и на карте 10 000 элементов.

Использование четырехугольного дерева для поиска видимых элементов позволяет анимировать вид в режиме реального времени со скоростью 60FPS.

const keys = { // keys to listen to
    ArrowUp: false,
    ArrowLeft: false,
    ArrowRight: false,
    ArrowDown: false,
};
document.addEventListener('keydown', keyEvent);
document.addEventListener('keyup', keyEvent);  
document.addEventListener("click",()=>requestAnimationFrame(mainLoop),{once:true});
var startTime;
var globalTime;
const mapItemCount = 10000;
const maxItemSize = 120; // in pixels
const minItemSize = 20; // in pixels
const maxQuadDepth = 5;
const playfieldSize = 16000; // in pixels
var id = 1;  // unique ids for map items
const mapItems = new Map();  // unique map items
const directions = {
    NONE: {idx: 0, vec: {x: 0, y: 0}},
    UP: {idx: 3, vec: {x: 0, y: -1}},
    RIGHT: {idx: 0, vec: {x: 1, y: 0}},
    DOWN: {idx: 1, vec: {x: 0, y: 1}},
    LEFT: {idx: 2, vec: {x: -1, y: 0}},
};
const ctx = canvas.getContext("2d");

function mainLoop(time) {
    if(!startTime) { startTime = time }
    globalTime = time - startTime;
    playfield.sizeCanvas();
    ctx.setTransform(1,0,0,1,0,0);
    ctx.clearRect(0,0,canvas.width,canvas.height);
    
    player.update();
    playfield.setView(player);  // current transform set to view
    
    playfield.drawVisible();
    player.draw();
    
    info.textContent = `Player: X:${player.x|0} Y${player.y|0}: , View Left:${playfield.left | 0} Top:${playfield.top | 0} , visibleItems: ${playfield.visibleItems.size} of ${mapItems.size}`;
    
    requestAnimationFrame(mainLoop);
}

function Quad(x, y, w, h, depth = maxQuadDepth) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    if (depth > 0) { this.divide(depth) }
    else { this.items = [] }
}
Quad.prototype = {
    divide(depth) {
        this.subQuads = [];
        this.subQuads.push(new Quad(this.x, this.y, this.w / 2, this.h / 2, depth - 1));
        this.subQuads.push(new Quad(this.x + this.w / 2, this.y, this.w / 2, this.h / 2, depth - 1));
        this.subQuads.push(new Quad(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, this.h / 2, depth - 1));
        this.subQuads.push(new Quad(this.x, this.y + this.h / 2, this.w / 2, this.h / 2, depth - 1));       
    },
    isInView(pf) {  // pf is playfield
        return !(this.x > pf.left + pf.cWidth || this.x + this.w < pf.left || this.y > pf.top + pf.cHeight || this.y + this.h < pf.top);
    },
    addItem(item) {
        if (!(item.x > this.x + this.w || item.x + item.w < this.x || item.y > this.y + this.h || item.y + item.h < this.y)) {
            if (this.subQuads) {
                for (const quad of this.subQuads) { quad.addItem(item) }
            } else { this.items.push(item.id) }
        }
    },
    getVisibleItems(pf, itemMap, items = new Map()) {
        if (this.subQuads) {
            for (const quad of this.subQuads) {
                if (quad.isInView(pf)) { quad.getVisibleItems(pf, itemMap, items) }
            }
        } else {
            for (const id of this.items) { items.set(id, itemMap.get(id)) }
        }
        return items
    }   
}
// only one instance then define as object
const playfield = {
    width: playfieldSize,
    height: playfieldSize,
    view: [1,0,0,1,0,0],  // view as transformation matrix
    cWidth: 0,  // canvas size
    cHeight: 0, // canvas size
    top: 0,
    left: 0,
    sizeCanvas() {
        if(canvas.width !== innerWidth || canvas.height !== innerHeight) {
            this.cWidth = canvas.width = innerWidth;
            this.cHeight = canvas.height = innerHeight;         
        }
    },
    setView(player) {
        var left = player.x - this.cWidth / 2;
        var top = player.y - this.cHeight / 2;
        left = left < 0 ? 0 : left > this.width - this.cWidth ? this.width - this.cWidth : left;
        top = top < 0 ? 0 : top > this.height - this.cHeight ? this.height - this.cHeight : top;
        this.view[4] = -(this.left = left);
        this.view[5] = -(this.top = top);
        ctx.setTransform(...this.view);
        this.visibleItems.clear();
        this.quadMap.getVisibleItems(this, mapItems, this.visibleItems);
    },
    drawVisible() {
        for(const item of this.visibleItems.values()) { item.draw() }  
    },
    quadMap: new Quad(0, 0, playfieldSize, playfieldSize),
    visibleItems: new Map(),
}

function MapItem(x, y, w, h, col = "#ABC") {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.col = "#ABC";
    this.id = id++;
    mapItems.set(this.id,this);
    playfield.quadMap.addItem(this);
}
MapItem.prototype = {
    draw() {
        ctx.fillStyle = this.col;
        ctx.fillRect(this.x, this.y, this.w, this.h);
    }
}
addMapItems(mapItemCount)
function addMapItems(count) {
    while (count-- > 0) {
        const x = Math.random() * playfield.width;
        const y = Math.random() * playfield.height;
        const w = Math.random() * (maxItemSize - minItemSize) + minItemSize;
        const h = Math.random() * (maxItemSize - minItemSize) + minItemSize;
        const item = new MapItem(x,y,w,h);
    }
}

// only one instance then define as object
const player = {
    x: 1200,
    y: 1200,
    speed: 10,
    image: undefined,
    direction: undefined,
    draw() {
        ctx.fillStyle = "#F00";
        ctx.save();  // need to save and restore as I use rotate to change the current transform that 
                     // holds the current playfield view.
        const x = this.x;
        const y = this.y;
        ctx.transform(1,0,0,1,x,y);
        ctx.rotate(this.direction.idx / 2 * Math.PI);
        ctx.beginPath();
        ctx.lineTo(20, 0);
        ctx.lineTo(-10, 14);
        ctx.lineTo(-10, -14);
        ctx.fill();
        ctx.restore();
        
    },
    update() {
        var dir = directions.NONE;
        if (keys.ArrowUp) { dir = directions.UP }
        if (keys.ArrowDown) { dir = directions.DOWN }
        if (keys.ArrowLeft) { dir = directions.LEFT }
        if (keys.ArrowRight) { dir = directions.RIGHT }
        this.x += dir.vec.x * this.speed;
        this.y += dir.vec.y * this.speed;
        this.x = this.x < 0 ? 0 : this.x > playfield.width ? playfield.width : this.x;
        this.y = this.y < 0 ? 0 : this.y > playfield.height ? playfield.height : this.y;
        this.direction = dir;
        
    }
};


function keyEvent(e) {
    if (keys[e.code] !== undefined) {
        keys[e.code] = e.type === "keydown";
        e.preventDefault();
    }
}
canvas {
    position: absolute;
    left: 0px;
    top: 0px;
}
#info {
    font-family: arial;
    position: absolute;
    left: 0px;
    top: 0px;
    font-size: small;
}
<canvas id="canvas"></canvas>
<div id="info">Click to start</div>
...