All SDK docs

libvpy

The game library most SDK games are written against: lifecycle, drawing, text, input, math, sound and levels, in integer C.

libvpy is the top layer of the SDK and the one most games should use. It is a small game library in plain C. It is integer-only on purpose, with no math.h, so it compiles to plain integer ARM code that runs the same everywhere.

It is also backend-neutral. libvpy is written against the PiTrex/host contract, so the same game source builds for:

  • the Vectrex Studio Debug Cart;
  • the UVMC2;
  • the desktop harness and the WASM simulator;
  • PiTrex, coming soon.

To use it, #include <vpy.h> and add $(VPY_C_SDK)/vpy.c to UVM2_SRCS.

The SDK has three layers, and they can be mixed freely:

LevelHeaderWhat it is
1 — libvpysdk/vpy-c/include/vpy.h (+ the modules on the next pages)A game library. Most games live here.
2 — PiTrex/host contractvectrex/vectrexInterface.hThe backend-neutral surface: about 20 declarations.
3 — the cartridge SDKsdk/uvm2-sdk/*.hThe cartridge runtime itself.

Units

SpaceRangeUsed by
VPy logicalx and y from −127 to +127, +y up, origin at the centrelibvpy
PiTrexVPy × 127, so about ±16 000v_directDraw32
devicethe SDK's own unituvm2_draw_move_abs, uvm2_zero_jump
subunit (q4)1/16 of a device unitthe *_q4 calls
brightness0..127; 0 means "do not draw"everywhere
angle0..127 is a full circlevpy_sin, vpy_cos, vpy_atan2
bus cycle667 ns; 30 000 of them make a 50 Hz frameuvm2_exec, uvm2_stats

Lifecycle

void vpy_init(void);          /* vectrexinit + v_init + v_setRefresh */
void vpy_frame_begin(void);   /* v_WaitRecal + refresh the input snapshot */
void vpy_run(void (*setup)(void), void (*loop)(void));   /* never returns */

vpy_run is the whole main loop. It initialises, calls setup() once, and then, every frame, calls vpy_frame_begin(), vpy_music_update(), vpy_sfx_update() and your loop(). There is no "present" call: everything drawn inside loop() is one frame.

#include <vpy.h>
 
static int s_x;
 
static void setup(void) { s_x = 0; }
 
static void loop(void)
{
    if (vpy_j1_x() >  32) s_x++;
    if (vpy_j1_x() < -32) s_x--;
    s_x = vpy_clamp(s_x, -100, 100);
 
    vpy_print_text(-60, 100, "HELLO");
    vpy_draw_rect(-120, -120, 240, 240, 40);
    vpy_draw_line(s_x - 10, 0, s_x + 10, 0, 110);
}
 
int main(void) { vpy_run(setup, loop); return 0; }

Drawing

void vpy_set_intensity(int b);
void vpy_move(int x, int y);                                  /* set the cursor */
void vpy_draw_line(int x0,int y0,int x1,int y1,int b);
void vpy_draw_circle(int cx,int cy,int r,int b);              /* 16 segments */
void vpy_draw_ellipse(int cx,int cy,int rx,int ry,int b);     /* 16 segments */
void vpy_draw_rect(int x,int y,int w,int h,int b);            /* x,y = lower-left */
void vpy_draw_filled_rect(int x,int y,int w,int h,int b);     /* hatched, 3 units apart */
void vpy_draw_polygon(const int *xy,int n,int b);             /* n vertices, xy[2n] */
void vpy_draw_line_dev(int32_t x0,int32_t y0,int32_t x1,int32_t y1,int b);  /* deflection units */

The stroke buffer

Every drawing call goes through a stroke buffer that holds 1024 strokes. The strokes reach v_directDraw32 only when the frame is flushed, which happens in vpy_run, vpy_frame_begin or vpy_wait_recal.

A game that calls v_WaitRecal() directly never flushes and draws nothing. If vpy_draw_stats()->frames stays at 0, this is why. Use vpy_wait_recal() instead.

void vpy_flush(void);
void vpy_wait_recal(void);                 /* flush, then v_WaitRecal */
void vpy_set_priority(int p);              /* VPY_PRI_LOW 0 / NORMAL 128 / KEEP 255 */
int  vpy_pending_strokes(void);
const vpy_draw_stats_t *vpy_draw_stats(void);   /* frames, strokes, peak, shed, dropped, clamped */
int  vpy_sin_q14(int a), vpy_cos_q14(int a);    /* 4096 steps per turn, 16384 = 1.0 */

When the buffer is full, a new stroke either evicts a lower-priority one or is dropped. This choice is made by the game library, not the SDK: the SDK always draws exactly what it is given. Both cases are counted. If shed or dropped is not zero, the frame on screen is not the frame the game built.

The effects, soft-body and rope modules draw at VPY_PRI_LOW. That makes them the first strokes shed when a frame is full, so the scenery survives.

Drawing cost

On the cartridges, a lit stroke costs about the same whatever its length, and a blanked jump between strokes costs extra. So chain your strokes (start each one where the last ended), prefer fewer and longer strokes, and don't set an intensity again if it hasn't changed. See Drawing.

Compiled assets

void vpy_draw_vector(const unsigned char *data,int x,int y);
void vpy_draw_vector_ex(const unsigned char *data,int x,int y,int mirror,int intensity);
void vpy_draw_anim(const unsigned char *anim,
                   const unsigned char *const *sprites,int x,int y,int mirror);
  • data is a position-independent path stream: a compiled .vec file.
  • anim is a frame descriptor plus a matching table of sprite pointers (.vanim).
  • Bézier segments are tessellated into lines.
  • intensity of 0 or less keeps each path's own intensity.

The asset compiler (vpy_cli compile-asset, part of Vectrex Studio) is not in the starter kit. The readers are, and the formats are documented in vpy.c's comments.

Text

void vpy_set_text_size(int s);
void vpy_print_text(int x,int y,const char *s);
void vpy_print_number(int x,int y,long n);

Glyphs advance by different amounts, so measure a string's width rather than computing it. examples/hello_uvmc2/tools/text_metrics.c does that.

Input

int  vpy_j1_x(void);            /* -127..127 */
int  vpy_j1_y(void);            /* -127..127, + = up */
int  vpy_j1_button(int n);      /* n = 1..4 -> 0/1 */
void vpy_update_buttons(void);  /* force a re-read mid-frame */

Input is sampled once per frame, between frames, and cached, so reading it costs nothing. By default the stick reads as digital (−127, 0 or +127), so ordinary if (x > 32) code behaves as it would on a real cartridge. See Input for analog mode.

Math

int vpy_abs, vpy_min, vpy_max, vpy_clamp;
int vpy_sin(int a), vpy_cos(int a);   /* a 0..127 = full circle -> -127..127 */
int vpy_sqrt(int v);                  /* integer Newton */
int vpy_atan2(int y,int x);           /* -> 0..127 */
int vpy_rand(void), vpy_rand_range(int lo,int hi);
void vpy_seed(unsigned s);

Sound

void vpy_beep(int on);
void vpy_tone(int period,int volume);            /* channel A; period 12-bit, vol 0..15 */
void vpy_play_music(const unsigned char *vmus);  /* a compiled PSG event stream */
void vpy_music_update(void);                     /* auto-called by vpy_run */
void vpy_stop_music(void);
void vpy_play_sfx(const unsigned char *vsfx);    /* one-shot, channel C */
void vpy_sfx_update(void);

Music (.vmus) and effects (.vsfx) play on the console's AY-3-8912. For digitised samples and the UVMC2's 16-bit jack, see Sound.

Levels and enemies (optional)

libvpy includes a scrolling-level runtime without tiles, with these calls:

  • Levels: vpy_load_level, vpy_show_level, vpy_update_level, the camera accessors, and vpy_level_collision_x/y.
  • Enemies: a pool with patrol, area and wander AI: vpy_spawn_enemies, vpy_update_enemies, vpy_draw_enemies, vpy_kill_enemy, and per-enemy accessors.

It reads compiled .vplay images, the same format Vectrex Studio's level and enemy editors produce. Code you don't call is removed by --gc-sections, so ignoring this part costs nothing.

The modules

On top of libvpy, each of these is one .c file to add to UVM2_SRCS:

ModulePage
vpy3d: meshes, camera, occlusion, 3D text, stereovpy3d
vpyphys, vpyimpact: rigid bodies and impact soundsPhysics
vpyfx, vpycam, vpyease: sparks, shattering, camera, easingEffects & camera
vpybone, vpyik, vpysoft, vpyrope: skeletons, IK, soft bodies, ropesAnimation
vpyent, vpyai, vpyreplay: entities, steering, replaysGameplay

Every module is integer-only and deterministic: the same calls give the same result on every target, bit for bit. Every module counts what it refuses or sheds rather than failing silently, and every one has a check program in sdk/vpy-c/tools/ that tests it against its header.