Написание простой, логической игры Sokoban на JavaScript

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,752
Реакции
1,448
Credits
25,205
Несколько вещей, которые я узнал, делая простую игру-головоломку Sokoban на JavaScript.

Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся-версия .

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

Вот игра которую я сделал:

sokoban.gif


Оригинальная игра имеет немного лучшую графику:

sokoban-original.gif


В моей версии большая синяя точка-это символ, розовые точки-места хранения, а оранжевые блоки - ящики.

Я написал его на лету в течение нескольких часов. Создание маленьких игр сильно отличается от того, что я обычно делаю на работе, поэтому я нашел это забавным и достижимым испытанием. К счастью, с некоторыми предыдущими проектами (Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся) У меня был некоторый опыт работы с концепцией построения координат.

Поехали!

Карта и объекты​

Первое, что я сделал, это построил карту, которая представляет собой двумерный массив, где каждая строка соответствует координате y, а каждый столбец-координате x.
JavaScript:
const map = [
  ['y0 x0', 'y0 x1', 'y0 x2', 'y0 x3'],
  ['y1 x0', 'y1 x1', 'y1 x2', 'y1 x3'],
  // ...etc
]
Так что доступ map[0][0]будет y0 x0и map[1][3]будет y1 x3.
Оттуда легко сделать карту, основанную на существующем уровне Сокобана, где каждая координата является сущностью в игре - ландшафт, игрок и т. Д.
Сущности
JavaScript:
const EMPTY = 'empty'
const WALL = 'wall'
const BLOCK = 'block'
const SUCCESS_BLOCK = 'success_block'
const VOID = 'void'
const PLAYER = 'player'

Карта
JavaScript:
onst map = [
  [EMPTY, EMPTY, WALL, WALL, WALL, WALL, WALL, EMPTY],
  [WALL, WALL, WALL, EMPTY, EMPTY, EMPTY, WALL, EMPTY],
  [WALL, VOID, PLAYER, BLOCK, EMPTY, EMPTY, WALL, EMPTY],
  // ...etc

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

Логика игры​

Есть не так уж много действий, о которых стоит беспокоиться. Игрок может двигаться ортогонально - вверх, вниз, влево и вправо - и есть несколько вещей, чтобы рассмотреть:
  • PLAYERИ BLOCKне может двигаться через а WALL
  • PLAYERИ BLOCKможет перемещаться через EMPTYпространство или VOIDпространство (место хранения).
  • Игрок может нажать кнопку a BLOCK
  • А BLOCKстановится аSUCCESS_BLOCK, когда оно находится на вершине а VOID.
И это буквально все. Я также закодировал еще одну вещь, которая не является частью оригинальной игры, но для меня она имела смысл:
  • А BLOCKможет подтолкнуть все остальные BLOCKфигуры
Когда игрок толкает блок, который находится рядом с другими блоками, все блоки будут двигаться до тех пор, пока он не столкнется со стеной.

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

Движение​

Поэтому первое, что нам нужно сделать в любое время, когда происходит изменение, - это найти текущие координаты игрока и то, какой тип сущности находится выше, ниже, слева и справа от них.
JavaScript:
function findPlayerCoords() {
  const y = map.findIndex(row => row.includes(PLAYER))
  const x = map[y].indexOf(PLAYER)

  return {
    x,
    y,
    above: map[y - 1][x],
    below: map[y + 1][x],
    sideLeft: map[y][x - 1],
    sideRight: map[y][x + 1],
  }
}
Теперь, когда у вас есть игрок и соседние координаты, каждое действие будет действием перемещения. Если игрок пытается пройти через проходимую ячейку (пустую или пустую), просто переместите игрока. Если игрок пытается толкнуть блок, переместите игрока и блок. Если соседний блок-стена, ничего не делайте.
JavaScript:
function move(playerCoords, direction) {
  if (isTraversible(adjacentCell[direction])) {
    movePlayer(playerCoords, direction)
  }

  if (isBlock(adjacentCell[direction])) {
    movePlayerAndBlocks(playerCoords, direction)
  }
}
Используя начальное игровое состояние, вы можете выяснить, что там должно быть. Пока я передаю направление функции, я могу установить новые координаты - добавление или удаление a yбудет вверх и вниз, добавление или удаление an x будет влево или вправо.
JavaScript:
function movePlayer(playerCoords, direction) {
  // Replace previous spot with initial board state (void or empty)
  map[playerCoords.y][playerCoords.x] = isVoid(levelOneMap[playerCoords.y][playerCoords.x])
    ? VOID
    : EMPTY

  // Move player
  map[getY(playerCoords.y, direction, 1)][getX(playerCoords.x, direction, 1)] = PLAYER
}
Если игрок перемещает блок, я написал небольшую рекурсивную функцию, чтобы проверить, сколько блоков находится в ряду, и как только у него будет это количество, он проверит, что такое соседняя сущность, переместит блок, если это возможно, и переместит игрока, если блок переместился.
JavaScript:
function countBlocks(blockCount, y, x, direction, board) {
  if (isBlock(board[y][x])) {
    blockCount++
    return countBlocks(blockCount, getY(y, direction), getX(x, direction), direction, board)
  } else {
    return blockCount
  }
}

const blocksInARow = countBlocks(1, newBlockY, newBlockX, direction, map)
Затем, если блок можно переместить, он просто переместит его или переместит и преобразует его в блок успеха, если он находится над хранилищем, а затем переместит игрока.
JavaScript:
map[newBoxY][newBoxX] = isVoid(levelOneMap[newBoxY][newBoxX]) ? SUCCESS_BLOCK : BLOCK
movePlayer(playerCoords, direction)

Визуализация​

Легко отслеживать всю игру в 2D-массиве и выводить обновленную игру на экран с каждым движением. Игровой тик невероятно прост - каждый раз, когда происходит событие keydown для up, down, left, right (или w, a, s, d для интенсивных игроков)move(), будет вызвана функция, которая использует индекс игрока и соседние типы ячеек, чтобы определить, каким должно быть новое, обновленное состояние игры. После изменения render()вызывается функция, которая просто окрашивает всю доску в обновленное состояние.
JavaScript:
const sokoban = new Sokoban()
sokoban.render()

// re-render
document.addEventListener('keydown', event => {
  const playerCoords = sokoban.findPlayerCoords()

  switch (event.key) {
    case keys.up:
    case keys.w:
      sokoban.move(playerCoords, directions.up)
      break
    case keys.down:
    case keys.s:
      sokoban.move(playerCoords, directions.down)
      break
    case keys.left:
    case keys.a:
      sokoban.move(playerCoords, directions.left)
      break
    case keys.right:
    case keys.d:
      sokoban.move(playerCoords, directions.right)
      break
    default:
  }

  sokoban.render()
})
Функция рендеринга просто отображает каждую координату и создает прямоугольник или круг нужного цвета.
JavaScript:
function render() {
  map.forEach((row, y) => {
    row.forEach((cell, x) => {
      paintCell(context, cell, x, y)
    })
  })
}
В основном весь рендеринг в HTML canvas состоит из пути для контура (обводки) и пути для внутренней части (заливки). Поскольку один пиксель на координату был бы довольно крошечной игрой, я умножил каждое значение на а multipler, которое 75в данном случае было пикселями.
JavaScript:
function paintCell(context, cell, x, y) {
  // Create the fill
  context.beginPath()
  context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
  context.fillStyle = colors[cell].fill
  context.fill()

  // Create the outline
  context.beginPath()
  context.rect(x * multiplier + 5, y * multiplier + 5, multiplier - 10, multiplier - 10)
  context.lineWidth = 10
  context.strokeStyle = colors[cell].stroke
  context.stroke()
}

Вывод​

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

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