Class: Example

Inherits:
RGame::Core::App show all
Defined in:
ext/rgame_core/example.rb

Overview

An App subclass overrides only the hooks it needs; the rest are inherited no-ops, so this class is the smallest useful driver of the engine.

Constant Summary collapse

Controls =
RGame::Util::Controls
Color =
RGame::Util::Color
RED =
Color.new(224, 64, 64)
GREEN =
Color.new(64, 224, 96)
BLUE =
Color.new(64, 96, 224)
YELLOW =
Color.new(224, 192, 64)
TRANSLUCENT_WHITE =
Color.new(255, 255, 255, 128)
AUDIO_HINT_NONE =

Frozen and chosen between rather than built: #draw runs sixty times a second, and a string per frame is a garbage collection waiting to happen.

'audio: pass a sound file on the command line to try it'
AUDIO_HINT_STOPPED =
'audio: Space plays a sample, Return starts the music'
AUDIO_HINT_PLAYING =
'audio: Space plays a sample, Return stops the music'

Instance Attribute Summary collapse

Attributes inherited from RGame::Core::App

#media_root

Instance Method Summary collapse

Methods inherited from RGame::Core::App

#assets, #audio, #button_up, #caption, #caption=, #close, #fps, #gamepad_count, #gamepad_name, #gamepad_present?, #height, #input_axis, #input_down?, #needs_redraw?, #run, #ticks_ms, #width

Constructor Details

#initialize(sound_path = nil) ⇒ Example

Returns a new instance of Example.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'ext/rgame_core/example.rb', line 52

def initialize(sound_path = nil)
  super(width: 800, height: 600, caption: 'rgame via Ruby')
  @input = RGame::Core::Input.new(self)
  @pads = RGame::Core::Gamepad.new(self)
  @renderer = RGame::Core::Renderer.new(self)
  @frames = 0
  @ticks = 0
  @cursor_x = 400.0
  @cursor_y = 300.0
  @spin = 0.0
  @baked = nil
  @device = Controls::KEYBOARD

  # The device is opened whether or not there is anything to play through it,
  # so that starting with no sound card takes the same path as starting with
  # one. Nothing here is tied to the window: audio has no GL context and
  # survives one being recreated.
  @audio = RGame::Core::Audio.new
  puts "audio backend: #{@audio.backend}"
  return unless sound_path

  @sample = RGame::Core::Sample.new(@audio, sound_path)
  @song = RGame::Core::Song.new(@audio, sound_path)
end

Instance Attribute Details

#cursor_xObject (readonly)

Returns the value of attribute cursor_x.



50
51
52
# File 'ext/rgame_core/example.rb', line 50

def cursor_x
  @cursor_x
end

#cursor_yObject (readonly)

Returns the value of attribute cursor_y.



50
51
52
# File 'ext/rgame_core/example.rb', line 50

def cursor_y
  @cursor_y
end

#framesObject (readonly)

Returns the value of attribute frames.



50
51
52
# File 'ext/rgame_core/example.rb', line 50

def frames
  @frames
end

#ticksObject (readonly)

Returns the value of attribute ticks.



50
51
52
# File 'ext/rgame_core/example.rb', line 50

def ticks
  @ticks
end

Instance Method Details

#audio_statusObject



162
163
164
165
166
# File 'ext/rgame_core/example.rb', line 162

def audio_status
  return AUDIO_HINT_NONE unless @song

  @song.playing? ? AUDIO_HINT_PLAYING : AUDIO_HINT_STOPPED
end

#button_down(id) ⇒ Object



144
145
146
147
148
149
150
151
152
# File 'ext/rgame_core/example.rb', line 144

def button_down(id)
  case id
  when Controls::KEY_ESCAPE then close
  # Held down, this is the overlap check: each press is another voice rather
  # than a restart, so a fast run of them should pile up rather than stutter.
  when Controls::KEY_SPACE then @sample&.play
  when Controls::KEY_RETURN then toggle_music
  end
end

#drawObject

One of everything, so a look at the window checks the whole drawing path. Colours are RGame::Util::Color values rather than arrays: a Color is frozen and allocates nothing when it is drawn, which is what per-frame code wants.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'ext/rgame_core/example.rb', line 104

def draw
  r = @renderer

  # Baked on the first frame and replayed after that: forty rectangles cost
  # one call per frame instead of forty. A real game bakes its tile layers
  # this way. Recording has to happen inside #draw, which is why it is here
  # rather than in initialize.
  @baked ||= r.record do
    40.times { |i| r.rect(i * 18, 0, 12, 12, color: GREEN, z: 0) }
  end
  # Scrolls with the same value the spinning square turns by, so it is
  # visibly the *replay* moving and not a rebake.
  @baked.draw((@spin % 18) - 18, 560)
  r.rect(40, 40, 160, 100, color: RED, z: 0)
  # Higher z wins wherever they overlap, whatever order the calls came in.
  r.rect(120, 90, 160, 100, color: TRANSLUCENT_WHITE, z: 1)
  r.triangle(400, 40, 480, 180, 320, 180, color: GREEN, z: 0)
  r.circle(620, 110, 70, color: BLUE, z: 0)
  r.line(40, 240, 760, 240, thickness: 6, color: YELLOW, z: 0)

  # The transform stack: a square turning about its own centre.
  r.rotated(@spin, 400, 380) { r.rect(340, 320, 120, 120, color: GREEN, z: 0) }

  # The clip stack: the rectangle is twice the size of what shows.
  r.clipped(60, 480, 200, 80) { r.rect(60, 440, 400, 160, color: RED, z: 0) }

  # The cursor the arrow keys move.
  r.debug_box(@cursor_x - 8, @cursor_y - 8, 16, 16)

  # Text, including accents and punctuation the shipped font has to cover, so
  # a look at the window checks kerning and coverage at once. The second line
  # is centred using text_width, which is what a UI actually does with it.
  r.text('rgame — Grüße, œuvre, 5 €', 40, 270, color: YELLOW)
  label = format('%d fps', fps)
  r.text(label, 400 - (r.text_width(label) / 2), 270 + r.text_height)
  r.text(audio_status, 40, 270 + (r.text_height * 2), color: YELLOW)

  @frames += 1
end

#frame_beginObject

Once per frame, before the tick batch: pick which device to read. Doing it here rather than per tick is the pattern the engine is shaped around — one sample per frame, reused by however many catch-up ticks follow.



80
81
82
83
84
# File 'ext/rgame_core/example.rb', line 80

def frame_begin
  # Poll rather than track: Gamepad answers straight from the engine, so
  # there is no local copy of the connection state to keep in step.
  @device = @pads.connected?(0) ? @pads.device(0) : Controls::KEYBOARD
end

#gamepad_connected(slot) ⇒ Object

The callbacks are for reacting to the change; the polling above is for reading the current state. Slots are stable across a replug, so a pad that falls out and returns comes back as the same player.



171
172
173
# File 'ext/rgame_core/example.rb', line 171

def gamepad_connected(slot)
  puts "controller connected in slot #{slot}: #{@pads.name(slot)}"
end

#gamepad_disconnected(slot) ⇒ Object



175
176
177
# File 'ext/rgame_core/example.rb', line 175

def gamepad_disconnected(slot)
  puts "controller disconnected from slot #{slot}"
end

#resize(width, height) ⇒ Object



179
180
181
# File 'ext/rgame_core/example.rb', line 179

def resize(width, height)
  puts "resized to #{width}x#{height}"
end

#toggle_musicObject

Return is a toggle, so one key covers both transitions and the "play after stop starts from the beginning" behaviour is audible by pressing it twice.



156
157
158
159
160
# File 'ext/rgame_core/example.rb', line 156

def toggle_music
  return unless @song

  @song.playing? ? @song.stop : @song.play(looping: true)
end

#update(dt) ⇒ Object

One fixed simulation tick. dt is always the engine's fixed step.



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'ext/rgame_core/example.rb', line 87

def update(dt)
  @ticks += 1
  @spin += dt * 45.0
  speed = 200.0 * dt
  @cursor_x -= speed if @input.down?(:left, device: @device)
  @cursor_x += speed if @input.down?(:right, device: @device)
  @cursor_y -= speed if @input.down?(:up, device: @device)
  @cursor_y += speed if @input.down?(:down, device: @device)

  # Analog sticks are additive on top of the dpad; the keyboard reads 0.0.
  @cursor_x += @input.axis(:move_x, device: @device) * speed
  @cursor_y += @input.axis(:move_y, device: @device) * speed
end