Class: Example
- Inherits:
-
RGame::Core::App
- Object
- RGame::Core::App
- Example
- 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
-
#cursor_x ⇒ Object
readonly
Returns the value of attribute cursor_x.
-
#cursor_y ⇒ Object
readonly
Returns the value of attribute cursor_y.
-
#frames ⇒ Object
readonly
Returns the value of attribute frames.
-
#ticks ⇒ Object
readonly
Returns the value of attribute ticks.
Attributes inherited from RGame::Core::App
Instance Method Summary collapse
- #audio_status ⇒ Object
- #button_down(id) ⇒ Object
-
#draw ⇒ Object
One of everything, so a look at the window checks the whole drawing path.
-
#frame_begin ⇒ Object
Once per frame, before the tick batch: pick which device to read.
-
#gamepad_connected(slot) ⇒ Object
The callbacks are for reacting to the change; the polling above is for reading the current state.
- #gamepad_disconnected(slot) ⇒ Object
-
#held?(key, pad) ⇒ Boolean
Core is the raw layer: Input answers "is this scancode down on this device", and naming the key and the pad button for one intent is the caller's job.
-
#initialize(sound_path = nil) ⇒ Example
constructor
A new instance of Example.
- #resize(width, height) ⇒ Object
-
#toggle_music ⇒ Object
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.
-
#update(dt) ⇒ Object
One fixed simulation tick.
Methods inherited from RGame::Core::App
#assets, #audio, #button_up, #caption, #caption=, #close, #fps, #frame_end, #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_x ⇒ Object (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_y ⇒ Object (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 |
#frames ⇒ Object (readonly)
Returns the value of attribute frames.
50 51 52 |
# File 'ext/rgame_core/example.rb', line 50 def frames @frames end |
#ticks ⇒ Object (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_status ⇒ Object
173 174 175 176 177 |
# File 'ext/rgame_core/example.rb', line 173 def audio_status return AUDIO_HINT_NONE unless @song @song. ? AUDIO_HINT_PLAYING : AUDIO_HINT_STOPPED end |
#button_down(id) ⇒ Object
155 156 157 158 159 160 161 162 163 |
# File 'ext/rgame_core/example.rb', line 155 def (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 |
#draw ⇒ Object
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.
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 143 144 145 146 147 148 149 150 151 152 153 |
# File 'ext/rgame_core/example.rb', line 115 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_begin ⇒ Object
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.
182 183 184 |
# File 'ext/rgame_core/example.rb', line 182 def gamepad_connected(slot) puts "controller connected in slot #{slot}: #{@pads.name(slot)}" end |
#gamepad_disconnected(slot) ⇒ Object
186 187 188 |
# File 'ext/rgame_core/example.rb', line 186 def gamepad_disconnected(slot) puts "controller disconnected from slot #{slot}" end |
#held?(key, pad) ⇒ Boolean
Core is the raw layer: Input answers "is this scancode down on this device", and naming the key and the pad button for one intent is the caller's job. Asking both costs nothing, because a device only answers for its own kind of input — a gamepad reads false for a scancode.
A game does not write this. RGame::Engine::InputMap is a table of exactly these pairs, one entry per action, and the scene graph reads named actions instead. This example is here to exercise Core on its own, so it does without that and shows what the layer above is for.
110 |
# File 'ext/rgame_core/example.rb', line 110 def held?(key, pad) = @input.down?(key, device: @device) || @input.down?(pad, device: @device) |
#resize(width, height) ⇒ Object
190 191 192 |
# File 'ext/rgame_core/example.rb', line 190 def resize(width, height) puts "resized to #{width}x#{height}" end |
#toggle_music ⇒ Object
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.
167 168 169 170 171 |
# File 'ext/rgame_core/example.rb', line 167 def toggle_music return unless @song @song. ? @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 held?(Controls::KEY_LEFT, Controls::PAD_DPAD_LEFT) @cursor_x += speed if held?(Controls::KEY_RIGHT, Controls::PAD_DPAD_RIGHT) @cursor_y -= speed if held?(Controls::KEY_UP, Controls::PAD_DPAD_UP) @cursor_y += speed if held?(Controls::KEY_DOWN, Controls::PAD_DPAD_DOWN) # Analog sticks are additive on top of the dpad; the keyboard reads 0.0. @cursor_x += @input.axis(Controls::AXIS_LEFT_X, device: @device) * speed @cursor_y += @input.axis(Controls::AXIS_LEFT_Y, device: @device) * speed end |