Class: SFML::App

Inherits:
Object
  • Object
show all
Extended by:
InputActions, Keybindings
Includes:
InputQueries
Defined in:
lib/sfml/app.rb

Overview

Subclass-friendly main loop. Removes the boilerplate of window creation, event pumping, clock management, and clear/display so a small app fits in a few methods.

class MyApp < SFML::App
antialiasing 4               # class-level config — applies to every instance
framerate    120
background   SFML::Color.new(18, 20, 28)

def setup
  @ball = SFML::CircleShape.new(radius: 30, position: [200, 200],
                                fill_color: SFML::Color.white)
end

def update(dt)
  @ball.move([60 * dt.as_seconds, 30 * dt.as_seconds])
end

def draw
  window.draw(@ball)
end
end

MyApp.new(title: "Hello").run

The loop auto-handles :closed and :key_pressed/:escape by calling #quit; everything else is forwarded to #on_event. Override that to handle keys, mouse, etc.

Configuration

Every constructor kwarg is also a class-level macro: declare defaults with antialiasing 4 etc. inside the class body, and MyApp.new picks them up. Per-instance kwargs still win on a case-by-case basis. Subclasses inherit their parent's settings — set one in a base class, override in a subclass.

Constant Summary collapse

CONFIG_KEYS =

Names of every class-level configuration macro on SFML::App.

%i[
  width height title
  framerate vsync background
  style fullscreen
  antialiasing context
  fixed_timestep
].freeze
FIXED_TIMESTEP_MAX_CATCHUP =

Cap on catch-up update calls per frame when running with fixed_timestep. Prevents the "spiral of death" where a slow frame queues so many physics steps that the next frame is even slower. After this many steps, residual time is dropped.

5

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from InputActions

action, action_bindings

Methods included from Keybindings

key_handlers, on_key

Methods included from InputQueries

#action_pressed?, #axis

Constructor Details

#initialize(**opts) ⇒ App

Per-instance kwargs override anything set at the class level. Anything not given here OR at the class level falls back to the hard-coded defaults below.



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/sfml/app.rb', line 96

def initialize(**opts)
  cfg = self.class

  width        = opts[:width]        || cfg.width        || 800
  height       = opts[:height]       || cfg.height       || 600
  title        = opts[:title]        || cfg.title        || cfg.name
  framerate    = opts[:framerate]    || cfg.framerate    || 60
  vsync        = opts.fetch(:vsync,        cfg.vsync)
  background   = opts[:background]   || cfg.background   || Color::BLACK
  style        = opts.fetch(:style,        cfg.style)
  fullscreen   = opts.fetch(:fullscreen,   cfg.fullscreen)
  fullscreen   = false if fullscreen.nil?
  antialiasing = opts.fetch(:antialiasing, cfg.antialiasing)
  context      = opts.fetch(:context,      cfg.context)

  window_opts = { framerate: framerate, fullscreen: fullscreen }
  window_opts[:vsync]        = vsync         unless vsync.nil?
  window_opts[:style]        = style         unless style.nil?
  window_opts[:antialiasing] = antialiasing  unless antialiasing.nil?
  window_opts[:context]      = context       unless context.nil?

  @window           = RenderWindow.new(width, height, title, **window_opts)
  @background_color = background
end

Instance Attribute Details

#background_color ⇒ Object

The background color.



91
92
93
# File 'lib/sfml/app.rb', line 91

def background_color
  @background_color
end

#current_scene ⇒ Object (readonly)

---- Scenes ----

current_scene is whatever the app last activated via switch_to. When non-nil, App's default update / draw / on_event / on_resize forward to it; key bindings on the scene class shadow ones on the app class while the scene is active.



142
143
144
# File 'lib/sfml/app.rb', line 142

def current_scene
  @current_scene
end

#interpolation_alpha ⇒ Object (readonly)

Fraction of a fixed timestep accumulated since the last update. In range [0.0, 1.0). Use it in #draw to interpolate between the previous and current world state:

def draw
pos = @prev_pos.lerp(@curr_pos, interpolation_alpha)
...
end

Only meaningful when fixed_timestep is set; otherwise 0.



186
187
188
# File 'lib/sfml/app.rb', line 186

def interpolation_alpha
  @interpolation_alpha
end

#window ⇒ Object (readonly)

The Backing SFML::RenderWindow.



89
90
91
# File 'lib/sfml/app.rb', line 89

def window
  @window
end

Class Method Details

.initial_scene(klass = nil) ⇒ Object

Set the scene class the app should switch into automatically at setup time. Inheritable: a subclass that doesn't set one inherits the parent's choice.



79
80
81
82
83
# File 'lib/sfml/app.rb', line 79

def initial_scene(klass = nil)
  @initial_scene = klass if klass
  return @initial_scene if instance_variable_defined?(:@initial_scene)
  superclass.respond_to?(:initial_scene) ? superclass.initial_scene : nil
end

Instance Method Details

#draw ⇒ Object

Returns the draw.



253
254
255
# File 'lib/sfml/app.rb', line 253

def draw
  @current_scene&.draw
end

#height ⇒ Object

Returns the height.



125
# File 'lib/sfml/app.rb', line 125

def height = @window.size.y

#on_event(event) ⇒ Object

The framework consumes:

* `:closed` — closes the window (always)
* `:resized` — forwarded to `on_resize`
* `:key_pressed` whose code matches a scene- or app-level
`on_key` binding

Everything else lands here. Override to handle game-specific input. By default forwards to the current scene's on_event.



264
265
266
# File 'lib/sfml/app.rb', line 264

def on_event(event)
  @current_scene&.on_event(event)
end

#on_resize(width, height) ⇒ Object

Default: forward to the current scene. Override to additionally do app-wide layout fixups; call super to keep the scene in the loop.



271
272
273
# File 'lib/sfml/app.rb', line 271

def on_resize(width, height)
  @current_scene&.on_resize(width, height)
end

#pause ⇒ Object

Pause the simulation. #draw keeps firing; #update doesn't.



168
169
# File 'lib/sfml/app.rb', line 168

def pause   = (@paused = true)
# Resume the simulation.

#paused? ⇒ Boolean

true while the simulation is paused.

Returns:

  • (Boolean)


174
# File 'lib/sfml/app.rb', line 174

def paused? = @paused == true

#quit ⇒ Object

Close the window. The main loop exits at the start of the next frame.



129
130
131
132
# File 'lib/sfml/app.rb', line 129

def quit
  @window.close
  self
end

#resume ⇒ Object

Resume the simulation.



170
171
# File 'lib/sfml/app.rb', line 170

def resume  = (@paused = false)
# Flip the pause state.

#run ⇒ Object

The main entry point. Calls #setup once, then runs the per-frame loop until the window closes.

When fixed_timestep N is set on the class, update(dt) is called exactly N times per second (with a fixed dt), and rendering runs as fast as vsync/framerate allows. This is the standard pattern for deterministic physics — without it, large-dt frames produce different results than small-dt frames.



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/sfml/app.rb', line 196

def run
  setup
  clock           = Clock.new
  ts              = self.class.fixed_timestep
  step_seconds    = ts && (1.0 / ts)
  dt_fixed        = ts && Time.seconds(step_seconds)
  accumulator     = 0.0
  @interpolation_alpha = 0.0

  while @window.open?
    frame_dt = clock.restart
    @window.each_event { |event| _dispatch(event) }

    if dt_fixed
      accumulator += frame_dt.as_seconds
      steps = 0
      while accumulator >= step_seconds && steps < FIXED_TIMESTEP_MAX_CATCHUP
        update(dt_fixed) unless paused?
        accumulator -= step_seconds
        steps += 1
      end
      # If we hit the catch-up cap, drop residual time — better
      # to slightly slow the simulation than spiral into longer
      # and longer frames.
      accumulator = 0.0 if steps == FIXED_TIMESTEP_MAX_CATCHUP
      @interpolation_alpha = accumulator / step_seconds
    else
      update(frame_dt) unless paused?
    end

    @window.clear(@background_color)
    draw
    @window.display
  end
  teardown
  self
end

#setup ⇒ Object

---- Override these in subclasses --------------------------------------

Defaults forward to the current scene when one is active. The initial_scene class macro auto-instantiates a scene at setup time so subclasses with initial_scene Foo don't have to define setup themselves.



241
242
243
244
245
# File 'lib/sfml/app.rb', line 241

def setup
  if (klass = self.class.initial_scene)
    switch_to(klass)
  end
end

#switch_to(scene_or_class) ⇒ Object

Activate scene_or_class. Tears down the previous scene before calling setup on the new one. Accepts either:

* a Scene subclass — instantiated with `self` as host
* an existing Scene instance — used as-is


148
149
150
151
152
153
154
155
156
157
158
# File 'lib/sfml/app.rb', line 148

def switch_to(scene_or_class)
  new_scene =
    case scene_or_class
    when Class then scene_or_class.new(self)
    else            scene_or_class
    end
  @current_scene&.teardown
  @current_scene = new_scene
  @current_scene&.setup
  @current_scene
end

#teardown ⇒ Object

Called once after the main loop exits. Tears down the active scene by default.



277
278
279
# File 'lib/sfml/app.rb', line 277

def teardown
  @current_scene&.teardown
end

#toggle_pause ⇒ Object

Flip the pause state.



172
173
# File 'lib/sfml/app.rb', line 172

def toggle_pause = (@paused = !paused?)
# `true` while the simulation is paused.

#update(dt) ⇒ Object

Returns the update.



248
249
250
# File 'lib/sfml/app.rb', line 248

def update(dt)
  @current_scene&.update(dt)
end

#width ⇒ Object

Width and height shortcuts that always reflect the current window size — matters once the user is allowed to resize.



123
124
# File 'lib/sfml/app.rb', line 123

def width  = @window.size.x
# Returns the height.