🇺🇦Допомогти Україні

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

Цей допис поки що не має жодних доповнень від автора/ки.

[Ruby] Чим відрізняються змінні, що починаються з @, @@ та $?
23 черв., 14:00

[Ruby] Чим відрізняються змінні, що починаються з @, @@ та $?

meme code
meme code@memecode
Що таке функція в програмуванні?
24 черв., 18:15

Що таке функція в програмуванні?

meme code
meme code@memecode
[Фікс] extconf.rb failed під час встановлення Ruby-бібліотеки Gosu
27 черв., 16:38

[Фікс] extconf.rb failed під час встановлення Ruby-бібліотеки Gosu

meme code
meme code@memecode
Як зробити пустий git commit?
28 черв., 08:33

Як зробити пустий git commit?

meme code
meme code@memecode
Ruby-бібліотека Gosu для створення 2D-ігор
29 черв., 08:48

Ruby-бібліотека Gosu для створення 2D-ігор

meme code
meme code@memecode
Gosu Ruby Tutorial - пройдемось по офіційній документації
03 лип., 11:50

Gosu Ruby Tutorial - пройдемось по офіційній документації

meme code
meme code@memecode
Як пофіксити збій Windows викликаний CrowdStrike?
19 лип., 13:53

Як пофіксити збій Windows викликаний CrowdStrike?

meme code
meme code@memecode
Що означає .map(&:name) в Ruby?
28 лип., 11:18

Що означає .map(&:name) в Ruby?

meme code
meme code@memecode
Як працює метод map в Ruby? Огляд роботи методу з прикладами
30 лип., 07:33

Як працює метод map в Ruby? Огляд роботи методу з прикладами

meme code
meme code@memecode
Що означає крапка на початку файлу(.gitignore, .DS_Store, .bashrc тощо)?
02 серп., 13:15

Що означає крапка на початку файлу(.gitignore, .DS_Store, .bashrc тощо)?

meme code
meme code@memecode
Що таке .gitignore? Для чого потрібен та як використовувати
02 серп., 14:58

Що таке .gitignore? Для чого потрібен та як використовувати

meme code
meme code@memecode
Як видалити файл .DS_Store з Git репозиторію?
02 серп., 19:34

Як видалити файл .DS_Store з Git репозиторію?

meme code
meme code@memecode