R-cade Game Engine
(require r-cade) | package: r-cade |
R-cade is a simple, retro game engine for Racket.
1 Homepage
All the most recent updates, blog posts, etc. can be found at http://r-cade.io.
2 Core
procedure
(run game-loop width height [ #:init init #:scale scale-factor #:fps frame-rate #:shader enable-shader #:title window-title]) → void? game-loop : procedure? width : exact-nonnegative-integer? height : exact-nonnegative-integer? init : procedure? = #f scale-factor : exact-nonnegative-integer? = #f frame-rate : exact-nonnegative-integer? = 60 enable-shader : boolean? = #t window-title : string? = "R-cade"
The game-loop parameter is a function you provide, which will be called once per frame and should take no arguments.
The width and height parameters define the size of video memory (not the size of the window!).
The init procedure - if provided - is called before the game-loop starts. If you have initialization or setup code that requires R-cade state be initialized, this is where you can safely do it.
The scale-factor parameter will determine the initial size of the window. The default will let auto pick a scale factor that is appropriate given the size of the display.
The frame-rate is the number of times per second the game-loop function will be called the window will update with what’s stored in VRAM.
The enable-shader controls whether or not the contents of VRAM are rendered using a fullscreen shader effect. If this is set to #f the effect will be disabled.
The window-title parameter is the title given to the window created.
procedure
(quit) → void?
procedure
(goto game-loop) → void?
game-loop : procedure?
procedure
(sync) → void?
procedure
(frame) → exact-nonnegative-integer?
procedure
(frametime) → real?
procedure
(gametime) → real?
procedure
(width) → exact-nonnegative-integer?
procedure
(height) → exact-nonnegative-integer?
3 Input
All btn-* functions return either #t or #f to indicate if the button should currently be considered "pressed".
The hold parameter should be set to #f if the button predicate should only return #t once when the button is initially pressed, but #f if held down.
If hold is #t, then the rate parameter can also be optionally set to limit how often a held button can return true. The rate is in presses per second.
For example, if you want to know if the Z button was just pressed this frame by the player, you would check with:
(btn-z)
If you want to know if the Z button is pressed, regardless of how long it has been held down for:
(btn-z #t)
But, let’s say you’re using the Z button to shoot a weapon, but only want the user to be able to fire at a rate of 3 times per second, you could check with:
(btn-z #t 3)
procedure
(btn-start [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-select [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-quit [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-z [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-x [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-up [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-down [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-right [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-left [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-mouse [hold rate]) → boolean?
hold : boolean? = #f rate : exact-nonnegative-integer? = #f
procedure
(btn-any) → boolean?
(or (btn-start) (btn-select) (btn-quit) (btn-z) (btn-x))
This function isn’t really used much outside of wait.
procedure
(mouse-x) → exact-nonnegative-integer?
procedure
(mouse-y) → exact-nonnegative-integer?
procedure
(hide-mouse) → void?
procedure
(show-mouse) → void?
4 Actions
Sometimes you want to be able to bind buttons to specific, named actions so your code is easier to read (and modify if you want to change your button mapping). To do this, use the action function.
procedure
(action btn [hold rate]) → procedure?
btn : procedure? hold : boolean? = #f rate : exact-nonnegative-integer? = 0
The btn parameter should be one of the btn-* functions (e.g. btn-z).
The hold and rate parameters are the same as what would be passed to the btn function.
5 Timers
It’s possible to create countdown timer functions that expire after some time has elapsed.
procedure
(timer time [#:loop loop]) → procedure?
time : real? loop : boolean? = #f
If loop is #t, then when the timer expires it will automatically reset back to time and begin counting down again.
Example use:
(define boss-attack-timer (timer 5 #:loop #t)) (define (game-loop) (when (boss-attack-timer) (do-boss-attack)))
Note: the timer will only advance when called. This allows you to "pause" a timer by simply not calling it (e.g. while the game is paused). However, this also means calling the function multiple times in the same frame will advance it multiple times.
6 Drawing
procedure
(cls [c]) → void?
c : exact-nonnegative-integer? = 0
procedure
(color c) → void?
c : exact-nonnegative-integer?
procedure
(set-color! c r g b) → void?
c : exact-nonnegative-integer? r : byte? g : byte? b : byte?
procedure
(draw x y sprite) → void?
x : real? y : real? sprite : (listof byte?)
(draw 10 12 '(#b01000000 #b11100000 #b01000000))
The above would draw a 3x3 sprite that looks like a + sign to the pixels at (10,12) -> (12,14). Any bit set in the sprite pattern will change the pixel color in VRAM to the current color. Any cleared bits are skipped.
Remember! The most significant bit of each byte is drawn at x. This is important, because if you’d like to draw a single pixel at (x,y), you need to draw #b10000000 and not #b00000001!
procedure
(draw-ex x y sprite) → void?
x : real? y : real? sprite : (listof integer?)
procedure
(text x y s) → void?
x : real? y : real? s : any
procedure
(line x1 y1 x2 y2) → void?
x1 : real? y1 : real? x2 : real? y2 : real?
procedure
(rect x y w h #:fill boolean?) → void?
x : real? y : real? w : real? h : real? boolean? : #f
procedure
(circle x y r #:fill boolean?) → void?
x : real? y : real? r : real? boolean? : #f
7 Fonts
There are three built-in fonts and it’s possible to create your own and use them as well.
procedure
(make-font sprites [ #:advance width #:base base]) → font? sprites : (vectorof (listof byte?)) width : exact-nonnegative-integer? = 8 base : exact-nonnegative-integer? = 33
The width parameter is the pixel width of each character sprite. The base parameter is the ordinal value of the first ASCII character in the font (typically this is 33, the #\! character). Any character drawn that isn’t in the font is drawn as a space.
procedure
(font font) → void?
font : font?
procedure
(font-sprite char) → (or/c (listof byte?) #f)
char : char?
procedure
(font-advance) → exact-nonnegative-integer?
procedure
(font-height) → exact-nonnegative-integer?
value
basic-font : font?
value
tall-font : font?
value
wide-font : font?
8 Voices
All sounds (and music) are played using voices. A voice is both an "instrument" (wave function) and an "envelope" (volume function).
Additionally, you can create your own wave functions (instruments) with the synth macro.
The envelope function is used to set the volume of a sound over the duration of it. The envelope function is given a single value in the range [0.0, 1.0] indicating where in the sound it is. It should return a value in the range [0.0, 1.0], where 0.0 indicates a null amplitude and 1.0 indicates full amplitude. Some pre-defined envelopes include:
There is also an envelope function that helps with the creation of your own envelopes.
procedure
(voice? x) → boolean?
x : any
value
basic-voice : voice? = (voice sin basic-envelope)
syntax
(synth (wave-function q) ...)
wave-function = procedure? q = real?
Each wave-function can be any function valid as the instrument of a voice. Most common would be sin and cos. For each wave-function there should also be a corresponding q argument that is how much that wave function will be multiplied by.
The wave functions are passed the frequency harmonic of the sound they are used for in the order they are provided to the synth macro. For example, if the sound is playing a solid tone of 440 Hz, then the first wave function will be at 440 Hz, the second wave function at 880 Hz, the third at 1320 Hz, etc.
For example:
(synth (sin 1.0) (cos 0.3) (sin 0.1) (cos -0.3))
The above would be equivelant to the following wave function:
(λ (x) (+ (* (sin x) 1.0) (* (cos (* x 2)) 0.3) (* (sin (* x 3)) 0.1) (* (cos (* x 4)) -0.3)))
The function returned takes the x argument, applies it to each of the harmonics and returns the sum of them.
A simple online tool for playing with harmonic sound functions can be found at https://meettechniek.info/additional/additive-synthesis.html.
TIP: Instead of just the generic sine and cosine functions, trying sythenizing with some other wave functions like triangle-wave and noise-wave!
procedure
(envelope y ...) → procedure?
y : real?
(define z-envelope (envelope 1 1 0 0))
This means that in the time range of [0.0, 0.33] the sound will play at full amplitude. From [0.33, 0.66] the envelope will decay the amplitude linearly from 1.0 down to 0.0. Finally, from [0.66, 1.0] the amplitude of the sound will be forced to 0.0.
value
square-wave : procedure?
value
triangle-wave : procedure?
value
sawtooth-wave : procedure?
value
noise-wave : procedure?
value
basic-envelope : procedure? = (const 1)
value
fade-in-envelope : procedure? = (envelope 0 1)
value
fade-out-envelope : procedure? = (envelope 1 0)
value
z-envelope : procedure? = (envelope 1 1 0 0)
value
s-envelope : procedure? = (envelope 0 0 1 1)
value
peak-envelope : procedure? = (envelope 0 1 0)
value
trough-envelope : procedure? = (envelope 1 0 1)
value
adsr-envelope : procedure? = (envelope 0 1 0.7 0.7 0)
9 Sound
All audio is played by composing 16-bit PCM WAV data using a voice. Audio data that can be played is created using the sound and music functions.
procedure
(sound curve seconds [voice]) → sound?
curve : procedure? seconds : real? voice : voice? = basic-voice
The voice is used to define the wave function and volume envelope used when generating the PCM data for this sound. It is optional, and the default voice is just a simple sin wave and the basic-envelope.
procedure
(tone freq seconds [voice]) → sound?
freq : real? seconds : real? voice : voice? = basic-voice
procedure
(sweep start-freq end-freq seconds [voice]) → sound?
start-freq : real? end-freq : real? seconds : real? voice : voice? = basic-voice
procedure
(play-sound sound) → void?
sound : sound?
procedure
(stop-sound) → void?
procedure
(sound-volume vol) → void?
vol : real?
10 Music
Music is created by parsing notes and creating an individual waveform for each note, then combining them together into a single waveform to be played on a dedicated music channel. Only one "tune" can be playing at a time.
procedure
(music notes [ #:tempo beats-per-minute] #:voice voice?) → music? notes : string? beats-per-minute : exact-nonnegative-integer? = 160 voice? : basic-note
"C#3--" is a C-sharp in 3rd octave and held for a total of 3 quarter-notes time;
"Bb" is a B-flat held for a single quarter-note and uses the octave of whatever note preceeded it;
The default octave is 4, but once an octave is specified for a note then that becomes the new default octave for subsequent notes.
How long each note is held for (in seconds) is determined by the #:tempo (beats per minute) parameter. A single beat is assumed to be a single quarter-note. So, with a little math, a "C#–" at a rate of 160 BPM would play for 1.125 seconds (3 beats * 60 s/m ÷ 160 bpm). It is not possible to specify 1/8th and 1/16th notes. In order to achieve them, increase the #:tempo appropriately.
By default, the voice used is the basic-note, which uses the adsr-envelope function (ADSR stands for attack, decay, sustain, release). Unlike sounds, which use the envelope function across the entire sound, when generating music the envelope function is applied to each note. This is important to keep in mind if you decide to override the envelope with your own, as it’s how each note can be distinguished from the next.
value
basic-note : voice? = (voice sin adsr-envelope)
procedure
(play-music riff [#:loop loop]) → void?
riff : music? loop : boolean? = #t
procedure
(stop-music) → void?
procedure
(pause-music [pause]) → void?
pause : boolean? = #t
procedure
(music-volume vol) → void?
vol : real?