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

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

Це не переклад Ruby Tutorial сторінки бібліотеки Gosu, а скоріш огляд з коментарями та додатковою інформацію. Не люблю сухі Readme, тимпаче для чогось, що пов'язано з іграми.
Почнемо з того, що для встановлення ruby-бібліотеки робимо стандартне:
gem install gosu
Далі - створюємо просту гру на Ruby використовуючи офіційну документацію Gosu. Починаємо.

Перезаписуємо колбеки підкласу Gosu::Window

Написання гри на Gosu починається з підкласу Gosu::Window. Window - це вікно, в якому буде відкриватись і відображатись гра. Мінімальний код виглядає ось так:
require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
  end
  
  def update
    # ...
  end
  
  def draw
    # ...
  end
end

Tutorial.new.show
Якщо зберегти та запустити файл:
ruby demo.rb
Ми побачимо таке вікно (window):
Gosu - Стартовий екран (клас Window)
Gosu - Стартовий екран (клас Window)
Не треба досконало знати ruby та gosu щоб зрозуміти за що відповідають ці строки:
    super 640, 480
    self.caption = "Tutorial Game"
Конструктор ініціалізує базовий клас Gosu::Window для створення вікна розміром 640x480 пікселів. Він також встановлює заголовок вікна, який відображається у його заголовку. Ви можете створити повноекранне вікно, передавши після ширини та висоти:
fullscreen: true
В документації використовується старий синтаксис :fullscreen => true. Працюють обидва варіанти, але мені трохи вже ріже око старий синтаксис та й автокомпліт редактору коду пише в новому стилі. Тож щоб гра була на повний екран, наша строка має виглядати так:
    super 640, 480, fullscreen: true
Методи update і draw використовуються для створення анімації та логіки гри в Gosu. Метод update автоматично викликається 60 разів на секунду (FPS) і відповідає за головну логіку гри: рух об'єктів і перевірку зіткнень (коли один об'єкт доторкається до іншого(collisions)).
Метод draw зазвичай викликається теж 60 разів на секунду, але іноді може пропускатися для кращої продуктивності. Він відповідає за перемальовування всього, що відображається на екрані, але не повинен містити жодної логіки гри. Якщо простіше на прикладі - коли гравець пройшов рівень і починає новий, метод update може змінювати стан гри (наприклад, завантажувати новий рівень або змінювати розташування об'єктів). Потім метод draw викликається для відображення нових елементів на екрані.
Далі у нас основна програма. Ми створюємо вікно і викликаємо метод show, який працює до тих пір, поки вікно не буде закрите користувачем або командою close. Це і є головний цикл гри (main loop) або ж Window Main Loop. Документація Gosu має окрему сторінку з описом цього циклу. Думаю що з ним треба ознайомитись більш детально, щоб зрозуміти як працює механіка рушія.

Огляд Window Main Loop

Кожна гра на Gosu має підклас Gosu::Window, який перевизначає всі потрібні колбеки. Коли викликаєш window.show, Gosu входить у головний цикл гри.
Методи update і draw працюють разом, щоб гра ожила. Коли запускаєш вікно гри, Gosu постійно викликає методи update та draw. Уявіть це як постійний процес, де метод update оновлює стан гри (наприклад, рух об'єктів), а метод draw перемальовує все, що бачиш на екрані. Цей процес повторюється дуже швидко, зазвичай 60 разів на секунду.
Але є кілька важливих деталей, які потрібно зрозуміти. Метод draw дуже гнучкий. Зазвичай він викликається кожного разу після update, але іноді може бути викликаний операційною системою, якщо вікно потребує перемальовування. Або ж виклики до draw можуть пропускатися, якщо вікно приховано. Тому дії у грі не повинні залежати від ідеальної синхронізації між update і draw, навіть якщо зазвичай це так і працює.
Коли створюєш вікно гри, ти не знаєш, який метод буде викликаний першим - update чи draw. Тому в методі initialize потрібно встановити початковий стан гри, який буде працювати в будь-якому випадку.
Також важливо знати, що в Gosu жоден колбек не переривається іншим, і нічого не працює в багатопотоковому режимі. Це означає, що можна бути впевненим, що під час виклику update або draw не буде викликатися інший метод.

Падіння продуктивності

Щоб гра працювала добре на повільних комп'ютерах, є два рішення. Це дискретна логіка та фізика на основі дельта-часу. 
Дискретна Логіка
Перший спосіб - писати логіку гри в стилі туторіалу (який описан в цьому дописі). У кожному логічному кадрі (Window::update) всі об'єкти рухаються на фіксовану кількість пікселів. Це зазвичай найпростіший спосіб написати код гри.
Проблема в тому, що коли система занадто повільна для запуску гри з швидкістю 60 кадрів на секунду, все буде виглядати як у сповільненому русі. 
Для кращого ігрового досвіду потрібно намагатися зберігати FPS біля 60 кадрів на секунду. Тож на слабких машинах треба зменшувати кількість візуальних ефектів, коли показник Gosu::fps падає нижче 60. Можна використовувати спеціальний метод needs_redraw?, щоб пропускати виклики до Window::draw, наприклад, кожен другий кадр.

Фізика на Основі Дельта-Часу

Другий спосіб - використовувати дельта-час.
Дельта час (Delta Time, dt) у розробці комп'ютерних ігор — це проміжок часу між двома послідовними кадрами гри. Він використовується для забезпечення плавного та реалістичного руху та анімації незалежно від швидкості кадрів (FPS).
Можна викликати Gosu::milliseconds у кожному update, а потім оновлювати гру на основі різниці часу між двома викликами (дельта-час, dt). Формула position += speed * dt гарантує, що все буде рухатися з однаковою швидкістю незалежно від поточного фреймрейту. Проте цей варіант зазвичай вимагає трохи більше коду.
Gosu підтримує обидва стилі програмування, і можна вибрати той, який підходить проєкту гри найбільше. Ок, повертаємось до огляду туторіалу.

Використовуємо зображення

require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    
    @background_image = Gosu::Image.new("media/space.png", tileable: true)
  end
  
  def update
  end
  
  def draw
    @background_image.draw(0, 0, 0)
  end
end

Tutorial.new.show
space.png
space.png
Завантажте space.png і переконайтеся, що він знаходиться у media/space.png.
Gosu::Image#initialize приймає два аргументи: ім'я файлу та (необов'язково) хеш опцій. Тут також встановлюємо tileable true. Одразу розглянемо всі штуки, які в офіційній документацій відокремлені в окремому блоці - Basic Concepts.
Options Hash
Хеші — це спеціальні параметри, які можна передавати як опції в деякі методи в Gosu. Значення за замовчуванням завжди встановлюється для кожної опції. Щоб використовувати хеш, просто вкажіть ім'я опції, за яким слідує двокрапка, а потім призначте нове значення.
Наприклад, у методі Image.from_text .from_text(text, line_height, options = {}) опція для виділення тексту жирним надається як :bold (булеве значення) — значення за замовчуванням: false. Це можна передати як bold: true. Цей метод тоді стане .from_text("Цей текст буде жирним", 12, {bold: true}).
Z Ordering
Я думаю ви чули про z-index у CSS. Це опція, яка дозволяє задавати індекси різним елементам (шарам). Тобто елемент з z-індексом 1, буде знаходитись під/за елементом з індексом 2 і так далі. Така саме концепція z-ordering у Gosu.
Усі операції малювання в Gosu приймають значення з плаваючою точкою, яке називається "z". Об'єкти, намальовані з вищим значенням z, будуть намальовані поверх тих, у яких значення z нижче. Якщо два об'єкти мають однакове значення z, вони будуть намальовані в порядку виклику функцій малювання.
Tileability
Незнаю як правильно перекласти. Плитковість, плиточність, плиткова здатність, тайлебельність чи щось таке. 
Функції, що пов'язані зі створенням зображень, приймають булевий аргумент "tileable", який контролює, як будуть поводитися краї зображень при їх зміні розміру або обертанні. Спробуйте помітити тонку різницю між цими двома зображеннями, намальованими зі значенням scale_x = scale_y = 10.0:
hard borders. tiletable false та tiletable true
hard borders. tiletable false та tiletable true
Коли ви масштабуєте або обертаєте зображення, або малюєте зображення на нецілочисловій позиції, наприклад, 10.5, ваш графічний процесор буде інтерполювати вміст зображення для заповнення прямокутника.
Ліве зображення створено з tileable: false (за замовчуванням), і краї його розмиваються. Праве зображення, створене з tileable: true, має чіткі краї.
Як правило, плитки карт слід створювати з параметром tileable: true, щоб уникнути дрібних зазорів між плитками, а все інше слід використовувати зі значенням за замовчуванням.
Зверніть увагу, що зображення, створені з retro: true, ніколи не інтерполюються і не залежать від "tileability".
Порядок кутів
У функціях, які очікують аргументи (координати або кольори) для чотирьох кутів прямокутника, ви можете передавати параметри або за годинниковою стрілкою, або у формі Z:
індекси кутів у порядку z
індекси кутів у порядку z
Gosu розуміє обидва підходи, тому використовуйте той, який вам зручніший.
Малювання з кольорами
Майже всі функції малювання зображень приймають кольори модуляції. Кольори всіх пікселів на вихідному зображенні будуть помножені на ці кольори. Якщо ви малюєте червоне зображення (RGB 200, 0, 0) з кольором модуляції сірого (RGB 128, 128, 128), результат буде (RGB (200 * 128) / 255 = 100, 0, 0) (читайте також - що таке RGB). Параметри кольору можна використовувати тільки для зменшення інтенсивності кольорів у зображенні, вони не можуть їх посилити.
Найпоширеніший спосіб використання параметрів кольору — використання білого кольору з альфа-значенням меншим за 255, наприклад, (RGBA 255, 255, 255, 128). Цей код намалює зображення з 50% непрозорістю. Але також можна використовувати параметри кольору для затемнення зображень або для малювання їх в іншому відтінку, що найкраще працює з сірими зображеннями.
Повертаємось до коду з прикладу. Зверніть увагу на код:
@background_image = Gosu::Image.new("media/space.png", tileable: true)

...

def draw
  @background_image.draw(0, 0, 0)
end
Як було згадано до цього, метод draw потрібен для малювання буквально всього, тому ми перевизначаємо цей метод і малюємо наше фонове зображення саме тут. @background_image.draw(0, 0, 0) означає, що зображення @background_image буде намальоване в координатах (0, 0) з позицією Z, рівною 0. Давайте розберемо це більш детальніше:
  • 0 (перший аргумент): X-координата верхнього лівого кута зображення. В даному випадку, це 0, тобто зображення буде намальоване на самому лівому краю вікна.
  • 0 (другий аргумент): Y-координата верхнього лівого кута зображення. В даному випадку, це 0, тобто зображення буде намальоване на самому верхньому краю вікна.
  • 0 (третій аргумент): Z-координата, яка визначає порядок малювання зображення. Менші значення Z означають, що зображення буде малюватися позаду зображень з більшими значеннями Z. В даному випадку, значення Z рівне 0, тобто зображення буде малюватися на задньому плані, якщо інші зображення мають більші значення Z. Саме те що потрібно для фону - завжди бути позаду. Тож всі інші Z-значення мають бути більшими, щоб відображатись над фоновим зображенням.

Гравець (персонаж) та рухи

Туторіал пропонує нам розглянути наступний код класу Player (екземпляр цього класу - наш персонаж, яким ми будемо керувати).
class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x, @y = x, y
  end
  
  def turn_left
    @angle -= 4.5
  end
  
  def turn_right
    @angle += 4.5
  end
  
  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.offset_y(@angle, 0.5)
  end
  
  def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    
    @vel_x *= 0.95
    @vel_y *= 0.95
  end

  def draw
    @image.draw_rot(@x, @y, 1, @angle)
  end
end
Закиньте starfighter.bmp у теку media
starfighter.bmp
І розглянемо код трохи детальніше.
Player#accelerate і функції offset_x/offset_y
  • offset_x(angle, distance) і offset_y(angle, distance) обчислюють горизонтальне і вертикальне зміщення відповідно, подібно до того, як функції cos і sin використовуються в тригонометрії для обчислення координат точок на колі.
  • Наприклад, якщо об'єкт рухається під кутом 30° на відстань 100 пікселів, offset_x(30, 100) обчислює зміщення по горизонталі, а offset_y(30, 100) — по вертикалі.
    Angles
    Angles
Завантаження BMP зображень
  • Gosu автоматично замінює колір #ff00ff (яскраво-рожевий) на прозорі пікселі. Це корисно для створення прозорих областей в зображеннях. Особисто я використовую .png файли з прозорістю в своїх міні-іграх.
Метод draw_rot
Метод draw_rot у Gosu використовується для малювання зображення з обертанням навколо його центру. У цьому випадку ми використовуємо @image.draw_rot(@x, @y, 1, @angle). Ось детальний опис кожної опції:
  • @image - це об'єкт класу Gosu::Image, який містить зображення, що ми хочемо намалювати. Його потрібно створити заздалегідь, наприклад, за допомогою Gosu::Image.new("path/to/image.png").
  • @x - це координата x, де буде розташований центр зображення на екрані. Координати екрана починаються з лівого верхнього кута (0, 0) і зростають вправо.
  • @y - це координата y, де буде розташований центр зображення на екрані. Координати екрана починаються з лівого верхнього кута (0, 0) і зростають вниз.
  • 1 - це значення Z-координати або рівень глибини. Чим більше значення Z, тим вище (мається наувазі що цей шар буде над шаром з меншим значенням) об'єкт буде намальований на екрані відносно інших об'єктів. Об'єкти з більшим значенням Z будуть намальовані поверх об'єктів з меншим значенням Z. У цьому випадку значення 1 означає, що зображення буде намальовано поверх об'єктів з Z-координатою меншою, ніж 1.
  • @angle - це кут повороту зображення в градусах. Обертання відбувається за годинниковою стрілкою. Наприклад, 0 градусів означає, що зображення не обертається, 90 градусів означає, що зображення обертається на 90 градусів за годинниковою стрілкою, а 180 градусів означає, що зображення обертається на 180 градусів (перевертається догори дном).
Наразі наш код виглядає приблизно так:
require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    
    @background_image = Gosu::Image.new("media/space.png", tileable: true)
  end
  
  def update
  end
  
  def draw
    @background_image.draw(0, 0, 0)
  end
end

class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x, @y = x, y
  end
  
  def turn_left
    @angle -= 4.5
  end
  
  def turn_right
    @angle += 4.5
  end
  
  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.offset_y(@angle, 0.5)
  end
  
  def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    
    @vel_x *= 0.95
    @vel_y *= 0.95
  end

  def draw
    @image.draw_rot(@x, @y, 1, @angle)
  end
end

Tutorial.new.show
Та якщо запустити гру, ми побачимо лише екран з фоновим зображенням. 
Tutorial Game - Поки що лише фонове зображення
Tutorial Game - Поки що лише фонове зображення
Перейдемо до наступного кроку.

Використання класу Player

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"

    @background_image = Gosu::Image.new("media/space.png", tileable: true)

    @player = Player.new
    @player.warp(320, 240)
  end

  def update
    if Gosu.button_down? Gosu::KB_LEFT or Gosu::button_down? Gosu::GP_LEFT
      @player.turn_left
    end
    if Gosu.button_down? Gosu::KB_RIGHT or Gosu::button_down? Gosu::GP_RIGHT
      @player.turn_right
    end
    if Gosu.button_down? Gosu::KB_UP or Gosu::button_down? Gosu::GP_BUTTON_0
      @player.accelerate
    end
    @player.move
  end

  def draw
    @player.draw
    @background_image.draw(0, 0, 0)
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

Tutorial.new.show
Тут ми додали обробку введення з клавіатури (input, обробка натискання клавіш). button_down та button_up дозволяють реагувати на натискання і відпускання кнопок. Виклик super у button_down зберігає стандартну поведінку для комбінацій клавіш, таких як перемикання між повноекранним та віконним режимом. Документацію щодо інших кнопок можна прочитати на сайті RDoc, але останнім часом він щось то працює то нє. Тож документацію можна прочитати безпосередньо всередені бібліотеки gosu (gosu/rdoc/gosu.rb)
Методи button_down(id) та button_up(id) підходять для одноразових подій, але не для безперервних дій, таких як переміщення гравця. Для безперервних дій використовується метод Gosu.button_down?(id), який перевіряє, чи утримується кнопка, і повертає true, доки кнопка натиснута.
Метод update викликає методи @player.turn_left, @player.turn_right та @player.accelerate на основі натиснутих клавіш, а також метод @player.move для безперервного переміщення гравця.
Тобто наразі наш коди виглядає ось так:
require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"

    @background_image = Gosu::Image.new("media/space.png", tileable: true)

    @player = Player.new
    @player.warp(320, 240)
  end

  def update
    if Gosu.button_down? Gosu::KB_LEFT or Gosu::button_down? Gosu::GP_LEFT
      @player.turn_left
    end
    if Gosu.button_down? Gosu::KB_RIGHT or Gosu::button_down? Gosu::GP_RIGHT
      @player.turn_right
    end
    if Gosu.button_down? Gosu::KB_UP or Gosu::button_down? Gosu::GP_BUTTON_0
      @player.accelerate
    end
    @player.move
  end

  def draw
    @player.draw
    @background_image.draw(0, 0, 0)
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x, @y = x, y
  end
  
  def turn_left
    @angle -= 4.5
  end
  
  def turn_right
    @angle += 4.5
  end
  
  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.offset_y(@angle, 0.5)
  end
  
  def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    
    @vel_x *= 0.95
    @vel_y *= 0.95
  end

  def draw
    @image.draw_rot(@x, @y, 1, @angle)
  end
end

Tutorial.new.show
А коли запускаємо гру - ми отримуємо космічний корабель в центрі екрану якій має фонове зображення (космос). Вікно можна закрити за допомогою кнопки ESC. А космічним кораблем можна керувати за допомогою стрілочок.
Поточний стан гри. Корабель літає ^_^
Поточний стан гри. Корабель літає ^_^

Проста анімація

Анімація — це послідовність зображень, тому ми будемо використовувати вбудований масив Ruby для їх збереження. У реальній грі ми, можливо, захочемо створити окремий клас для анімації, але наразі масив є достатньо зручним рішенням.
Давайте додамо до гри зірочки, які будуть з'являютись випадковим чином на екрані. Вони мають відтворювати анімацію обертання до тих пір, поки не будуть зібрані гравцем. Визначення класу Star доволі коротке:
class Star
  attr_reader :x, :y

  def initialize(animation)
    @animation = animation
    @color = Gosu::Color::BLACK.dup
    @color.red = rand(256 - 40) + 40
    @color.green = rand(256 - 40) + 40
    @color.blue = rand(256 - 40) + 40
    @x = rand * 640
    @y = rand * 480
  end

  def draw  
    img = @animation[Gosu.milliseconds / 100 % @animation.size]
    img.draw(@x - img.width / 2.0, @y - img.height / 2.0,
        ZOrder::STARS, 1, 1, @color, :add)
  end
end
Ми не хочемо завантажувати всю анімацію кожного разу при створенні зірки, тому передаємо існуючу анімацію в конструктор. Щоб показати різний кадр анімації зірок кожні 100 мілісекунд, ми ділимо час, повернутий методом Gosu.milliseconds, на 100, а потім беремо залишок від ділення на кількість кадрів. Обране зображення малюється додатково, центроване за позицією зірки та модульоване випадковим кольором, який ми згенерували в конструкторі. Але давайте трохи детальніше розглянемо цей код, бо щось тут забагато калькуляцій і їх треба розуміти.
attr_reader :x, :y
Цей метод створює геттери для змінних екземпляра @x і @y, що дозволяє доступ до них ззовні класу. Почитайте додатково про attr_accessor, attr_reader та attr_writer у ruby.
initialize(animation)
Це конструктор класу Star. Він викликається при створенні нового об'єкта цього класу. Має він такі змінні екземпляру:
  • @animation = animation зберігає передану анімацію в змінну екземпляра @animation.
  • @color = Gosu::Color::BLACK.dup створює копію чорного кольору.
  • Наступні три рядки встановлюють випадковий відтінок червоного, зеленого та синього кольорів для зірки. rand(256 - 40) + 40 генерує випадкове число від 40 до 215 для кожного кольорового компонента, щоб забезпечити досить яскравий колір.
  • @x = rand * 640 і @y = rand * 480: задають випадкові координати для зірки на екрані розміром 640x480 пікселів.
draw
Цей метод відповідає за відображення зірки на екрані.
  • img = @animation[Gosu.milliseconds / 100 % @animation.size] вибирає кадр з анімації зірки на основі поточного часу. Gosu.milliseconds / 100 ділить час на 100 мілісекунд, що дозволяє змінювати кадр кожні 100 мілісекунд. Операція % @animation.size гарантує, що індекс залишається в межах розміру анімації.
  • img.draw(@x - img.width / 2.0, @y - img.height / 2.0, ZOrder::STARS, 1, 1, @color, :add) малює обраний кадр анімації. Позиціонує зображення так, щоб його центр був на координатах @x і @y. ZOrder::STARS вказує на шар малювання, який визначає порядок накладання зображень. 1, 1 — це масштабні фактори по горизонталі та вертикалі (без змін). @color встановлює колір зірки, а :add задає режим додавання кольорів, який дозволяє кольорам змішуватися більш яскраво.
Далі, треба навчити клас Player взаємодіяти з зірочками.
class Player
  ...
  def score
    @score
  end

  def collect_stars(stars)
    stars.reject! { |star| Gosu.distance(@x, @y, star.x, star.y) < 35 }
  end
end
Далі треба розширити можливості класу Window додавши взаємодії з зірочками:
...
class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

    @player = Player.new
    @player.warp(320, 240)

    @star_anim = Gosu::Image.load_tiles("media/star.png", 25, 25)
    @stars = Array.new
  end

  def update
    ...
    @player.move
    @player.collect_stars(@stars)

    if rand(100) < 4 and @stars.size < 25
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @background_image.draw(0, 0, ZOrder::BACKGROUND)
    @player.draw
    @stars.each { |star| star.draw }
  end
  ...
Закиньте файл star.png до теки media.
star.png
star.png
Ну наче все. Тепер в грі мають з'являтись анімовані зірочки, а ми можемо їх збирати. Ось GIF-ка:
Stars
Stars
Далі Gosu туторіал пропонує нам ознайомитись зі звуком та текстом.

Текст та звук

Перше що зробимо - додамо текст, а саме Score (очки за зібрані зірочки).
class Tutorial < Gosu::Window
  def initialize
    ...
    @font = Gosu::Font.new(20)
  end

  ...

  def draw
    @background_image.draw(0, 0, ZOrder::BACKGROUND)
    @player.draw
    @stars.each { |star| star.draw }
    @font.draw_text("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, Gosu::Color::YELLOW)
  end
end
Наведений код відрендеріть жовтий напис Score: 0 у верхньому лівому куті. Але Score покищо не змінюється, коли ми збираємо зірочки. Щоб все почало працювати - треба оновити клас Player. І раз вже оновлюємо код, давайте додамо звук.
Зверніть увагу на код, який дозволяє працювати зі звуком:
@beep = Gosu::Sample.new("media/beep.wav")
...
@beep.play
Ну і сам код:
class Player
  attr_reader :score

  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @beep = Gosu::Sample.new("media/beep.wav")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  ...

  def collect_stars(stars)
    stars.reject! do |star|
      if Gosu.distance(@x, @y, star.x, star.y) < 35
        @score += 10
        @beep.play
        true
      else
        false
      end
    end
  end
end
Метод collect_stars якраз нараховує бали (Score) та відтворює звук beep з файлу beep.wav -
beep.wav (4,2 кБ)
Працююча демка
Працююча демка
Якщо хтось заплутався в коді, ось фінальний код, який можна просто скопіювати і запустити:
require 'gosu'

module ZOrder
  BACKGROUND, STARS, PLAYER, UI = *0..3
end

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

    @player = Player.new
    @player.warp(320, 240)

    @star_anim = Gosu::Image.load_tiles("media/star.png", 25, 25)
    @stars = Array.new
    @font = Gosu::Font.new(20)
  end

  def update
    if Gosu.button_down? Gosu::KB_LEFT or Gosu::button_down? Gosu::GP_LEFT
      @player.turn_left
    end
    if Gosu.button_down? Gosu::KB_RIGHT or Gosu::button_down? Gosu::GP_RIGHT
      @player.turn_right
    end
    if Gosu.button_down? Gosu::KB_UP or Gosu::button_down? Gosu::GP_BUTTON_0
      @player.accelerate
    end
    @player.move
    @player.collect_stars(@stars)

    if rand(100) < 4 and @stars.size < 25
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @background_image.draw(0, 0, ZOrder::BACKGROUND)
    @player.draw
    @stars.each { |star| star.draw }
    @font.draw_text("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, Gosu::Color::YELLOW)
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

class Player
  attr_reader :score

  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @beep = Gosu::Sample.new("media/beep.wav")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def score
    @score
  end

  def collect_stars(stars)
    stars.reject! do |star|
      if Gosu.distance(@x, @y, star.x, star.y) < 35
        @score += 10
        @beep.play
        true
      else
        false
      end
    end
  end

  def warp(x, y)
    @x, @y = x, y
  end
  
  def turn_left
    @angle -= 4.5
  end
  
  def turn_right
    @angle += 4.5
  end
  
  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.offset_y(@angle, 0.5)
  end
  
  def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    
    @vel_x *= 0.95
    @vel_y *= 0.95
  end

  def draw
    @image.draw_rot(@x, @y, 1, @angle)
  end
end

class Star
  attr_reader :x, :y

  def initialize(animation)
    @animation = animation
    @color = Gosu::Color::BLACK.dup
    @color.red = rand(256 - 40) + 40
    @color.green = rand(256 - 40) + 40
    @color.blue = rand(256 - 40) + 40
    @x = rand * 640
    @y = rand * 480
  end

  def draw  
    img = @animation[Gosu.milliseconds / 100 % @animation.size]
    img.draw(@x - img.width / 2.0, @y - img.height / 2.0,
        ZOrder::STARS, 1, 1, @color, :add)
  end
end

Tutorial.new.show
Демонстраційне відео залив на YouTube та додав до допису (дивитись на початку цієї сторінки).

🔗 Цитувати допис: "Gosu Ruby Tutorial - пройдемось по офіційній документації"

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

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


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

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

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

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

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