R-cade Game Engine
1 Homepage
2 Core
run
quit
goto
wait
sync
frame
frametime
gametime
width
height
3 Input
btn-start
btn-select
btn-quit
btn-z
btn-x
btn-up
btn-down
btn-right
btn-left
btn-mouse
btn-any
mouse-x
mouse-y
hide-mouse
show-mouse
4 Actions
action
5 Timers
timer
6 Drawing
cls
color
set-color!
draw
draw-ex
text
line
rect
circle
7 Fonts
make-font
font
font-sprite
font-advance
font-height
basic-font
tall-font
wide-font
8 Voices
voice
voice?
basic-voice
synth
envelope
square-wave
triangle-wave
sawtooth-wave
noise-wave
basic-envelope
fade-in-envelope
fade-out-envelope
z-envelope
s-envelope
peak-envelope
trough-envelope
adsr-envelope
9 Sound
sound
tone
sweep
play-sound
stop-sound
sound-volume
10 Music
music
basic-note
music?
play-music
stop-music
pause-music
music-volume
8.12

R-cade Game Engine🔗ℹ

Jeffrey Massung <massung@gmail.com>

 (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"
Creates a new game window, video memory, and enters the main game loop.

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?

Closes the window, which will terminate the main game loop.

procedure

(goto game-loop)  void?

  game-loop : procedure?
Changes the game loop function to game-loop. This doesn’t take affect until the next frame. This function is used to change between various game states. For example, you might have a start-screen game state, a main-game state, and a pause-game state that are switched between.

procedure

(wait [until])  void?

  until : procedure? = btn-any
Hard stops the game loop and waits until either the window is closed or the until function returns true. While waiting, events are still processed. This is commonly used to wait for the user to press a button.

procedure

(sync)  void?

Called once per frame automatically by the main game loop. You shouldn’t need to call this yourself unless you are creating your own game loop. It processes all events, renders video memory, and ensures the framerate is locked.

procedure

(frame)  exact-nonnegative-integer?

Returns the current frame: 1, 2, 3, ...

procedure

(frametime)  real?

Returns the delta time (in seconds) since the last frame. It’s best to use this when applying any kind of velocity to a game object instead of assuming the framerate will be constant.

procedure

(gametime)  real?

Returns the total time (in seconds) since the game started.

procedure

(width)  exact-nonnegative-integer?

Returns the width of VRAM in pixels. This is the same value that was passed to the run function.

procedure

(height)  exact-nonnegative-integer?

Returns the height of VRAM in pixels. This is the same value that was passed to the run function.

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
Returns the state of the ENTER key.

procedure

(btn-select [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the SPACEBAR key.

procedure

(btn-quit [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the ESCAPE key.

procedure

(btn-z [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the Z key.

procedure

(btn-x [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the X key.

procedure

(btn-up [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the UP arrow key.

procedure

(btn-down [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the DOWN arrow key.

procedure

(btn-right [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the RIGHT arrow key.

procedure

(btn-left [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the LEFT arrow key.

procedure

(btn-mouse [hold rate])  boolean?

  hold : boolean? = #f
  rate : exact-nonnegative-integer? = #f
Returns the state of the LEFT mouse button.

procedure

(btn-any)  boolean?

Returns the equivelant of:
(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?

Returns the X pixel (in VRAM) that the mouse is over. 0 is the left edge.

procedure

(mouse-y)  exact-nonnegative-integer?

Returns the Y pixel (in VRAM) that the mouse is over. 0 is the top edge.

procedure

(hide-mouse)  void?

Hides the mouse cursor while over the window.

procedure

(show-mouse)  void?

Shows the mouse cursor while over the window.

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
Returns a function with arity 0 that returns either #t or #f, indicating whether or not the action is should be considered "pressed".

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
Returns a function that will count down the time until it reaches 0.0, at which point the function returned will return #t indicating the time has elapsed.

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
Clears video memory with the specified color. Remember that video memory isn’t magically wiped each frame.

procedure

(color c)  void?

  c : exact-nonnegative-integer?
Changes the active color to c (0-15). The default color palette is the same as the PICO-8:

procedure

(set-color! c r g b)  void?

  c : exact-nonnegative-integer?
  r : byte?
  g : byte?
  b : byte?
Changes the color in the palette at index c to the RGB byte values specified by r, g, and b.

procedure

(draw x y sprite)  void?

  x : real?
  y : real?
  sprite : (listof byte?)
Uses the current color to render a 1-bit sprite composed of bytes to VRAM at (x,y). For example:

(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?)
This is exactly the same as draw, except that the sprite is considered to be 16-bits wide. The most significant byte of each scanline is rendered first, followed by the least significant byte.

procedure

(text x y s)  void?

  x : real?
  y : real?
  s : any
Draw the value s at (x,y) using the current font. The default font is a fixed-width, ASCII font with character range [#x20,#x7f]. Each character is 3x6 pixels in size.

procedure

(line x1 y1 x2 y2)  void?

  x1 : real?
  y1 : real?
  x2 : real?
  y2 : real?
Draw a line from (x1,y1) to (x2,y2) using the current color.

procedure

(rect x y w h #:fill boolean?)  void?

  x : real?
  y : real?
  w : real?
  h : real?
  boolean? : #f
Draw a rectangle starting at (x,y) with a width w and height h using the current color. If fill is #t then it will be a solid rectangle, otherwise just the outline.

procedure

(circle x y r #:fill boolean?)  void?

  x : real?
  y : real?
  r : real?
  boolean? : #f
Draw a circle with its center at (x,y) and radius r using the current color. If fill is #t then it will be solid, otherwise just the outline.

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
Define a new font that can be set with the font function and rendered with text.

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?
Sets the current font to draw characters with when using the text function.

procedure

(font-sprite char)  (or/c (listof byte?) #f)

  char : char?
Returns the sprite in the current font for char or #f if no sprite exists for it.

procedure

(font-advance)  exact-nonnegative-integer?

Returns the cursor x advance value in pixels for the current font. This is always the width of the font plus 1 pixel.

procedure

(font-height)  exact-nonnegative-integer?

Returns the line height in pixels for the current font. The line height is always the number of scanlines of the first character sprite in the font plus 1 pixel.

value

basic-font : font?

The default font of 3x6 character sprites.

value

tall-font : font?

A font of 5x8 character sprites.

value

wide-font : font?

A font of 7x8 character sprites.

8 Voices🔗ℹ

All sounds (and music) are played using voices. A voice is both an "instrument" (wave function) and an "envelope" (volume function).

procedure

(voice instrument envelope)  voice?

  instrument : procedure?
  envelope : procedure?
The instrument function is like sin or cos. It is given a value in the range of [0.0, 2pi] and returns a value in the range of [-1.0, 1.0]. Aside from any built-in Racket functions (e.g. sin and cos) there are 4 other pre-defined wave functions you can use:

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
Returns #t if x is a valid voice object.

value

basic-voice : voice? = (voice sin basic-envelope)

The default voice used to create sounds.

syntax

(synth (wave-function q) ...)

 
wave-function = procedure?
     
q = real?
Creates a lambda function that is the combination of multiple wave-functions at frequency harmonics, each muliplied by q.

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?
Returns a function that can be used as the #:envelope paramater to the sound function. It builds a simple, evenly spaced, linearly interpolated plot of amplitude envelopes. For example, the z-envelope is defined as:

(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?

A wave function that may be passed as an instrument.

value

triangle-wave : procedure?

A wave function that may be passed as an instrument.

value

sawtooth-wave : procedure?

A wave function that may be passed as an instrument.

value

noise-wave : procedure?

A wave function that may be passed as an instrument.

value

basic-envelope : procedure? = (const 1)

An envelope function that may be passed as an envelope.

value

fade-in-envelope : procedure? = (envelope 0 1)

An envelope function that may be passed as an envelope.

value

fade-out-envelope : procedure? = (envelope 1 0)

An envelope function that may be passed as an envelope.

value

z-envelope : procedure? = (envelope 1 1 0 0)

An envelope function that may be passed as an envelope.

value

s-envelope : procedure? = (envelope 0 0 1 1)

An envelope function that may be passed as an envelope.

value

peak-envelope : procedure? = (envelope 0 1 0)

An envelope function that may be passed as an envelope.

value

trough-envelope : procedure? = (envelope 1 0 1)

An envelope function that may be passed as an envelope.

value

adsr-envelope : procedure? = (envelope 0 1 0.7 0.7 0)

An envelope function that may be passed as an envelope. This is the default enevelope used for musical notes.

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
All sounds are made using the sound function. The curve argument is a function that is given a single value in the range of [0.0, 1.0] and should return a frequency to play at that time; 0.0 is the beginning of the waveform and 1.0 is the end. The seconds parameter defines the length of the waveform.

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
Helper function that returns a sound that plays a constant frequency.

procedure

(sweep start-freq end-freq seconds [voice])  sound?

  start-freq : real?
  end-freq : real?
  seconds : real?
  voice : voice? = basic-voice
Helper function that returns a sound using a curve function that linearly interpolates from start-freq to end-freq.

procedure

(play-sound sound)  void?

  sound : sound?
Queues the sound buffer to be played on one of 8 sound channels. If no sound channels are available then the sound will not be played.

procedure

(stop-sound)  void?

Stops all sounds currently playing and clears the sound queue.

procedure

(sound-volume vol)  void?

  vol : real?
Sets the volume of all sounds played. 0.0 is muted and 100.0 is full volume.

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
Parses the notes string and builds a waveform for each note. Notes are in the format <key>[<octave>][<hold>]. For example:

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)

The default voice used to create music.

procedure

(music? x)  boolean?

  x : any
Returns #t if x is a PCM music object.

procedure

(play-music riff [#:loop loop])  void?

  riff : music?
  loop : boolean? = #t
Stops any music currently playing and starts playing riff. The loop parameter will determine whether the riff stops or repeats when finished.

procedure

(stop-music)  void?

Stops any music currently playing.

procedure

(pause-music [pause])  void?

  pause : boolean? = #t
If pause is #t then the currently playing music is paused, otherwise it is resumed. If the music was not already pausedy and is told to resume, it will instead restart from the beginning.

procedure

(music-volume vol)  void?

  vol : real?
Sets the volume of any music played. 0.0 is muted and 100.0 is full volume.