Реализация движения игрока с помощью setInterval

Если вы закрываете голосование или голосуете против вопроса, прокомментируйте, почему. Я готов изменить это, но я могу сделать это только если ты скажешь мне, что не так.

В настоящее время я пытаюсь реализовать движение игрока для похожей на бомбермена игры, которую я пишу. Макет очень похож на этот:

Приоритеты направления движения
По сути, когда вы нажимаете одну из клавиш со стрелками, игрок должен начать двигаться в этом направлении, пока не достигнет блока. (Это уже работает)

Но это сложнее. Например, когда вы держите left а затем также up игрок должен двигаться up пока он не ударяет блок, то он должен попытаться пойти left пока он не может идти up снова или попадает в блок.

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

Положение с плавающей точкой

Игрок не просто перемещается с поля (0|0) на поле (0|1). Игрок имеет фиксированную скорость, настроенную в переменной (по умолчанию 1 поле в секунду), и его положение будет обновляться каждые ~10 миллисекунд. Лаги могут привести к задержке обновления позиции на несколько секунд.

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

Проблема столкновения

У игрока точно такая же ширина и высота, как у любого другого элемента в игре. Это означает, что для того, чтобы перейти от (0|2.0001312033) к (1|2), вы должны сначала точно достичь (0|2), чтобы игрок не сталкивался с блоками на (1|1) и (1|3) и только тогда вы действительно сможете пройти, чтобы достичь (1|2).

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

Пропуск поля

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


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

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

Может быть, есть какая-нибудь библиотека игровых движений, которая может помочь? Есть другие идеи?

Мой код далеко

Это ключевые части моего текущего кода. Я пытался удалить все ненужные части.

"use strict";
class Player {
    constructor(gameField, pos) { // gameField is an object containing lots of methods and properties like the game field size and colision detection fucntions
        // this._accuratePos is the floating point position
        this._accuratePos = JSON.parse(JSON.stringify(pos));
        // this.pos is the integer posision 
        this.pos = JSON.parse(JSON.stringify(pos));
        // this.moveSpeed is the movement speed of the player
        this.moveSpeed = 3;
        // this.activeMoveActions will contain the currently pressed arrow keys, sorted by priority. (last pressed, highest prio)
        this.activeMoveActions = []
        // this.moveInterval will contain an interval responsible for updating the player position
        this.moveInterval;
    }

    // directionKey can be 'up', 'down', 'left' or 'right'
    // newState can be true if the key is pressed down or false if it has been released.
    moveAction(directionKey, newState=true) { // called when a key is pressed or released. e.g. moveAction('left', false) // left key released
        if (this.activeMoveActions.includes(directionKey)) // remove the key from the activeMoveActions array
            this.activeMoveActions = this.activeMoveActions.filter(current => current !== directionKey);
        if (newState) // if the key was pressed down
            this.activeMoveActions.unshift(directionKey); // push it to the first position of the array

        if (this.activeMoveActions.length === 0) { // if no direction key is pressed
            if (this.moveInterval) { // if there still is a moveInterval
                clearInterval(this.moveInterval); // remove the moveInterval
            return; // exit the function
        }

        let lastMoveTime = Date.now(); // store the current millisecond time in lastMoveTime
        let lastAccPos = JSON.parse(JSON.stringify(this.accuratePos)); // store a copy of this.accuratePos in lastAccPos

        this.moveInterval = setInterval(()=>{
            let now = Date.now(); // current time in milliseconds
            let timePassed = now-lastMoveTime; // time passed since the last interval iteration in milliseconds
            let speed = (this.moveSpeed*1)/1000; // the movement speed in fields per millisecond
            let maxDistanceMoved = timePassed*speed; // the maximum distance the player could have moved (limited by hitting a wall etc)
            // TODO: check if distance moved > 1 and if so check if user palyer went through blocks

            let direction = this.activeMoveActions[0]; // highest priority direction
            // this.activeMoveActions[1] would contain the second highest priority direction if this.activeMoveActions.length > 1

            let newAccPos = JSON.parse(JSON.stringify(lastAccPos)); // store a copy of lastAccPos in newAccPos
            // (newAccPos will not necessarily become the new player posision. only if it's a valid position.)
            if (direction === 'up') { // if the player pressed the arrow up key
                newAccPos.y -= maxDistanceMoved; // subtract the maxDistanceMoved from newAccPos.y 
            } else if (direction === 'down') {
                newAccPos.y += maxDistanceMoved;
            } else if (direction === 'left') {
                newAccPos.x -= maxDistanceMoved;
            } else if (direction === 'right') {
                newAccPos.x += maxDistanceMoved;
            }

            // if it is possible to move the plyer to the new position in stored in newAccPos 
            if (!this.gameField.posIntersectsMoveBlockingElement(newAccPos) && this.gameField.posIsOnField(newAccPos)) {
                this.accuratePos = JSON.parse(JSON.stringify(newAccPos)); // set the new player position to a copy of the modified newAccPos
            } else { // if the newly calculated position is not a possible position for the player
                this.accuratePos = JSON.parse(JSON.stringify(this.pos)); // overwrite the players accurate position with a copy of the last rounded position
            }

            realityCheck(); // handle colliding items and explosions
            lastMoveTime = now; // store the time recorded in the beginning of the interval function
            lastAccPos = JSON.parse(JSON.stringify(newAccPos)); // store a copy of the new position in lastAccPos
        }, 10); // run this function every 10 milliseconds
    }
    set accuratePos(newAccPos) {
        let newPos = { // convert to rounded position
            x: Math.round(newAccPos.x),
            y: Math.round(newAccPos.y)
        };
        if (this.gameField.posIsOnField(newPos)) { // if the posision is on the game field
            this._accuratePos = JSON.parse(JSON.stringify(newAccPos));
            this.pos = newPos; 
        }
    }
    get accuratePos() {
        return this._accuratePos;
    }
    realityCheck() { 
        // ignore this method, it simply checks if the current position collides with an items or an explosion
    }

}

2 ответа

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

Первое и самое важное: представить игровой цикл http://gameprogrammingpatterns.com/game-loop.html. Не выпускайте никаких "setIntervals" внутри вашего игрового кода. Для каждого кадра в вашей игре сделайте следующее:

  • Читать состояние контроллера

  • Подать команду на базовый игровой объект

  • Вызовите update(timeMillis) для всех игровых объектов, чтобы выполнить команды

В рамках реализации игрового цикла вы можете решить проблему "отставание на секунду". Например, установив минимальное время в миллисекундах на 100 мс (т. Е. Если игра меньше 10 FPS, сам игровой процесс будет замедляться). Есть много других вариантов, просто прочитайте о различных подходах игрового цикла.

Затем атакуйте реализацию контроллера. Реализуйте отдельный четко определенный объект контроллера, возможно что-то вроде (TypeScript):

class Controller {
  public update(timeMillis: number);
  public getPrimaryDirection(): Vector;
  public getSecondaryDirection(): Vector;
}

Проверьте это внутри игрового цикла с console.log, затем отложите его.

Затем сфокусируйтесь на объекте LevelGrid и вашей проблеме столкновения. Существует множество способов решения проблем, о которых вы упоминаете, при столкновении и навигации. Вот несколько советов о возможных решениях:

  • Простой подход "шаги фиксированного размера". Выберите небольшую дельту фиксированного размера (например, 0,1 пикселя или 1 пиксель). Внутри игрового цикла создайте под-цикл, который будет перемещать игрока на фиксированный шаг в правильном направлении, тогда как LevelGrid.canMoveTo(x, y, w, h) возвращает true. Вычтите время, необходимое для перехода на 0,1 пикселя от остальной части TimeDelta. Когда остальная часть TimeDelta меньше нуля, выйдите из subloop. При реализации LevelGrid.canMoveTo правильно проверяйте расстояние "эпсилон", чтобы точность с плавающей запятой не повредила вам.

  • Подход "физика кастинга". Сделайте функцию LevelGrid.castRectangle(x, y, w, h, dx, dy). Он вычислит следующее: если прямоугольник с заданным размером будет двигаться в заданном направлении, где именно он попадет в первую стену? Вы можете найти реализацию во многих библиотеках физики или игровых движках.

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

Использование связанных ячеек карты.

Как и связанные списки, связанные карты упрощают навигацию по сложным лабиринтам.

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

Клетка.

Сначала вы определяете каждую ячейку карты для хранения позиции ячейки, x, y,

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

Таким образом, клетка может выглядеть как

cell = {
    x,y, // the cell pos
    left : null, // left is blocked
    right : cell, // a cell to the right that can be moved to 
    up    : cell, // a cell above that can be moved to 
    down  : cell, // a cell below that can be moved to 
}

Ячейка слева также должна указывать на ячейку справа. таким образом cell.left.right === cell, Или два шага влево и назад cell.left.left.right.right === cell

Игрок

Игрок держит ячейку, которая находится под ним (всегда начинается непосредственно над ячейкой) player.cell = ?

Легко проверить, может ли игрок двигаться в каком-либо направлении. Скажем, левый элемент управления нажат, просто проверьте if(keyboard.left && player.cell.left) { //can move left, И позиция для перемещения сохраняется в ячейке

Поскольку вы не хотите, чтобы игрок мгновенно переходил к следующей ячейке, вы сохраняете эту ячейку и с помощью счетчика начинаете движение к новой ячейке. Значения счетчика от 0 до 1, где 0 соответствует текущей ячейке, а 1 - следующей.

Когда счет достигнет 1, установите ячейку под игроком на следующую ячейку, установите счетчик на 0 и повторите процесс. Проверьте вход, затем проверьте ячейку для ячейки в этом направлении, переместите, если так.

Если игрок меняет направление, просто уменьшите счетчик до нуля.

Приоритетное направление движения

Отслеживая последнее движение, вы можете использовать массив для определения очередности следующих движений.

const checkMoveOrder = {
   left : ["up","down","right","left"],
   right : ["up","down","left","right"],
   up : ["left","right","down","up"],
   down : ["left","right","up","down"],
}

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

Много других преимуществ.

  • Полностью исключает необходимость тестирования столкновения карты игрока.

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

  • Делает проецирование позиции игрока вперед во времени очень легко. Просто следуйте за клетками из клетки под игроком.

  • Наряду с движением игрока это помогает и неигровым персонажам, что значительно упрощает поиск путей.

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

  • Игрок никогда не может застрять, так как вы не можете перейти в место, где нет выхода.

Другие вопросы по тегам