Рубрики
Без рубрики

Зомби стрелок

Привет! В этом посте я покажу вам, как создать простую 2D -игру с стрельбой в зомби, используя ваниль JS … Tagged with JavaScript, Gamedev.

Привет! В этом посте я покажу вам, как создать простую 2D -игру с стрельбой в зомби, используя ванильный JS и холст HTML5. Весь код можно найти на моем GitHub Анкет

Живая демонстрация

Этот проект размещен в прямом эфире на Repl.it, так что посмотрите, что мы будем делать Здесь Анкет

Структура папки

Часто довольно запутанно иметь дело с такими длинными учебниками по кодированию, поэтому я предоставил простую структуру папок, которая может помочь. Я знаю, что мое именование файлов не самое лучшее (т.е. не капитализация имен файлов класса), но вы можете изменить их по мере необходимости.

index.html
css /
    globals.css
    index.css
js /
    index.js
    config.js
    classes /
        bullet.js
        player.js
        zombie.js
    libs /
        animate.js
        input.js
        pointer.js
        utils.js

Кодовые фрагменты

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

Помните, что если это сбивает с толку или вы хотите увидеть, где должны быть размещены функции, ознакомьтесь с кодом на GitHub.

HTML Mayout

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

index.html





  
  
  Shooter
  
  
  


  

До сих пор мы добавили базовые метатеги, холст и включили наши файлы CSS и JS.

Основные CSS

Вы можете пропустить эту часть на CSS. Я только что включил его в случай, если я расширил проект, например, добавление меню «Пуск». Обычно в моих проектах css/globals.css Содержит сбросы размера коробки и любые переменные для темы сайта. css/index.css Есть все остальное, что нужно для стиля index.html Анкет Опять же, этот шаг в основном ненужный, учитывая, что большая часть работы будет выполнена в JS.

css/globals.css

html, body {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  box-sizing: border-box;
  overflow: hidden; /* generally you don't mess with this but I don't want any scrolling regardless */
}

*, ::before, ::after {
  box-sizing: inherit;
}

css/index.css

/* make the canvas wrapper expand to the entire page */
#app {
  min-height: 100vh;
  width: 100%;
}

/* make canvas expand to the entire page */
#app-scene {
  height: 100%;
  width: 100%;
}

JavaScript

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

Конфигурация

Обычно вы захотите поместить переменные, которые изменяют поведение игры в config.js Анкет Например, вы можете указать скорость игрока или сколько точек хит -хэмби у зомби. Я оставлю вам специфику, так что все, что я экспортирую, это то, насколько большим должен быть холст (весь экран).

JS/config.js

const width = window.innerWidth
const height = window.innerHeight

export {
  width,
  height
}

Утилит

Библиотеки, такие как P5.JS, предоставляют множество встроенных функций, которые упрощают математику. Единственные функции, которые нам нужны, – это реализация случайный и расстояние Анкет

JS/libs/utils.js

const random = (min, max) => {
  return (Math.random() * (max - min)) + min
}

const distance = (x1, y1, x2, y2) => {
  let xx = Math.pow((x2 - x1), 2)
  let yy = Math.pow((y2 - y1), 2)
  return Math.sqrt(xx + yy)
}

export {
  random,
  distance
}

Анимация

Во -первых, нам нужно ссылаться на наш холст и настроить базовый игровой цикл. Основной процесс рендеринга и обновления будет настроен в JS/libs/animate.js , а затем импортируется для использования в js/index.js Анкет

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

Здесь мы инициализируем все переменные, которые мы будем использовать. Обновление это функция, которую мы перейдем в оживить Функция (см. Ниже), которую мы хотим запустить каждый кадр.

js/libs/animate.js

let interval, start, now, then, elapsed
let update

StartAnimation Устанавливает нашу анимацию на 60 кадров в секунду и начинает AnimationLoop функция, которая рекурсивно вызывает с requestAnimationFrame .

js/libs/animate.js

const startAnimation = () => {
  interval = 1000 / 60
  then = Date.now()
  start = then
  animationLoop()
}

// recursively call animationLoop with requestAnimationFrame
const animationLoop = () => {
  requestAnimationFrame(animationLoop)

  now = Date.now()
  elapsed = now - then

  if(elapsed > interval) {
    then = now - (elapsed % interval)
    update()
  }
}

Наконец, мы экспортируем функцию утилиты, чтобы установить Обновление и начать анимацию.

js/libs/animate.js

const animate = (u) => {
  update = u
  startAnimation()
}

export default animate

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

js/index.js

import animate from "./libs/animate.js"
import { width, height } from "./config.js"

// get the canvas and context
const canvas = document.getElementById("app-scene")
const ctx = canvas.getContext("2d")

Object.assign(canvas, {
  width, height
})

const update = () => {
  ctx.clearRect(0, 0, width, height) // refreshes the background
}

animate(update)

Игрок

Если вы бросите Консоль.log в Обновление Вы увидите, что он неоднократно запускается, но на экране ничего не нарисовано. Пришло время добавить игрока, которого мы можем контролировать!

На данный момент я инициализации класса с некоторыми переменными по умолчанию и пустыми функциями.

JS/Classes/Player.js

import { width, height } from "../config.js"

class Player {
  vector = {
    x: width / 2,
    y: height / 2
  }
  speed = 2
  radius = 20
  angle = - Math.PI / 2

  rotate() {}
  move() {}
  update() {
    this.move()
  }
  render(ctx) {}
}

export default Player

Рендеринг игрока

В Player.Render Мы укажем, как должен выглядеть персонаж в нашей игре. Я не использую блюд, и я не профессионал в разработке активов, поэтому наш игрок буквально будет мячом цвета кожи.

Казалось бы, случайный -2 или +5 используется для регулировки расположения рук и пистолета, поэтому поиграйте с координатами, которые я передаю в функции рисования. Многое из того, что я сделал, чтобы игрок выглядел порядочным, это угадайте и проверьте.

JS/Classes/Player.js

render(ctx) {
  // rotation logic (doesn't do anything for now)
  ctx.save()

  let tX = this.vector.x 
  let tY = this.vector.y 
  ctx.translate(tX, tY)
  ctx.rotate(this.angle)
  ctx.translate(-tX, -tY)

  // Draw a circle as the body
  ctx.beginPath()
  ctx.fillStyle = "#ffe0bd"
  ctx.arc(this.vector.x, this.vector.y, this.radius, 0, Math.PI * 2)
  ctx.fill()

  // Draw a black rectangle as the "gun"    
  ctx.beginPath()
  ctx.fillStyle = "#000"
  ctx.rect(this.vector.x + this.radius + 15, this.vector.y - 5, 25, 10)
  ctx.fill()

  // Specify how the hands should look
  ctx.beginPath()
  ctx.strokeStyle = "#ffe0bd"
  ctx.lineCap = "round"
  ctx.lineWidth = 4

  // Right Hand
  ctx.moveTo(this.vector.x + 5, this.vector.y + this.radius - 2) 
  ctx.lineTo(this.vector.x + this.radius + 15, this.vector.y + 5)
  ctx.stroke()

  // Left Hand
  ctx.moveTo(this.vector.x + 5, this.vector.y - this.radius + 2)
  ctx.lineTo(this.vector.x + this.radius + 15, this.vector.y - 5)
  ctx.stroke()

  // also part of the rotation logic
  ctx.restore()
}

На экране!

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

js/index.js

import Player from "./classes/player.js"

const player = new Player()
const update = () => {
  player.update()
  player.render(ctx)
}

animate(update)

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

Движение

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

JS/libs/input.js

let keymap = []

window.addEventListener("keydown", e => {
  let { key } = e
  if(!keymap.includes(key)) {
    keymap.push(key)
  }
})

window.addEventListener("keyup", e => {
  let { key } = e
  if(keymap.includes(key)) {
    keymap.splice(keymap.indexOf(key), 1)
  }
})

const key = (x) => {
  return keymap.includes(x)
}
// now, we can use key("w") to see if w is still being pressed
export default key

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

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

JS/Classes/Player.js

import key from "../libs/input.js"

class Player {
  move() {
    if(key("w") && this.vector.y - this.speed - this.radius > 0) {
      this.vector.y -= this.speed
    }
    if(key("s") && this.vector.y + this.speed + this.radius < height) {
      this.vector.y += this.speed
    }
    if(key("a") && this.vector.x - this.speed - this.radius > 0) {
      this.vector.x -= this.speed
    }
    if(key("d") && this.vector.x + this.speed + this.radius < width) {
      this.vector.x += this.speed
    }
  }
}

Ротация

Игрок может перемещаться, но пистолет только указывает вверх. Чтобы исправить это, нам нужно найти местоположение мыши и повернуть игрока к нему.

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

js/libs/pointer.js

const pointer = (canvas, event) => {
  const rect = canvas.getBoundingClientRect()
  const x = event.clientX - rect.left
  const y = event.clientY - rect.top

  return {
    x, y
  }
}

export default pointer

Игрок должен вращаться к координатам указателя, поэтому давайте быстро добавим это. Мы уже добавили логику, чтобы учесть угол игрока, поэтому нам не нужно ничего менять в Player.Render Анкет

JS/Classes/Player.js

// destructure the pointer coords
rotate({ x, y }) {
  let dy = y - this.vector.y
  let dx = x - this.vector.x
  // essentially get the angle from the player to the cursor in radians
  this.angle = Math.atan2(dy, dx)
}

Но ждать! Когда мы обновляем демонстрацию, игрок не смотрит на нашу мышь. Это потому, что мы никогда не слушаем MouseMove событие, чтобы получить координаты мыши.

js/index.js

import pointer from "./libs/pointer.js"

document.body.addEventListener("mousemove", (e) => {
  let mouse = pointer(canvas, e)
  player.rotate(mouse)
})

Теперь у нас есть движущийся игрок, который может осмотреть вокруг.

Зомби

Как и игрок, давайте создадим класс зомби. Много кода зомби будет выглядеть очень знакомо. Однако вместо того, чтобы вращаться и перемещаться в зависимости от ввода пользователя, он просто будет следовать за игроком.

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

JS/Classe/Zombie.js

import { width, height } from "../config.js"
import { random } from "../libs/utils.js"

class Zombie {
  speed = 1.1
  radius = 20
  health = 5

  constructor(player) {
    this.vector = {
      x: width + this.radius,
      y: random(-this.radius, height + this.radius)
    }
    this.rotate(player)
  }

  rotate(player) {}
  update(player, zombies) {
    this.rotate(player)
  }
  render(ctx) {}
}


export default Zombie

Рендеринг зомби

Зомби будут зелеными шариками с растянутыми руками. Вращающаяся логика, тело и руки – это по сути те же вещи, которые можно найти в Player.Render Анкет

JS/Classe/Zombie.js

render(ctx) {
  ctx.save()

  let tX = this.vector.x 
  let tY = this.vector.y 
  ctx.translate(tX, tY)
  ctx.rotate(this.angle)
  ctx.translate(-tX, -tY)

  ctx.beginPath()
  ctx.fillStyle = "#00cc44"
  ctx.arc(this.vector.x, this.vector.y, this.radius, 0, Math.PI * 2)    
  ctx.fill()

  // Hands
  ctx.beginPath()
  ctx.strokeStyle = "#00cc44"
  ctx.lineCap = "round"
  ctx.lineWidth = 4

  // Right Hand
  ctx.moveTo(this.vector.x + 5, this.vector.y + this.radius - 2) 
  ctx.lineTo(this.vector.x + this.radius + 15, this.vector.y + this.radius - 5)
  ctx.stroke()

  // Left Hand
  ctx.moveTo(this.vector.x + 5, this.vector.y - this.radius + 2)
  ctx.lineTo(this.vector.x + this.radius + 15, this.vector.y - this.radius + 5)
  ctx.stroke()

  ctx.restore()
}

На экране!

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

JS/Classe/Zombie.js

import Zombie from "./classes/zombie.js"

const player = new Player()
const zombies = [ new Zombie(player) ]

const update = () => {
  zombies.forEach(zombie => {
    zombie.update(player, zombies)
    zombie.render(ctx)
  })    

  player.update()
  player.render(ctx)
}

animate(update)

Следуйте за игроком

Зомби привлекаются к человеческому мозгу. К сожалению, зомби, который мы только что сделали, только что сидит за экраном. Давайте начнем с того, что заставь зомби следовать за игроком. Основные функции, которые позволяют этому произойти, – это Zombie.rotate (укажите на игрока) и Zombie.update (Вызовы вращаются и движется в общем направлении координат игрока).

Если вы не понимаете Math.cos или Math.sin , интуитивно это имеет смысл, потому что Cosine относится к X и Sine относится к Y. Мы в основном преобразовываем угол в x и y, чтобы применить его к вектору положения зомби.

JS/Classe/Zombie.js

rotate(player) {
  let dy = player.vector.y - this.vector.y
  let dx = player.vector.x - this.vector.x
  this.angle = Math.atan2(dy, dx)
}

update(player, zombies) {
  this.rotate(player)
  this.vector.x += Math.cos(this.angle) * this.speed
  this.vector.y += Math.sin(this.angle) * this.speed
}

Хотя мы еще не внедрили систему стрельбы, мы хотим удалить зомби, когда его здоровье достигает 0. Давайте изменим функцию обновления, чтобы разбивать мертвые зомби.

JS/Classe/Zombie.js

update(player, zombies) {
  if(this.health <= 0) {
    zombies = zombies.splice(zombies.indexOf(this), 1)
    return
  }

  this.rotate(player)
  this.vector.x += Math.cos(this.angle) * this.speed
  this.vector.y += Math.sin(this.angle) * this.speed
}

Пуль

Зомби атакуют! Но что мы делаем? У нас нет боеприпасов! Нам нужно сделать класс пули, чтобы мы могли начать убивать монстров.

Когда мы призываем к новой пуле, нам нужно выяснить, с чего должна начаться пуля ( bullet.vector ), и в каком направлении следует начать заголовок ( bullet.Angle ). * 40 Рядом с векторной частью сдвигает пулю возле пистолета, а не появляется прямо на вершине игрока.

JS/Classes/Bullet.js

import { width, height } from "../config.js"
import { distance } from "../libs/utils.js"

class Bullet {
  radius = 4
  speed = 10

  constructor(x, y, angle) {
    this.angle = {
      x: Math.cos(angle),
      y: Math.sin(angle)
    }
    this.vector = {
      x: x + this.angle.x * 40, 
      y: y + this.angle.y * 40
    }
  }

  boundary() {}
  update(bullets, zombies) {
    this.vector.x += this.angle.x * this.speed
    this.vector.y += this.angle.y * this.speed
  }
  render(ctx) {}
}

export default Bullet

Рендеринг пуль

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

JS/Classes/Bullet.js

render(ctx) {
  ctx.beginPath()
  ctx.arc(this.vector.x, this.vector.y, this.radius, 0, Math.PI * 2)
  ctx.fillStyle = "#000"
  ctx.fill()
}

Граница

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

JS/Classes/Bullet.js

boundary() {
  return (this.vector.x > width + this.radius ||
          this.vector.y > height + this.radius ||
          this.vector.x < 0 - this.radius ||
          this.vector.y < 0 - this.radius)
}
update(bullets, zombies) {
  if(this.boundary()) {
    bullets = bullets.splice(bullets.indexOf(this), 1)
    return
  }

  this.vector.x += this.angle.x * this.speed
  this.vector.y += this.angle.y * this.speed
}

Нажмите, чтобы стрелять

Каждый раз, когда мы нажимаем на экран, мы должны выпустить новую пулю. После импорта класса пули в основной сценарий мы сделаем Пули Массив, который мы можем выдвинуть новую пулю на каждый раз, когда пользователь щелкает на экране. Таким образом, мы можем пройти и обновить каждую пулю.

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

js/index.js

import Bullet from "./classes/bullet.js"

const bullets = []

document.body.addEventListener("click", () => {
  bullets.push(
    new Bullet(player.vector.x, player.vector.y, player.angle)
  )
})

const update = () => {
  bullets.forEach(bullet => {
    bullet.update(bullets, zombies)
    bullet.render(ctx)
  })
}

animate(update)

Убейте зомби!

На данный момент пули проходят прямо через зомби.

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

JS/Classes/Bullet.js

update(bullets, zombies) {
  if(this.boundary()) {
    bullets = bullets.splice(bullets.indexOf(this), 1)
    return
  }

  for(const bullet of bullets) {
    for(const zombie of zombies) {
      let d = distance(zombie.vector.x, zombie.vector.y, this.vector.x, this.vector.y)
      if(d < zombie.radius) {
        bullets = bullets.splice(bullets.indexOf(this), 1)
        zombie.health -- 
        return
      }
    }
  }

  this.vector.x += this.angle.x * this.speed
  this.vector.y += this.angle.y * this.speed
}

Попробуйте стрелять в зомби 5 раз. Надеюсь, пули и зомби исчезнут.

Бонус: бесконечные волны

Один зомби скучен. Как насчет того, как мы появляемся в зомби каждые три секунды? js/index.js

setInterval(() => {
    zombies.push(new Zombie(player))
}, 3 * 1000)

Закрытие

Теперь у нас есть полностью функциональная игра с стрельбой в зомби. Надеемся, что это дало вам краткое представление о разработке игры с холстом HTML5. В настоящее время ничего не происходит, когда зомби касается вас, но не должно быть слишком сложно реализовать планку HP Player (посмотрите на код столкновения пули и столкновения с зомби). Я с нетерпением жду, как вы расширяете или оптимизируете эту игру!

Оригинал: “https://dev.to/phamn23/simple-zombie-shooter-49d”