Статьи

ROFLBALT

Сегодня мы расскажем о проекте, который несколько любящих Rubyists написали для RailsCamp в прошлом месяце. Пол Аннесли ( твиттер ) и Деннис Хотсон ( твиттер ) объединили свои усилия в RailsCamp Australia, и в результате получился фейерверк на основе ASCII.

Во-первых, если вы не знакомы с RailsCamps, вот реклама с их сайта:

Представьте себя и группу единомышленников-рубиновых хакеров в загородном уединении с нулевым интернетом для веселых выходных. Вы будете смеяться, взламывать, учиться, плакать (ну, вы, вероятно, не будете плакать … но вы знаете … это было поэтично), и, скорее всего, вы играете в Guitar Hero.

Смысл RailsCamp, насколько я могу судить, состоит в том, чтобы отключиться от всего, кроме Ruby и других участников. Это фестиваль программирования, где они поощряют хакерство и креативность — две вещи, которые в изобилии живут в сообществе Ruby. Все, кого я знаю, кто присутствовал на RailsCamp, вернулись и сказали, что это был шанс изменить их карьеру. Мне нужно получить один запланированный около Шарлотты.

Проэкт

Проект, на котором остановились Денис и Пол, назывался ROFLBALT. Это рубиновый порт несколько известной игры Canabalt , в которую можно играть онлайн или загрузить на мобильное устройство. Цель Canabalt (и всех игр -бальтового типа) — бегать и прыгать от здания к зданию, как можно дольше. Ваша оценка прямо пропорциональна тому, сколько времени вы пройдете, прежде чем умрете. Это просто кусочек мозгового леденца в простой игровой форме, тот тип конфет, который мой мозг любит больше всего.

Пол и Деннис стремились завершить ROFLBALT, используя менее 500 строк Ruby, и они выполнили его. Посмотрим как.

Как они это сделали

Я собираюсь попытаться разбить код для ROFLBALT. Теперь я предупреждаю вас, что Пол и Деннис, на первый взгляд и неудивительно, намного умнее меня. В некоторых частях этого кода я понятия не имел, как они 1) придумали, что они сделали, или 2) что, черт возьми, делает код. Это ни в коем случае не отражение в коде или проекте, а скорее в отношении автора, который является «особенным».

Вы можете найти код для проекта на github .

ROFLBALT разбит на следующие классы.

  • Игра
  • экран
  • пиксель
  • Фон
  • WindowColor
  • Кадровый буфер
  • Мир
  • BuildingGenerator
  • (Модуль) визуализируемых
  • Строительство
  • игрок
  • Кровь
  • Табло
  • GameOverBanner
  • RoflCoptor

Исполняемый файл (в Game.new.run bin) просто запускает Game.new.run , поэтому мы начнем с него.

Игра

SCREEN_WIDTH = 120
SCREEN_HEIGHT = 40
class Game
def initialize
reset
end
def reset
@run = true
@world = World.new(SCREEN_WIDTH)
@screen = Screen.new(SCREEN_WIDTH, SCREEN_HEIGHT, @world)
end
def run
Signal.trap(:INT) do
@run = false
end
while @run
start_time = Time.new.to_f
unless @world.tick
reset
end
render start_time
end
on_exit
end
def render start_time
@world.buildings.each do |building|
@screen.draw(building)
end
@screen.draw(@world.player)
@world.misc.each do |object|
@screen.draw(object)
end
@screen.render start_time
end
def on_exit
@screen.on_exit
end
end

view raw
game.rb
hosted with ❤ by GitHub

Игра, как вы уже догадались, расставляет все биты на месте, чтобы начать игру, а также управляет выходом из игры. Это зависит от мира и экрана. В методах Game все происходит на высоком уровне, так как мы имеем дело с объектом World и объектом Screen, которые являются абстракциями, созданными этим проектом. Игра начинается с веселья, в основном, в методе render . Он рисует здания, игрока и объекты остального мира. Обратите внимание, что «рисование» объекта означает передачу его методу draw на экране, что мы рассмотрим, когда попадем на экран. Кроме того, игра прослушивает прерывание сигнала (в данном случае Ctrl-C) и убивает игру, когда это происходит.

Мир

class World
def initialize horizon
@ticks = 0
@horizon = horizon
@building_generator = BuildingGenerator.new(self, WindowColor.new)
@background = Background.new(self)
@player = Player.new(25, @background)
@buildings = [ @building_generator.build(-10, 30, 120) ]
@misc = [ Scoreboard.new(self), RoflCopter.new(50, 4, @background) ]
@speed = 4
@distance = 0
end
attr_reader :buildings, :player, :horizon, :speed, :misc, :ticks, :distance, :background
def tick
# TODO: this, but less often.
if @ticks % 20 == 0
@building_generator.generate_if_necessary
@building_generator.destroy_if_necessary
end
@distance += speed
buildings.each do |b|
b.move_left speed
end
if b = building_under_player
if player.bottom_y > b.y
b.move_left(-speed)
@speed = 0
@misc << Blood.new(player.x, player.y)
@misc << GameOverBanner.new
player.die!
end
end
begin
if STDIN.read_nonblock(1)
if player.dead?
return false
else
player.jump
end
end
rescue Errno::EAGAIN
end
player.tick
if b = building_under_player
player.walk_on_building b if player.bottom_y >= b.y
end
@ticks += 1
end
def building_under_player
buildings.detect do |b|
b.x <= player.x && b.right_x >= player.right_x
end
end
end

view raw
world.rb
hosted with ❤ by GitHub

Мир содержит все элементы рендеринга, которые может содержать игра. Здания, фон, табло и игрок. у него также есть пара невидимых элементов, таких как Генератор зданий и горизонт (ширина экрана). В мире много зависимостей:

  • BuildingGenerator
  • Фон
  • игрок
  • Строительство
  • Табло

Мир также контролирует скорость игры и пройденное расстояние, как рассчитывается выигрыш. Метод галочки — это каждое «движение» мира. Когда игрок движется вправо (или, более точно, здания двигаются влево), метод тиков проверяет, чтобы убедиться, что здание все еще находится под игроком, в противном случае пришло время RENDER BLOOD и вызвать PLAYER.DIE (Извините, мне кажется, что я следует использовать заглавные буквы) Если вы не мертвы, это добавляет один к галочкам

экран

class Screen
OFFSET =20
def initialize width, height, world
@width = width
@height = height
@world = world
@background = world.background
create_frame_buffer
%x{stty -icanon -echo}
print «\033[0m» # reset
print «\033[2J» # clear screen
print «\x1B[?25l» # disable cursor
end
attr_reader :width, :height, :world
def create_frame_buffer
@fb = Framebuffer.new @background
end
def draw renderable
renderable.each_pixel(world.ticks) do |x, y, pixel|
@fb.set x, y, pixel
end
end
def render start_time
print «\e[H»
buffer = »
previous_pixel = nil
(0height).each do |y|
(OFFSET(width + OFFSET)).each do |x|
pixel = @fb.get(x, y)
if Pixel === previous_pixel && Pixel === pixel && pixel.color_equal?(previous_pixel)
buffer << pixel.char
else
buffer << pixel.to_s
end
previous_pixel = pixel
end
buffer << «\n«
end
print «\033[0m»
dt = Time.new.to_fstart_time;
target_time = 0.04
sleep target_timedt if dt < target_time
print buffer
create_frame_buffer
end
def on_exit
print «\033[0m» # reset colours
print «\x1B[?25h» # re-enable cursor
print «\n«
end
end

view raw
screen.rb
hosted with ❤ by GitHub

Экран инициализируется шириной и высотой нашей игровой «канвы», наряду с игровым миром. Если вы посмотрите на метод initialize, он выполнит некоторую stty магию, чтобы очистить экран и отключить курсор. Именно такие вещи заставляют меня любить читать чужой код. Я всегда вижу и изучаю вещи, которые мне не хватало бы в моей повседневной рутине программирования. Экран отвечает за рендеринг начальной игры «canvas» и рисование Renderables на холсте. Renderable — это классы, включающие модуль Renderable, к которому мы скоро перейдем.

пиксель

class Pixel
def initialize char = » «, fg = nil, bg = nil
@char = char
@fg, @bg = fg, bg
end
attr_reader :char
def fg; @fg || 255 end
def bg; @bg || 0 end
def to_s
«\033[48;5;%dm\033[38;5;%dm%s» % [ bg, fg, @char ]
end
def color_equal? other
fg == other.fg && bg == other.bg
end
end

view raw
pixel.rb
hosted with ❤ by GitHub

Класс Pixel представляет игровой пиксель, который является символом ASCII в ROLFBALT. Экземпляр Pixel состоит из цвета переднего плана, цвета фона и символа. Классная вещь, которую я узнал из класса Pixel, — как связываться с моими терминальными цветами. Метод to_s показывает это, используя метод интерполяции необычной строки (http://apidock.com/ruby/String/%25). Я переключал свой терминал с черного на белое на белое на черном и хихикал, когда моя жена проходила мимо и спрашивала, о чем я хихикаю. Все, что я мог сказать, это «умник» и понять, что подобные ситуации отделяют нас от «нормалей». В любом случае, я отвлекся… назад к коду…

Фон

class Background
PALETTE = [ 16, 232, 233 ]
PERIOD = 16.0
SPEED = 0.5
BLOCKINESS = 10.0
def initialize world
@world = world
end
def pixel x, y, char = » «
Pixel.new char, 0, color(x, y)
end
def color x, y
y = (y / BLOCKINESS).round * BLOCKINESS
sin = Math.sin((x + @world.distance.to_f * SPEED) / PERIOD + y / PERIOD)
PALETTE[(0.9 * sin + 0.9).round]
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Фон воспринимает мир как зависимость и в основном отвечает за возвращение цвета за все остальное. Например, когда игрок рендерится, он передает backround.color () в качестве цвета фона пикселям игроков. Милый маленький, сосредоточенный класс. Мне это нравится.

WindowColor

class WindowColor
PALETTE = [ 16, 60 ]
PERIOD = 6.0
def pixel x, y, char = » «
Pixel.new char, 0, color(x, y)
end
def color x, y
sin = Math.sin(x / PERIOD + y / (PERIOD * 0.5))
PALETTE[(0.256 * sin + 0.256).round]
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Цвет окон на зданиях. BuildingGenerator использует его. Это все

Кадровый буфер

class Framebuffer
def initialize background
@pixels = Hash.new { |h, k| h[k] = {} }
@background = background
end
def set x, y, pixel
@pixels[x][y] = pixel
end
def get x, y
@pixels[x][y] || @background.pixel(x, y)
end
def size
@pixels.values.reduce(0) { |a, v| a + v.size }
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Framebuffer создается классом Screen и принимает фон. Метод initialize напомнил мне о классном способе передачи блока в Hash Intializer, чтобы получить хеш, который имеет значение по умолчанию для любого члена, к которому осуществляется доступ. Рендеринг пикселей подается в кадровый буфер, а затем другие вещи (например, экран) могут захватить их позже. Он также использует фон, передавая фоновые пиксели по умолчанию, когда кадровый буфер не имеет пикселя для запрошенного x, y.

BuildingGenerator

class BuildingGenerator
def initialize world, background
@world = world
@background = background
end
def destroy_if_necessary
while @world.buildings.any? && @world.buildings.first.x < —100
@world.buildings.shift
end
end
def generate_if_necessary
while (b = @world.buildings.last).x < @world.horizon
@world.buildings << build(
b.right_x + minimium_gap + rand(24),
next_y(b),
rand(40) + 40
)
end
end
def minimium_gap; 16 end
def maximum_height_delta; 10 end
def minimum_height_clearance; 12; end
def next_y previous_building
p = previous_building
delta = 0
while delta.abs <= 1
delta = maximum_height_delta * —1 + rand(2 * maximum_height_delta + 1)
end
[25, [previous_building.ydelta, minimum_height_clearance].max].min
end
def build x, y, width
Building.new x, y, width, @background
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

BuildingGenerator берет мир и фон и, как вы уже догадались, он создает здания. Методы здесь сосредоточены на разрушении строительных объектов (путем изъятия их из мира world.buildings) и создания застройки как мира витки движется. Метод generate_if_necessary — это цикл, который продолжает создавать здания, в то время как координата x последнего здания меньше, чем горизонт миров (который является шириной экрана, если вы помните). Он вызывает метод сборки, который строит здание. Функция next_y учитывает предыдущее здание и гарантирует, что новое здание не слишком высоко для нашего игрока, чтобы прыгать. Это проблема домена, которую вы должны решить при создании видеоигры. 🙂

визуализируемых

module Renderable
def each_pixel ticks
(y(y + height)).each do |y|
(x(x + width)).each do |x|
rx = xself.x
ry = yself.y
yield x, y, pixel(x, y, rx, ry, ticks)
end
end
end
def right_x; x + width end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Это подводит нас к модулю Renderable, который входит в:

  • Строительство
  • игрок
  • Кровь (хи)
  • Табло
  • GameOverBanner
  • RoflCopter

Он определяет два метода: each_pixel и right_x . Кроме того, он ожидает, что включенный класс определит метод pixel , а также свойства (или методы) x , y , height и width . each_pixel говоря, метод each_pixel перебирает каждый пиксель объекта и возвращает x, y. этого пикселя, вместе с символом, который представляет этот пиксель для данного Renderable. В некотором смысле, он работает так же, как ваш телевизор, сканируя изображения и рендеринг их, пиксель за пикселем. Мне нравится, как Renderable делегирует pixel метод, заключая четкий контракт с вещами, которые включают Renderable, а также с вещами, которые используют Renderables.

Строительство

class Building
include Renderable
def initialize x, y, width, background
@x, @y = x, y
@width = width
@background = background
@period = rand(4) + 6
@window_width = @periodrand(2)1
@color = (235..238).to_a.shuffle.first # Ruby 1.8
@top_color = @color + 4
@left_color = @color + 2
end
attr_reader 😡, :y, :width
def move_left distance
@x -= distance
end
def height; SCREEN_HEIGHT@y end
def pixel x, y, rx, ry, ticks
if ry == 0
if rx == width1
Pixel.new » «
else
Pixel.new «=», 234, @top_color
end
elsif rx == 0 || rx == 1
Pixel.new «:», @left_color + 1, @left_color
elsif rx == 2
Pixel.new «:», 236, 236
elsif rx == width1
Pixel.new «:», 236, 236
else
if rx % @period >= @period@window_width && ry % 5 >= 2
Pixel.new(» «, 255, @background.color(rx + x/2, ry))
else
Pixel.new(«:», 235, @color)
end
end
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Глядя на наш первый Renderable, Building, он принимает x, y, который является самым левым x и самым большим y, шириной и фоновым объектом. Переместить его влево так же просто, как уменьшить его свойство x. Сложность происходит в пиксельном методе, ожидаемом от Renderable. Цель состоит в том, чтобы выяснить, для данного x / y, какой символ и цвет отображать. Если посмотреть на начало оператора if , если расстояние между y текущего пикселя и y здания равно нулю, то мы находимся на вершине здания, поэтому нарисуем «=». Остальная логика та же самая, выясняя, какой символ и цвет возвращать. Довольно умный.

игрок

class Player
include Renderable
def initialize y, background
@y = y
@background = background
@velocity = 1
@walking = false
end
def x; 0; end
def width; 3 end
def height; 3 end
def pixel x, y, rx, ry, ticks
Pixel.new char(rx, ry, ticks), 255, @background.color(x, y)
end
def char rx, ry, ticks
if dead?
[
‘ @ ‘,
‘\+/’,
‘ \\\\’,
][ry][rx]
elsif !@walking
[
‘ O/’,
‘/| ‘,
‘/ >’,
][ry][rx]
else
[
[
‘ O ‘,
‘/|v’,
‘/ >’,
],
[
‘ 0 ‘,
‘,|\\’,
‘ >\\’,
],
][ticks / 4 % 2][ry][rx]
end
end
def acceleration
if @dead
0.05
else
0.35
end
end
def tick
@y += @velocity
@velocity += acceleration
@walking = false
end
def y; @y.round end
def bottom_y; y + height end
def walk_on_building b
@y = b.yheight
@velocity = 0
@walking = true
end
def jump
jump! if @walking
end
def jump!
@velocity =2.5
end
def die!
@dead = true
@velocity = 0
end
def dead?
@dead
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Player — это еще один рендеринг, но он обрабатывает, какого персонажа рисовать иначе, чем строение. Глядя на код, он использует дельту между текущим x, y и игроком x, y, чтобы выяснить, какой символ возвращать, основываясь на одном из трех состояний: мертв, ходьба или другое (прыжок). Каждое состояние состоит из двумерного массива, который содержит фактические символы, которые вы можете видеть буквально. Код здесь очень умный по своей структуре, он формирует игрока в состоянии, затем использует индексы rx / ry для захвата персонажа. Довольно гениально.

Кровь

class Blood < Struct.new(:x, :y)
include Renderable
def height; 4 end
def width; 2 end
def x; super + 2 end
def pixel x, y, rx, ry, ticks
Pixel.new «:», 124, 52
end
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Класс Крови всегда использует один и тот же символ и цвет, но мне пришлось выделить для него собственный раздел. Я просто ЛЮБЛЮ, что есть класс Крови.

Табло и GameOverBanner

class Scoreboard
include Renderable
def initialize world
@world = world
end
def height; 3 end
def width; 20 end
def x;18 end
def y; 1 end
def template
[
‘ ‘,
‘ Score: %9s ‘ % [ @world.distance],
‘ ‘
]
end
def pixel x, y, rx, ry, ticks
Pixel.new template[ry][rx], 244, 234
end
end
class GameOverBanner
FG = 16
BG = 244
include Renderable
def x; 28 end
def y; 14 end
def width; 28 end
def height; 3 end
def template
[
‘ ‘,
‘ YOU DIED. LOL. ‘,
‘ ‘,
]
end
def pixel x, y, rx, ry, ticks
Pixel.new template[ry][rx], FG, BG
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

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

RoflCoptor

Переворот, наш ROFLCopter. Подход здесь, опять же, похож на другие основанные на шаблонах средства визуализации, но здесь у нас есть несколько шаблонов или фреймов. Х и у коптора изменяются в зависимости от текущего времени, используя формулу, которую я не очень понимаю. Я думаю, что мой любимый бит — спасение, где утверждается, что «RoflCopter» время от времени дает сбой.

Время играть

Ну, Пол и Деннис справились. Порт Canabalt для Ruby, который хорошо воспроизводится, а также содержит интересный код. Когда я попросил у джентльменов какие-либо анекдоты об этом, они ответили: «Попросите читателей заставить его работать в Ruby 1.8 как вызов». Итак, кто-нибудь готов к этому? А пока я собираюсь побить свой рекорд в ROFLBALT.