Реализация движения игрока с помощью 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"],
}
Таким образом, если последний ход игроков был оставлен, то в следующей ячейке ключи проверяются по порядку вверх, вниз, вправо, влево. игрок всегда поворачивается, если боковая клавиша направления не нажата.
Много других преимуществ.
Полностью исключает необходимость тестирования столкновения карты игрока.
Решает проблему выравнивания игрока, поскольку вы всегда располагаете выровненной ячейкой и расстоянием, на которое вы отошли от нее.
Делает проецирование позиции игрока вперед во времени очень легко. Просто следуйте за клетками из клетки под игроком.
Наряду с движением игрока это помогает и неигровым персонажам, что значительно упрощает поиск путей.
Вы можете использовать фиксированные временные шаги или случайные временные шаги, если вы переходите только от ячейки к ячейке.
Игрок никогда не может застрять, так как вы не можете перейти в место, где нет выхода.