Behind the scenes of "Lost in the Static"

home

Overview

Activities were interleaved in a fairly fine-grained way: e.g. an hour of working on a level or two, then an hour composing music, then a couple hours of coding.

An hour or two on a previous evening were devoted to a quick test of the graphics idea, and some brainstorming about what the game should be like.

Sources

Storage

Audio is stored in a special way to allow seamless measure transitions, which means the 137 seconds of music is stored as 190 seconds in the game file.

Milestones

Tools

Libraries

In addition to the code written specifically for the game, it also uses: Use of the public domain stb_* decoders means there's no need to include DLLs in the file, which is necessary to allow the game to run directly with no installer.

Programming

I had to write several reuseable tools for this game: The game also includes a built-in editor for creating paths for dynamic objects. (It was also used to design the boss.) About 60 lines of code are within an '#ifdef EDITOR' to facilitate this; the code is not in the shipping game.

Some noteable game code elements:

The noise generator uses 64KB of "precomputed" random bytes, generated by XORing the Mersenne Twister with the linear congruential generator used by BCPL (in other words, stb_rand()^stb_randLCG() ), combined with 32K random indexes into the 64KB table.

Sample Art

The images created were plain-colored which are then dynamically textured with noise within the game.

Sample room (scaled-down):

Sample creature:
Creature art is dynamically stretched and squished when drawn to give it more "life".

Sample Code

Every dynamic object has at least a few lines of custom code. This code is directly inserted in one of several "hook" functions for changing behaviors (rather than e.g. subclassing and overriding).

Here's one example:

    void object_waypoint(Object *o)
    {
       switch (o->id) {
    
             // wait only at end points of path
          case 1:
          case 22: case 23: case 24: case 25: case 26: case 27: case 28:
             if (o->target != 1 && o->target != o->path->num_path) o->wait = 0;
             break;
    
             // pick a random next destination                  
          case 6: case 7: {
             int n = stb_rand() % (o->path->num_path-2);
             if (o->target > o->prev_target) {
                if (n >= o->prev_target) ++n;
                if (n >= o->target) ++n;
             } else {
                if (n >= o->target) ++n;
                if (n >= o->prev_target) ++n;
             }
             o->target = n;
             break;
          }
    
             // teleport back to start
          case 8: case 9: case 10: case 11:
             if (o->target == o->path->num_path-1) {
                o->target = 1;
                o->prev_target = 0;
             }
             break;
    
             // teleport back to start
          case 19: case 20:
             if (o->target == 0) {
                o->target = 1;
                o->prev_target = 0;
             }
             break;
    
             // when we reach a waypoint, hide
          case 18:
             o->y = 4000;
             break;
       }
    }

The inner loop of the noise display looks like this:

    for (i=0; i < z->w; ++i) {
       int c = z->pixels[j*z->w + i];
       uint8 n = noises[c][i];
       out->pixels[j*out->w+i] = clut[n];
    }

Here's the entire music player:

    // MUSIC PLAYER
    
    #ifdef _DEBUG
    int audio=1;
    #else
    int audio=1;
    #endif
    
    stb_vorbis *vorb;
    short (*audio_data)[2];
    int length_decoded;
    
    uint next_measure_time;
    stb_thread vthread;
    
    void * decode_vorbis(void *p)
    {
       int pos=0;
       for(;;) {
          int n = stb_vorbis_get_frame_short_interleaved(vorb, 2, audio_data[pos], 8192);
          if (n == 0) {
             vthread = NULL;
             return NULL;
          }
          pos += n;
          length_decoded = pos;
          stb_barrier();
          if (next_measure_time) // once playback has started, start sleeping
             Sleep(1);
       }
    }
    
    void load_vorbis(void)
    {
       int total_len;
       int len;
       uint8 *data = packed_stb_file("merged.ogg", &len);
       vorb        = stb_vorbis_open_memory(data, len, NULL, NULL);
       total_len   = stb_vorbis_stream_length_in_samples(vorb);
       audio_data  = malloc(total_len * sizeof(audio_data[0]));
       if (!audio_data) outofmem();
       vthread     = stb_create_thread(decode_vorbis, NULL);
    }
    
    typedef struct
    {
       int id;
       int loc;
       int len;
       int start_offset;
       int duration;
    } Measure;
    
    Measure *measures;
    #define music ((void *) 1)
    
    int play_measure(int m)
    {
       int i;
       if (stb_arr_len(measures) == 0) return 0;
       for (i=0; i < stb_arr_len(measures); ++i) {
          if (measures[i].id == m) {
             Measure *z = &measures[i];
             if (z->loc + z->len <= length_decoded)
                mixlow_add_playback(audio_data[z->loc], z->len, 1, 2, 0, next_measure_time - z->
t_offset, z->len, 1, FADE_none,0,0, 1,0,music);
             else {
                next_measure_time = mixhigh_time() + 4000;
                return 0;
             }
             next_measure_time += z->duration;
             return 1;
          }
       }   
       return 1;
    }
    
    int *music_loop, mloop_len, mloop_next, mloop_start;
    void set_loop(int *loop, int len, int start)
    {
       if (music_loop == loop && mloop_len == len) return;
       music_loop = loop;
       mloop_len = len;
       mloop_next = 0;
       mloop_start = start;
    }
    
    void update_measure(void)
    {
       if (!audio) return;
       while (next_measure_time < mixhigh_time() + 6000) {
          if (mloop_next >= mloop_len) mloop_next = 0;
          if (!play_measure(music_loop[mloop_next])) return;
          ++mloop_next;
          if (mloop_next >= mloop_len) mloop_next = mloop_start;
       }
    }
    
    int title_loop[] =
    {
       1,1,
       2,27,5,30,
       2,27,3,28,
       4,29,5,27,
       6,31,7,32,
       6,31,7,32,
       8,33,7,32,
    };
    
    #define MLOOP(x)     set_loop(x, sizeof(x)/sizeof(x[0]), 0)
    #define MLOOPS(x,y)  set_loop(x, sizeof(x)/sizeof(x[0]), y)
    
    short *intro;
    int intro_len;
    
    void load_music(void)
    {
       int i,n=0,c,t,mlen;
       char **data, *mem;
    
       if (!audio) return;
    
       mem = packed_stb_file("static_title_3.ogg", &mlen);
       if (mem)
          n = stb_vorbis_decode_memory(mem, mlen, &c, &intro);
    
       #ifdef EDITOR
       if (n) n = 2000;
       #endif
    
       t = mixhigh_time() + 4000;
       if (n)
          mixlow_add_playback(intro, n, TRUE, c, 0, t, n, 1, FADE_none,0,0, 0.75, 0, music);
       next_measure_time = t + n + 44100 * 0.5;
    
       mem = packed_stb_file("merged_data.txt", &mlen);
       if (mem) {
          mem[mlen-1] = 0;
          data = stb_tokens(mem, "\r\n", &n);
          //data = stb_stringfile("merged_data.txt", &n);
    
          for (i=0; i < n; ++i) {
             Measure m;
             sscanf(data[i], "%d %d %d %d %d", &m.id, &m.loc, &m.len, &m.start_offset, &m.duration);
             stb_arr_push(measures, m);
          }
       }
       load_vorbis();
    
       MLOOPS(title_loop,6);
    }
    
    void audio_loopmode(void)
    {
       if (!audio) return;
    
       // we're trying to run at at least 30hz, which is:
       //  44100 / 30 = 4410 / 3 = 1300 samples
       mixhigh_step(4000);
    
       if (next_measure_time == 0) {
          if (length_decoded >= 44100*4) {  // wait until 4 seconds of audio is decoded
             next_measure_time = mixhigh_time() + 2000;
          }
          return;
       }
       update_measure();
    }
    
    // atexit function
    void music_stop(int nice)
    {
       int t = mixhigh_time();
       if (vthread) stb_destroy_thread(vthread);
       mixlow_end_set(music, FADE_linear, 0, nice ? 32000 : 3000);
       while (mixlow_num_active()) {
          mixhigh_step(4000);
          Sleep(10);
       }
       mixhigh_deinit();
    }


home