R-cade Game Engine
1 Homepage
2 Core
run
quit
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 Action Bindings
define-action
5 Drawing
cls
color
set-color!
draw
text
line
rect
circle
6 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
7 Sound
sound
tone
sweep
sound?
play-sound
stop-sound
sound-volume
8 Music
music
music?
play-music
stop-music
pause-music
music-volume
7.6

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 crt-effect    
  #: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
  crt-effect : 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 crt-effect controls whether or not the contents of VRAM are rendered using a fullscreen shader effect that mimics a CRT. 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

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

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 #f if their respective button is not pressed, otherwise they return the count of how many frames the button has been pressed for. For example, a return value of 1 would indicate the button was just pressed. This allows for easy testing of not pressed, pressed, just pressed, or against a desired repeat rate.

procedure

(btn-start)  (or #f exact-nonnegative-integer?)

Returns the state of the ENTER key.

procedure

(btn-select)  (or #f exact-nonnegative-integer?)

Returns the state of the SPACEBAR key.

procedure

(btn-quit)  (or #f exact-nonnegative-integer?)

Returns the state of the ESCAPE key.

procedure

(btn-z)  (or #f exact-nonnegative-integer?)

Returns the state of the Z key.

procedure

(btn-x)  (or #f exact-nonnegative-integer?)

Returns the state of the X key.

procedure

(btn-up)  (or #f exact-nonnegative-integer?)

Returns the state of the UP arrow key.

procedure

(btn-down)  (or #f exact-nonnegative-integer?)

Returns the state of the DOWN arrow key.

procedure

(btn-right)  (or #f exact-nonnegative-integer?)

Returns the state of the RIGHT arrow key.

procedure

(btn-left)  (or #f exact-nonnegative-integer?)

Returns the state of the LEFT arrow key.

procedure

(btn-mouse)  (or #f exact-nonnegative-integer?)

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 Action Bindings

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 define-action macro.

syntax

(define-action name btn [rate #f])

 
name = symbol?
     
btn = procedure?
     
rate = (or #t exact-nonnegative-integer?)
Defines a new function name that returns #t if the mapped btn binding predicate function should be considered "pressed".

If rate is #f (the default), then the action is defined as (define name btn).

If rate is #t then the action will return #t if the btn returns 1, indicating it was just pressed this frame.

Otherwise, rate should be a non-negative integer indicating how many times per seconds the action function should return #t assuming the button is held down. This is useful for actions like shooting that shouldn’t happen every frame, but you also don’t want the user to have to keep pressing the input button.

(define-action move-left btn-left)
(define-action jump btn-z #t)
(define-action fire btn-x 5)

5 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

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

6 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.

7 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

(sound? x)  boolean?

  x : any
Returns #t if x is a PCM sound buffer.

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.

8 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    
  #:instrument wave-function])  music?
  notes : string?
  beats-per-minute : exact-nonnegative-integer? = 160
  wave-function : procedure? = sin
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.

All notes are played with an ADSR (attack, decay, sustain, release) envelope. This cannot be overridden.

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.