RGame
RGame is a small 2D game engine for Ruby, written in Ruby and C. It's built with testability and performance in mind, and aims to be an engine where you can write your whole game code in Ruby, test it as usual with RSpec (or Minitest, or another test framework) and still have acceptable performance.
Why does this exist and should you use it?
RGame is not a universal game engine yet and is currently aimed at games with the following criteria:
- top-down 2D
- tiled maps and sprites for characters
- controlled with a controller or keyboard alone (or multiple controllers, it supports local multiplayer and split-screen)
- dialogue-, puzzle- or quest-based. There is no combat system in this engine, although you can of course build one
- pixel-art as art-style is possible, but not scaled perfectly when playing in fullscreen at the moment
RGame is the product of both my lazyness and me looking for something that did not exist. Coming from Ruby on Rails, I wanted to write games while not learning a new language. There are Ruby game engines, but none offered me what I wanted: The option to write modern, standard Ruby and proper specs for the game logic. The closest I found was Gosu, which is just a media layer. This project initially started as a "high level engine on top of Gosu", but eventually the mismatch of my design philosophy with Gosu drove me into rewriting this layer myself.
RGame puts a lot of emphazis on testing and being testable: It separates the layers that talk to SDL2/OpenGL from the high level engine concepts, so the whole game logic is testable headless.
It also tries to marry the beauty of Ruby with the hard performance requirements of games: Math-heavy use-cases are backed by C code instead of Ruby classes, and the engine codes allocates nothing on a per-frame basis to trigger as little garbadge collection runs as possible.
Should you use it, though? If you're looking for something mature, free, and more battle-tested: Take a look at Godot instead. If you're looking for something mature and battle-tested in Ruby land, take a look at dragonruby instead (it's not free, but it's probably worth the price).
If you're just starting with game development and planning on making the next big indie hit - might as well pick this one as the engine for the game you never finish!
In all seriousness, though: This is a hobby project of mine, and while it might develop into something actually useful, at the time of writing it's a playground. If you search for something I searched and found nothing - try RGame! If you want to learn how to write games or just need a small prototype, and you really like Ruby - try RGame! If all you know is Rails, but you want to make a game that doesn't run in a browser and don't care about shipping it - try RGame!
I would be really happy if someone else actually uses it, but at this point I can't really recommend it for anything else than small projects and/or learning the ropes of game development.
Getting started
Installing the gem puts an rgame command on your PATH, which you can use to setup a project:
gem install rgame
rgame new tictactoe
cd tictactoe
bundle install
bundle exec rspec # the game logic, headless — no window needed
ruby main.rb # the game itself
It's just a small skeleton — a game class, a root node, a spec and the usual configuration — but it is laid out the way the engine wants to be used: one file loads SDL, everything else stays graphics-free and therefore testable with no display. The rgame command explains the layout and the reasoning. This is also sets you up with a Rubocop configuration already geared towards RGame and game development in general.
On a common desktop that first line needs nothing else — no compiler, no SDL2, no header files. gem install rgame fetches a gem whose two C extensions are already built, with SDL2 linked into them:
- macOS 11 or later on Apple Silicon
- x86-64 Linux with glibc 2.29 or later
- 64-bit Windows, on a RubyInstaller Ruby
All three need Ruby 4.0. Anywhere else — an Intel Mac, a Raspberry Pi, Ruby 4.1 — gem install falls back to the gem that ships the C and compiles it on your machine. That one needs a compiler and SDL2.
Hello world
The simplest "game" you can write, all in one file:
require 'rgame/game'
module HelloWorld
Engine = RGame::Engine
Util = RGame::Util
UI = Engine::UI
Components = Engine::Components
class Scene < Engine::Node2D
def _draw(renderer, _view)
renderer.text('Hello world!', 250, 200)
end
end
end
game = RGame::Game.new(
root: HelloWorld::Scene.new,
caption: 'Hello world!'
)
game.startThe game keeps its classes in a module of its own. Engine, Util, UI and
Components inside it stand for rgame's namespaces of the same names: the two
layers a game is written against, and the engine's menus and components.
A game's own module says why
it takes this shape. You can learn more about how it works in the
documentation.
Examples
examples/ holds one small program per concept, each a single file you can run with ruby examples/<name>/main.rb. The examples page describes them in more detail. They're also full of code comments so you have an easier time learning the concepts.
| Example | Shows |
|---|---|
| walk | A player-controlled sprite — a node, three components, input as actions |
| sprite | One frame drawn at a node, with no animation behind it |
| velocity | Movement with nobody driving: a velocity, integrated |
| scroll_map | A Tiled map larger than the window, scrolled by a camera |
| collision | Two shapes touching, and who gets told about it |
| collectables | Coins taken by touch, and a chest opened with a press |
| collision_tiles | Walking into a wall of solid tiles, and sliding along it |
| jump_topdown | A hop in a top-down view, where the sprite rises and the feet stay on the ground |
| pits | Falling into a gap, hopping across one, and coming back flashing |
| checkpoints | Flags on a map that move where a fall brings the hero back |
| moving_platforms | A raft shuttling across a chasm, boarded with a timed hop |
| push_pull | Crates pushed by walking into them, and pulled with a held button |
| block_puzzle | Blocks shoved a cell at a time onto their squares |
| signals | A node announcing something happened, to nobody in particular |
| timer | Things that happen on a clock, with nothing pressed |
| pooling | Spawning a lot of things without allocating them |
| effects | A fade, a flash, sparkles and a lightning bolt that glow over what is behind them |
| game_menu | A menu over a world that keeps running |
| menu_navigation | Several screens, a fade between them, and settings that persist |
| radial_menu | A wheel of icons, chosen by the direction of the stick |
| quick_wheel | A wheel held open by a button and chosen by letting go |
| skill_bar | A row of tools, stepped through or fired by hotkeys |
| inventory | A bag in a grid that scrolls, focus that crosses to the verbs beside it, and a second page behind a tab |
| equipment | A character dressed from slots and a grid of clothes, and a bag that says what is worn |
| sound | A sound effect fired by a button, and the seam it travels |
| music | A looping track that fades in and out, pauses, and has its own volume |
| split_screen | Two players in one world, drawn once per viewport |
| input_glyphs | Prompts that match the device in your hands |
| input_holds | One button tapped and held, and a chord that silences it |
| fullscreen | Fullscreen, switched at any time, and the scale modes |
| smooth_art | Anti-aliased art scaled, turned and moved, and the filter that keeps it smooth |
| save_load | Writing game state to disk and putting it back |
| save_load_ids | A save that has to name things, and why a reference forces ids |
| localization | The same screen in two languages, switched and remembered |
| intro | A story broken into lines and pages that differ per language, typed out a character at a time |
| doors | A gate between two maps and a pair of warp pads, placed in Tiled |
| pathfinding | Setting a target for an actor and let if find its way there |
| dialogue | A branching conversation in a dialogue box, and nothing else |
| quests_and_dialogue | A village where talking moves a quest on, and one save keeps both |
| cutscene | A scene everybody watches, and a held skip that ends it the same way |
Building from source
This section is for two readers: anyone on a platform the binary gems miss, and anyone working on RGame itself. Installing on one of the three platforms above needs none of it.
Compiling the C engine needs a compiler and the system libraries it links against. A source install of the gem needs the compiler, pkg-config, SDL2 and OpenGL from the list below. The rest is for running rgame's own suites.
C engine
- A C compiler —
gccorclang makepkg-config- SDL2 development headers (
sdl2pkg-config package) - OpenGL development headers/libs (provided by Mesa on Linux)
-
Check (
checkpkg-config package) — C unit test framework, only needed formake test
PNG decoding, text and audio need no system libraries: stb_image.h,
stb_vorbis.c and miniaudio.h are vendored in ext/rgame_core/vendor/, and
stb_truetype.h in ext/rgame_util/vendor/ (all public domain / MIT). The
default font ships in lib/rgame/fonts/ (SIL OFL 1.1). miniaudio finds ALSA or
PulseAudio at runtime, so there is nothing to install for sound either. The
README in each vendor/ directory covers the details.
tools/ holds development tools that are not part of the engine and are not
built by make — currently one, which generates the audio suite's .ogg
fixture and needs libvorbisenc to run.
Ruby side
-
Ruby 4.0.5, pinned in
.ruby-version. -
Ruby development headers. Version-manager builds (mise, rbenv, rvm, asdf) include them. On a distro-packaged Ruby, install
ruby-dev(Debian/Ubuntu). These are whatextconf.rbcompiles against. -
Bundler, then
bundle installfor the dev/test gems (RSpec, RuboCop).
Nothing else. The engine's one runtime gem is rexml, which Bundler installs with it, and the Gemfile holds only development gems.
All three platforms below are built and tested on every push by CI - with that people have usually at home, so Apple Silicon and not Apple Intel, etc.
Debian / Ubuntu
sudo apt install build-essential pkg-config libsdl2-dev libgl1-mesa-dev check
rake spec:core opens real windows and starts its own Xvfb, which needs a few
more packages — the display itself, the xwininfo it polls to know the display
is up, a software rasteriser (Xvfb has no GPU, and SDL_GL_CreateContext fails
without one) and the XTEST runtime for synthetic keystrokes:
sudo apt install xvfb x11-utils libgl1-mesa-dri libxtst6
macOS (Homebrew)
brew install sdl2 pkg-config check
OpenGL ships with the Xcode Command Line Tools (xcode-select --install) — the
full Xcode is not needed, and neither is anything else: rake spec:core uses
the native window server, so there is no Xvfb equivalent to set up. Note
Homebrew's sdl2 formula now installs sdl2-compat, which is SDL2's API
implemented on top of SDL3; the engine works through it unchanged.
Windows
Use a RubyInstaller-built Ruby (mise, vfox and rbenv-style managers all
fetch those), which is what supplies ridk. The combined DevKit installer is
not required — a standalone MSYS2 that ridk can find works just as well, and
C:\msys64 is one of the places it looks:
winget install --id MSYS2.MSYS2 -e
ridk exec pacman -Syu --noconfirm # core update; may need a second pass
Then the libraries. Note make is an msys package with no prefix while
everything else is ucrt64-prefixed — that split is the whole Windows story:
ridk exec pacman -S --needed \
mingw-w64-ucrt-x86_64-SDL2 \
mingw-w64-ucrt-x86_64-check \
mingw-w64-ucrt-x86_64-pkgconf \
mingw-w64-ucrt-x86_64-gcc \
make
MSYS2 is several environments in one install, and picking the wrong one fails
in a way that looks like missing packages. UCRT64 builds native Windows
binaries, which is what RubyInstaller's Ruby can load; the plain msys
environment builds against a Cygwin-like runtime, which it cannot. Work from
the "MSYS2 UCRT64" shell or run ridk enable ucrt64 first, and verify before
trusting anything:
which gcc # must be /ucrt64/bin/gcc, NOT /usr/bin/gcc
pkg-config --cflags sdl2 # must print a ucrt64 include path
Git Bash is not the UCRT64 shell, even with /ucrt64/bin first on its PATH.
It carries its own copy of the MSYS runtime, and MSYS2's make started from it
loses the temporary directory on the way to gcc. Every compile then fails with
an error that points nowhere near the cause:
Cannot create temporary file in C:\WINDOWS\: Permission denied
From Git Bash, run the build through ridk.cmd exec make ext instead, or open
the UCRT64 shell. Git Bash finds ridk only by its full name, ridk.cmd.
One more Windows fact worth knowing up front: Check has no usable fork
there, so the first segfault kills the whole test binary and the output stops
mid-suite. Use a debugger rather than reading the log — CK_FORK=no gdb --args ./build/test_rgame, and CK_RUN_SUITE=<name> to run one suite.
Build & run
make # builds build/rgame (standalone C binary)
make run # build and run it
make test # build and run the Check unit tests (C, pure logic)
make ext # build both Ruby extensions
make ext-core # build only ext/rgame_core -> lib/rgame/core_ext.so
make ext-util # build only ext/rgame_util -> lib/rgame/util_ext.so
make clean # remove build artifacts, including both extensions'
make run opens a window with one of each drawing primitive in it — a rotating
square, a clipped rectangle, a circle, a thick line, a baked strip replayed
every frame, and a line of accented text. Esc or closing the window quits.
ruby ext/rgame_core/example.rb is the same scene driven from Ruby, and takes
an optional sound file — ruby ext/rgame_core/example.rb theme.ogg binds Space
to play it as a sample and Return to start and stop it as looping music. That is
the only place a real sound device is driven; everything automated runs against
a null or offline one.
The Ruby specs:
bundle install
make ext # both extensions; the suites need the compiled .so files
rake spec # headless specs: RGame::Util, the engine layer, packaging
rake spec:core # RGame::Core specs; opens real windows, boots its own Xvfb
rake # everything: make test, rake spec, rake spec:core
bundle exec rubocop # lint; configured in .rubocop.yml, which also loads the
# project's own cops from lib/rgame/rubocop/.
The two Ruby suites are two directories and two processes on purpose. spec/
must never load SDL — the engine layer's whole value is that it can be
specified with no window — and RSpec loads one root into one process, so a
single require "rgame/core" anywhere would define RGame::Core for every
other example in the run. Separate runners cannot be forgotten the way an
exclude rule can. rake spec needs make ext-util; rake spec:core needs
make ext-core.
Each make ext-* target compiles its extension and copies the resulting .so
into lib/rgame/, which is where require "rgame/util_ext" and require "rgame/core_ext" look for it. That mirrors how rake-compiler installs a
compiled extension into lib/<gem>/. Without that step the specs can't even
load, since RGame::Util::Tensor now lives in C.
rake spec needs only ext-util — it never touches RGame::Core, which is
what keeps it runnable with no display and no SDL. To drive the engine from
Ruby by hand (opens a real window):
make ext-core
ruby ext/rgame_core/example.rb
Packaging
Both extensions and the Ruby layer ship as one gem, built from rgame.gemspec and published at rubygems.org/gems/rgame:
gem install rgame # from RubyGems; compiles nothing on a covered platform
Or from a checkout, which builds the source gem locally:
rake build # package into pkg/rgame-<version>.gem
gem install pkg/rgame-*.gem # compiles both extensions on this machine
RubyGems holds two kinds of gem for each version. The source gem carries the
C and compiles on install. Three platform gems carry core_ext and util_ext
already built, with SDL2 linked statically into core_ext; they ship no .c,
no extconf.rb and declare no extensions, so they compile nothing. gem install picks by platform and Ruby version on its own.
CI builds and publishes all four. tools/platform_gem.rake builds a platform
gem, tools/check_platform_gem.rb checks it against nine rules before it may be
published, and the smoke job installs it on a runner with no SDL2 and plays
the examples out of it.
Installing the source gem runs each extconf.rb and installs the resulting
.so into the gem's own lib/rgame/, which is the same layout make ext
produces in a checkout — so require "rgame" and require "rgame/core" behave
identically whichever gem you got, including the guarantee that the first of
those loads no graphics libraries. Its system dependencies are the ones the C
engine needs (SDL2, OpenGL, pkg-config, a compiler); extconf.rb aborts with
the package to install if one is missing, rather than failing later at the link
step.
What ships is a glob over lib/, ext/ and docs/api/, not a hand-written
list: a new C source or a runtime asset dropped into either tree is packaged
without being registered anywhere. spec/packaging_spec.rb holds that up from
the other side — it asserts every source, header and data file is in the gem and
that no build artifact, spec directory or plan is, so the parts of this that
would otherwise be a checklist fail the suite instead.
The version is RGame::VERSION in lib/rgame/version.rb.
Project structure
The C lives under ext/rgame_core/ rather than a top-level src/, becausegem install runs each extconf.rb and an extension can only build sources inside its own directory — so one copy of the code serves both the standalone binary and the gem. The Ruby half is split the same way it is namespaced:
lib/rgame/util/, lib/rgame/core/ and lib/rgame/engine/.
A file-by-file map of the whole repository is in docs/project_structure.md.
Roadmap
There is no real roadmap for this project, but there are a few features on my mind which I want to include in the next release(s):
- support more of Tiled's features. Orthogonal maps will stay the only one supported for a while, though. - IN PROGRESS
- allow better input-to-action-mapping for complexer inputs like long presses or button combinations - DONE
- a better scene manager that means less boilerplate in each game with scene transitions. With it an example for teleports/room-transitions - DONE
- allow toggeling the debug layer and connect it better to systems like collision (show collision boxes on toggle) - DONE
- a dialogue system and better text representation - DONE
- an examples (and if needed, new components) for pushing and pulling objects in the game world - DONE
- an example (and if needed, new components) for inventory and equipment screens - DONE
- an example (and if needed, new components) for collectable and interactable nodes like coins and treasure chests - DONE
- audio transitions - fades, pauses, etc. - DONE
- visual effects like lightning, sparkles, fade-to-black - DONE
- better engine support for cutscenes - DONE
AI clause
This project is not vibe-coded, but AI tools were used heavily while writing code. If you dislike AI generated code, this project is not for you.