ruby-sfml
Modern, idiomatic Ruby bindings for SFML 3.x via CSFML and Ruby FFI.
Status: the API surface is complete for SFML 3.0 — system, window, graphics, audio, network, plus the higher-level
GameandAssetshelpers. 287 RSpec examples, 20 runnable example folders. Some details (gem-build verification, RBS signatures, hosted docs) are still pending.
Why
The original rbSFML is unmaintained and only works against SFML 2 and Ruby 2.2. ruby-sfml targets the current SFML 3.x line, modern Ruby (3.2+), and a Ruby-first API — blocks instead of polling loops, symbols instead of enums, operators on vectors, automatic resource cleanup via GC.
Requirements
- Ruby
>= 3.2 - CSFML 3.0 or compatible 3.x at the system level
Install CSFML
| OS | Command | Notes |
|---|---|---|
| Ubuntu 25.04+ / Debian | sudo apt install libcsfml-dev |
Ships CSFML 3 |
| Ubuntu 22.04 / 24.04 | repo too old (CSFML 2.5) | Build from 3.0.0 release |
| macOS (brew) | brew install csfml |
Currently 3.x |
| Arch Linux | sudo pacman -S csfml |
Currently 3.x |
| Windows | https://www.sfml-dev.org/download/csfml/ | Pick the 3.0 tarball |
ruby-sfml verifies the linked CSFML twice:
- At
gem install—extconf.rbchecks for the fivelibcsfml-*libraries plus a CSFML 3.0+ symbol (sfClock_isRunning). Aborts with a clear message if the system has CSFML 2.x. - At
require "sfml"— same probe runs as a runtime sanity check, in case libraries were swapped between install and use.
You'll see a useful error either way; nothing falls through to a cryptic CSFML segfault.
A 12-line game
require "sfml"
class Hello < SFML::Game
def setup
@ball = SFML::CircleShape.new(radius: 30, fill_color: SFML::Color.white,
position: [200, 200])
end
def update(dt) = @ball.move(60 * dt.as_seconds * SFML::Vector2[1, 0])
def draw = window.draw(@ball)
end
Hello.new(title: "Hello", background: SFML::Color.cornflower_blue).run
SFML::Game handles window creation, the main loop, event pumping, dt, and the Esc/close-button quit. Override setup / update / draw / on_event. Drop into the manual loop style any time you want full control.
A 5-line manual loop
require "sfml"
window = SFML::RenderWindow.new(800, 600, "Hello", framerate: 60)
while window.open?
window.each_event do |event|
case event
in {type: :closed} then window.close
in {type: :key_pressed, code: :escape} then window.close
else # always include `else` — case/in raises on unmatched events.
end
end
window.clear(SFML::Color.cornflower_blue)
window.display
end
Available modules
| Area | Classes |
|---|---|
| System | Vector2, Vector3, Rect, Time, Clock |
| Window | RenderWindow, Window (bare, GL-only), VideoMode, Event, Keyboard, Mouse, Joystick, Cursor, Clipboard |
| Graphics | Color, Image, Texture, RenderTexture, Sprite, CircleShape, RectangleShape, ConvexShape, Vertex, VertexArray, Font, Text, View, BlendMode, RenderStates, Shader, Transform |
| Audio | SoundBuffer, Sound, Music, Listener, SoundRecorder, SoundBufferRecorder (3D positional audio supported on Sound and Music) |
| Helpers | Assets (search-path + cache), Game (lifecycle main loop) |
Network: IpAddress, TcpSocket, TcpListener, UdpSocket for stream / datagram networking.
What's intentionally not wrapped
CSFML 3 has a few corners we deliberately don't expose. Each is either (a) niche enough not to justify the surface area, (b) better served by a Ruby standard library, or (c) requires patterns that don't translate cleanly to FFI.
Use Ruby stdlib instead
sf::Http—Net::HTTPis a better Ruby fitsf::Ftp—Net::FTPlikewisesf::SocketSelector—IO.selector Async
Callback-based APIs that fight FFI / the GVL
- Raw
sf::SoundRecorder(per-buffer callbacks on the audio thread) — useSFML::SoundBufferRecorderfor "record into memory, save on stop" sf::SoundStream(custom audio source via inheritance) — niche; if you need it, generate samples to a file and play viaMusic
Mobile / niche inputs (SFML 3 itself treats these as experimental)
sf::Touch,sf::Sensor(accelerometer, gyro, etc.)
Advanced graphics features
sf::VertexBuffer(static GPU vertex buffer) —VertexArraycovers the common case; if you need static-mesh perf, open an issue- Geometry shaders — only vertex and fragment stages on
SFML::Shader sf::Shader#setUniformArray(bulk uniforms) — set elements one by one- Stencil buffer ops (
clearStencil, customStencilMode) — accept CSFML defaults sf::Image#saveToMemory— onlyImage#save(path)is wrapped
Advanced audio features
- Sound / Music cones, velocity, Doppler factor, custom DSP via
setEffectProcessor— basic 3D positional + attenuation is in; the rest is rarely used in 2D gamedev sf::Listenercone — same reasoning
Embedding / integration corners
RenderWindow.createFromHandle(embed in another framework's window)- Custom
sf::InputStreamfor loading assets from non-file sources - Window icon, min/max size, native handle accessors on
SFML::Window
Other Ruby bindings worth knowing about
- SFML 2.x is not covered. The previous-generation gem rbSFML targets SFML 2; it's unmaintained and only works with Ruby ≤ 2.2.
If anything in the list above is blocking you, open an issue — "niche" is just a default, not a closed door.
Examples
Each example is a self-contained folder under examples/, numbered roughly in learning order. Assets each example needs sit next to its script. Run from the gem root:
bundle exec ruby examples/<NN_name>/<name>.rb
| # | Example | What it shows |
|---|---|---|
| 01 | hello_window | Empty window, manual event loop |
| 02 | events_demo | Pattern matching on input events |
| 03 | bouncing_ball | dt-based physics, CircleShape + RectangleShape |
| 04 | game_class | Same idea on top of SFML::Game |
| 05 | mouse_demo | Polling vs. events; paint with the mouse |
| 06 | pong | Two-player Pong with in-window score (Text) and bounce Sound |
| 07 | scrolling_world | View as a 2D camera: drag-pan, wheel-zoom around cursor, FPS HUD |
| 08 | joystick_demo | Live gamepad inspector (axes, buttons, connect/disconnect) |
| 09 | image_viewer | Load a PNG, mutate the Image, re-upload to Texture on a key |
| 10 | pixel_paint | Paint into a CPU Image, blit to GPU Texture each dirty frame |
| 11 | particles | Thousands of points in one draw call via VertexArray + ConvexShape ground |
| 12 | render_texture | Off-screen RenderTexture for trail / motion-blur effects |
| 13 | tilemap | Textured VertexArray tilemap + additive BlendMode torch |
| 14 | shader_wave | Pure GLSL fragment Shader — procedural ripple + plasma |
| 15 | cursors_clipboard | All 21 system Cursor shapes + Clipboard copy/paste |
| 16 | spatial_audio | 3D positional Sound + Listener — three drones around the cursor |
| 17 | voice_memo | Record from microphone via SoundBufferRecorder, save + play back |
| 18 | draw_primitives | Raw draw_primitives — line burst rebuilt every frame |
| 19 | udp_loopback | UDP send/receive on localhost via Network::UdpSocket |
| 20 | bare_window | SFML::Window (no 2D batcher) — events for raw-OpenGL apps |
Idioms baked in
- Symbols, not enums:
Keyboard.key_pressed?(:escape), notKeyboard::Key::Escape. - Pattern matching for events:
ruby case event in {type: :key_pressed, code: :escape} in {type: :resized, size: {x:, y:}} in {type: :mouse_button_pressed, button: :left, position: {x:, y:}} end - Vectors with operators:
pos + velocity * dt,2 * vec,vec.length, deconstruction incase/in. - Kwargs constructors:
Sprite.new(texture, position: [0, 0], color: SFML::Color.red),CircleShape.new(radius: 10, fill_color: ...)— no setter chains. - Asset manager with cache:
SFML::Assets.font("DejaVuSans"),SFML::Assets.sound("blip")— load each thing once, refer by name. - GC-managed resources: every CSFML pointer goes through
FFI::AutoPointer, sosfXxx_destroyis called automatically.
Versioning
The gem version is MAJOR.MINOR.PATCH.GEM_PATCH — the first three segments mirror the CSFML release the gem was built against; the fourth is our own patch level for fixes / additions on top of the same upstream:
| gem version | targets CSFML | meaning |
|---|---|---|
3.0.0.0 |
3.0.0 | First cut against CSFML 3.0.0 |
3.0.0.1 |
3.0.0 | Bug fix on top of CSFML 3.0.0 |
3.0.1.0 |
3.0.1 | CSFML 3.0.1 ships, we re-cut |
3.1.0.0 |
3.1.0 | New CSFML minor — added bindings for new APIs |
SFML::CSFML_VERSION exposes the upstream string at runtime.
Bundler-pinning patterns:
gem "ruby-sfml", "~> 3.0" # any 3.x.x.x — typical
gem "ruby-sfml", "~> 3.0.0" # only 3.0.0.x — hold across a CSFML minor
gem "ruby-sfml", "~> 3.0.0.0" # only our patches on CSFML 3.0.0 — paranoid pin
Process exit
ruby-sfml installs a single at_exit hook that:
- Stops every live
SFML::Sound/SFML::Musicso the audio thread quiets before anything is freed. - Calls
Kernel#exit!with the appropriate status, bypassing Ruby's natural finalizer pass.
This is intentional. CSFML's GL context, font glyph atlases, and OpenAL state are reclaimed by the OS on process exit; running each FFI::AutoPointer finalizer in a non-deterministic order tends to crash inside libGL/libopenal. The OS doesn't care, and now neither do we.
The trade-off: any user at_exit hook registered before require "sfml" will be skipped. Hooks registered after the require run first (Ruby's at_exit is LIFO) and are unaffected. Put your require at the top of the file (the normal place for it) and there's nothing to think about.
Architecture
Two layers. Users only touch the top one.
SFML::C # thin FFI wrapper around CSFML, 1:1 with the C API
SFML # idiomatic Ruby on top
Each render target (RenderWindow + RenderTexture) includes a Graphics::RenderTarget mixin that dispatches clear, display, draw, view=, map_pixel_to_coords etc. through the includer's CSFML_PREFIX. Adding a new target (say a future RenderImage) is ~30 lines.
When SFML 3.1 / CSFML 3.1 ships, only the bottom layer typically needs to move.
Development
bundle install
bundle exec rspec # 287 examples
bundle exec rake rdoc # generate HTML docs in doc/ (Aliki theme via RDoc 7)
The spec suite hits real CSFML for everything that isn't pure Ruby — Clock reads the real monotonic clock, Text#local_bounds measures real glyphs, audio loads a WAV — so a green run also confirms the FFI bindings line up. spec/fixtures/ holds the only assets the suite touches (a font and a tiny WAV) so tests are independent of examples/.
CI runs the full suite on Ubuntu and macOS × Ruby 3.2 / 3.3 / 3.4. Linux builds CSFML 3 from source (cached), then runs specs under xvfb-run so the headless runner has an X server for RenderWindow.
License
MIT. See LICENSE.txt.
The gem also bundles DejaVu Sans under its permissive license — used as the default font when you don't supply your own.