ГоловнаВсі публікаціїКатегоріїПро проєкт

Пишемо демо-гру 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.06.2024 14:00

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

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

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

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

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

meme code
meme code@memecode
Як зробити пустий git commit?
28.06.2024 08:33

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

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

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

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

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

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

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

meme code
meme code@memecode
Що означає .map(&:name) в Ruby?
28.07.2024 11:18

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

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

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

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

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

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

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

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

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

meme code
meme code@memecode