Обкладинка нотатки: Пишемо демо-гру Drones vs Zombies (Gosu / Ruby)

Пишемо демо-гру Drones vs Zombies (Gosu / Ruby)

Зміст дописунатисність на посилання, щоб перейти до потрібного місця
Я вже писав мінімальний огляд функціоналу бібліотеки gosu. Щоб написати просту 2D гру насправді непотрібно багато коду. Також можна уникнути графічних елементів та використовувати прості фігури (програмно намальовані). Для початку (під час вивчення рушія) так і треба робити, але стрибаючий прямокутник може бути не таким прикольним, як щось намальоване.
Я взяв безкоштовні іконки (атрибуція обов'язкова за умовам використання) Freepik на сайті Flaticon. Графіку можна знайти повністю безкоштовну, намалювати самостійно, замовити або ж згенерувати за допомогою ШІ. Генерований контент може підійти для процесу розробки, а для релізу у світ всеж таки краще скористатись послугами художників та підтримати економіку та їх творчість.
Ну тож повернемося до теми цього допису - демки гри Drones vs Zombies. Концепція доволі проста - за допомогою дрона потрібно знищувати зомбі, які намагаються дістатись до пілота. Демка не ідеальна, але гарно демонструє, що приблизно 250 строк коду та декілька картинок можуть перетворити ідею/концепцію гри у демку.
Сам код моєї демки:
# Drones VS Zombies v.1.0.0

# Icons: https://www.flaticon.com/authors/freepik
# Code:  demo by memecode https://tseivo.com/b/memecode

require 'gosu'

class DroneGame < Gosu::Window
  WIDTH = 800
  HEIGHT = 600

  def initialize
    super(WIDTH, HEIGHT)
    self.caption = "Drones VS Zombies"
    reset_game
    @background_color = Gosu::Color.new(255, 85, 104, 50)
    @font = Gosu::Font.new(20)
    @paused = false
  end

  def update
    return if @game_over || @paused

    @drones.each(&:update)
    handle_explosions
    @drones.reject!(&:exploded?)
    spawn_drone if @drones.empty?

    @zombies.each(&:update)
    handle_zombie_movement
    handle_collisions
    @zombies.reject!(&:dead?)
    spawn_zombie if rand < 0.02
    check_game_over
  end

  def draw
    draw_quad(0, 0, @background_color, WIDTH, 0, @background_color, WIDTH, HEIGHT, @background_color, 0, HEIGHT, @background_color)
    @operator.draw
    @drones.each(&:draw)
    @zombies.each(&:draw)
    @font.draw_text("Points: #{@points}", 10, 10, 2)
    if @game_over
      @font.draw_text("Game Over! Final Score: #{@points}", WIDTH / 2 - 100, HEIGHT / 2, 3, 1.0, 1.0, Gosu::Color::RED)
      @font.draw_text("Press Space to Restart", WIDTH / 2 - 100, HEIGHT / 2 + 30, 3, 1.0, 1.0, Gosu::Color::WHITE)
    elsif @paused
      @font.draw_text("Paused", WIDTH / 2 - 50, HEIGHT / 2, 3, 1.0, 1.0, Gosu::Color::YELLOW)
      @font.draw_text("Press Esc to Resume", WIDTH / 2 - 100, HEIGHT / 2 + 30, 3, 1.0, 1.0, Gosu::Color::WHITE)
    end
  end

  def button_down(id)
    case id
    when Gosu::KbReturn
      @drones.each(&:explode) unless @paused
    when Gosu::KbSpace
      reset_game if @game_over
    when Gosu::KbEscape
      @paused = !@paused unless @game_over
    end
  end

  private

  def handle_collisions
    @drones.each do |drone|
      # Check collision with operator
      if drone.collides_with?(@operator) && !drone.exploded?
        drone.explode
        @game_over = true
        next
      end

      # Check collision with zombies
      @zombies.each do |zombie|
        if drone.collides_with?(zombie) && !drone.exploded?
          zombie.hit
          drone.explode
          @points += 1
        end
      end
    end
  end

  def handle_explosions
    @drones.each do |drone|
      next unless drone.exploded?

      @zombies.reject! do |zombie|
        drone.collides_with?(zombie)
      end

      # Check collision with operator after explosion
      if drone.collides_with?(@operator)
        @game_over = true
      end
    end
  end

  def handle_zombie_movement
    @zombies.each do |zombie|
      zombie.move_towards(@operator.x, @operator.y)
    end
  end

  def spawn_zombie
    @zombies << Zombie.new
  end

  def spawn_drone
    @drones << Drone.new(@operator.x, @operator.y - 100)
  end

  def check_game_over
    @zombies.each do |zombie|
      if Gosu.distance(zombie.x, zombie.y, @operator.x, @operator.y) < 30
        @game_over = true
      end
    end
  end

  def reset_game
    @operator = Operator.new
    @drones = []
    @zombies = []
    @points = 0
    @game_over = false
    @paused = false
    spawn_drone
  end
end

class Operator
  attr_reader :x, :y, :size

  def initialize
    @x = DroneGame::WIDTH / 2
    @y = DroneGame::HEIGHT - 50
    @image = Gosu::Image.new("operator.png")
    @size = 100
  end

  def draw
    @image.draw(@x - @size / 2, @y - @size / 2, 1)
  end
end

class Drone
  attr_reader :x, :y, :exploded, :size

  def initialize(x, y)
    @x, @y = x, y
    @image = Gosu::Image.new("drone.png")
    @explosion_image = Gosu::Image.new("explosion.png")
    @exploded = false
    @size = 100
    @angle = 0  # Initialize angle
    @explosion_timer = 0
  end

  def update
    return if @exploded

    @y -= 5 if Gosu.button_down?(Gosu::KbUp)
    @y += 5 if Gosu.button_down?(Gosu::KbDown)
    @x -= 5 if Gosu.button_down?(Gosu::KbLeft)
    @x += 5 if Gosu.button_down?(Gosu::KbRight)

    # Rotation
    @angle -= 5 if Gosu.button_down?(Gosu::KbLeft)
    @angle += 5 if Gosu.button_down?(Gosu::KbRight)

    # Boundary checking
    @x = [[@x, 0].max, DroneGame::WIDTH].min
    @y = [[@y, 0].max, DroneGame::HEIGHT].min

    if @explosion_timer > 0
      @explosion_timer -= 1
    end
  end

  def draw
    if @exploded
      if @explosion_timer > 0
        @explosion_image.draw_rot(@x, @y, 1, @angle, 0.5, 0.5)
      end
    else
      @image.draw_rot(@x, @y, 1, @angle)
    end
  end

  def explode
    @exploded = true
    @explosion_timer = 550
  end

  def exploded?
    @exploded
  end

  def collides_with?(object)
    Gosu.distance(@x, @y, object.x, object.y) < (@size / 2 + object.size / 2)
  end
end

class Zombie
  attr_reader :x, :y, :size

  def initialize
    @x = rand * DroneGame::WIDTH
    @y = 0
    @image = Gosu::Image.new("zombie.png")
    @size = 100
    @speed = rand(0.05..0.2)
    @alive = true
  end

  def update
    return unless @alive
    @y += @speed
  end

  def draw
    @image.draw(@x - @size / 2, @y - @size / 2, 1)
  end

  def move_towards(target_x, target_y)
    if @y < target_y
      @y += @speed
    elsif @y > target_y
      @y -= @speed
    end
    if @x < target_x
      @x += @speed
    elsif @x > target_x
      @x -= @speed
    end
  end

  def hit
    die if Gosu.distance(@x, @y, DroneGame::WIDTH / 2, DroneGame::HEIGHT - 50) < 50
  end

  def die
    @alive = false
    @speed = 0
  end

  def dead?
    !@alive
  end
end

DroneGame.new.show
Картинки додайте самі (не хочу випадково порушити авторське право):
  • drone.png
  • explosion.png
  • operator.png
  • zombie.png
Розмір для кожної з них має бути 100х100px (png з прозорістю).

Клас DroneGame

  • Головний клас гри, унаслідований від Gosu::Window.
  • Ініціалізує вікно гри, встановлює колір фону та шрифт для текстових елементів.
  • Містить логіку для оновлення гри (update) та відображення (draw), обробляє події натискання кнопок (button_down).

Клас Operator

  • Представляє оператора дрону.
  • Відповідає за його положення на екрані та відображення зображення оператора.

Клас Drone

  • Представляє дрон, який використовується для атаки.
  • Має можливість рухатися вліво, вправо, вгору та вниз, обробляє вибух (explode) та перевіряти зіткнення з іншими об'єктами.

Клас Zombie

  • Представляє зомбі, що нападають на оператора.
  • Рухається у напрямку оператора, може бути враженим дронами, що призводить до знищення.

Основні функції

  • Старт і перезавантаження гри: гра починається зі спавну оператора та дронів, які захищають його. Після поразки (коли зомбі досягають оператора), гравець може перезавантажити гру, натиснувши пробіл (Space).
  • Динамічні об'єкти: дрони та зомбі мають власні об'єкти, які оновлюють їх рух та стан.
  • Зіткнення і вибухи: логіка обробки зіткнень між дронами, зомбі та оператором, а також вибухів дронів, що призводять до знищення зомбі та кінця гри.
Наступним кроком я хочу додати звуки, промальовувати могилки для зомбі після їх знищення, покращити рух / управління дроном (зараз він крутиться навколо осі), зробити анімацію пропелерів дрона тощо. Я вирішив публікувати демки крок за кроком, бо написання всіх бажаних штук потребує дуже багато часу.
Дрон вбив оператора :(
Дрон вбив оператора :(
Наприклад, перед публікацією цього допису я додав кілька речей:
  • оператор дрона може сам себе випадково знищити дроном
  • додав границі до полю бою (до цього дрон міг улетіти за край вікна та умовно загубитись)

🔗 Цитувати допис: "Пишемо демо-гру Drones vs Zombies (Gosu / Ruby)"

Якщо ви хочете процитувати цей допис у своїй роботі, статті, блозі, використовуйте наведену нижче інформацію.

Розгорнути деталі


🙌 Підтримати блог @memecode

Ви можете поширити цей допис у соцмережах, чим допоможете платформі цейво розвиватись (* ^ ω ^)

📝 Більше публікацій:
Обкладинка нотатки: [Ruby] Чим відрізняються змінні, що починаються з @, @@ та $?
Обкладинка нотатки: Що таке функція в програмуванні?
Обкладинка нотатки: [Фікс] extconf.rb failed під час встановлення Ruby-бібліотеки Gosu
Обкладинка нотатки: Як зробити пустий git commit?
Обкладинка нотатки: Ruby-бібліотека Gosu для створення 2D-ігор
Обкладинка нотатки: Gosu Ruby Tutorial - пройдемось по офіційній документації
Обкладинка нотатки: Як пофіксити збій Windows викликаний CrowdStrike?
Обкладинка нотатки: Що означає .map(&:name) в Ruby?
Обкладинка нотатки: Як працює метод map в Ruby? Огляд роботи методу з прикладами
Обкладинка нотатки: Що означає крапка на початку файлу(.gitignore, .DS_Store, .bashrc тощо)?
Обкладинка нотатки: Що таке .gitignore? Для чого потрібен та як використовувати
Обкладинка нотатки: Як видалити файл .DS_Store з Git репозиторію?
Дисклеймер

Інформація на сайті tseivo.com є суб'єктивною та відображає особисті погляди та досвід авторів та авторок блогів.

Використовуйте цей ресурс як одне з декількох джерел інформації під час своїх досліджень та прийняття рішень. Завжди застосовуйте критичне мислення. Людина сама несе відповідальність за свої рішення та дії.