Star Cruiser 7 - Making of

dotMizi

New member
In the early 80s, the first home computers appeared in German department stores (link to audio podcast in german language). Before I got my own VIC20, these booths were the only way to get in touch with home computers and the sales staff were happy to have young people standing in front of the machines demonstrating the unknown systems to other interested people. So it happened that I spent hours of my free time in a department store in front of an Atari 400 playing Star Raiders.

Later at home I tried to reprogram Star Raiders on my VIC20 in BASIC and of course this could never be successful. When I got an Amiga, I thought about splitting the RGB signal and reproducing the game as an anaglyph display on two tube-based b/w camcorder viewfinders as a head mounted display to get a real 3D effect. But this project was never finished either.

When I bought a backup 3DS for my daughter early this year because I needed to fix various things on hers and didn't know if I would succeed, I got a device with the wrong region code and none of our game cardridges worked. Luckily, though, I managed to fix the old one for my daughter.

To make the otherwise worthless wrong-region 3DS usable for me, I installed the homebrew channel and i wondered how to create such homebrew software. I quickly found the toolchain devkitPro. The enclosed examples are certainly good, but of course they only show their small range of functions and therefore do not give a good overview. Unfortunately, large homebrew projects quickly become very confusing and one gets lost in the extensive code. The program 3DS_mandelbrot seemed like a good start for me, because it is an extremely small code, shows graphics on both screens, uses the 3D effect of the 3DS and uses the touch display as well as the buttons. When I asked myself what my first 3DS homebrew app could be, the idea of making a 3D Star Raiders replica came back to me.

The Mandelbrot homebrew basically uses everything I wanted to use for the game, so I was able to simply take over large parts of the main function, especially the initialisation and the ending:

C:
int main() //using xem's template
{
  // Initializations
  srvInit();        // services
  aptInit();        // applets
  hidInit();        // input
  gfxInitDefault(); // graphics
  gfxSet3D(true);   // stereoscopy (true: enabled / false: disabled)

  circlePosition c3po;
  touchHandler t;

  ... // Mainloop here

  // Exit
  gfxExit();
  hidExit();
  aptExit();
  srvExit();

  // Return to hbmenu
  return 0;
}

The main loop also already contains everything you need for a complete 3DS homebrew:

C:
// Main loop
  while (aptMainLoop())
  {
    // Wait for next frame
    gspWaitForVBlank();

    // Read which buttons are currently pressed or not

    hidScanInput();
    hidCircleRead(&c3po);

    ... // Progam logic and screen drawings here

    // Flush and swap framebuffers
    gfxFlushBuffers();
    gfxSwapBuffers();
  }

So instead of drawing the Mandelbrot set, I "only" had to draw the view into space and the cockpit. However, I just couldn't get the pixels where I wanted them to be. Although I had the function for drawing the Mandelbrot set in front of me, it took me hours to understand that the screens were rotated 90 degrees from what I expected.

Screen.PNG
So x and y coordinates have to be swapped and you are dealing with a display that is 240 pixels wide and 400 pixels high. This would not be so bad if each pixel did not consist of 3 colour bytes. So if you don't take the rotated display into account and write the colour bytes in y instead of x direction, you get a mighty mess on the screen. However, I have implemented this swap in the basic drawing functions so that I could continue to work with a familiar coordinate system in the programme without getting a knot in my brain. You also get unpredictable effects when writing to a frame buffer while the display is configured as a console. So getting debug output isn't an easy task when both screens are in use for graphics. Another surprise came when I thought the 3D slider would control the 3D effect on the hardware side. In fact, the slider is just another (analogue) input. You have to calculate the stereo separation in the programme depending on the slider position and take the offset into account when drawing in the frame buffers.

With this knowledge I was able to realise a wonderful 3D flight through a starfield as a first step.

starfield_flight.gif

Since I couldn't configure a console debugging was difficult and I needed writing on the display anyway, so I created a function that can write text to coordinates in the framebuffer:

C:
void draw_text (char* text, u8* fbAdr, int posX, int posY, int color, int max_width)
{
    int i;
    int x, y;

    x = posX;
    y = posY;

    for (i=0; i < strlen(text); i++)
    {
        if ((x+8 < max_width)&&(y+8 < HEIGHT))
        {
            int idx = (int)text[i] - 32;
            int j, k;
            for (j=0; j<8; j++)
                for (k=0; k<8; k++)
                {
                    u8 pixel = character[idx][7-j][k];
                    if (pixel == 1)
                    {
                        fbAdr[3*((x+k)*HEIGHT + y +j)+0] = color_b[color];
                        fbAdr[3*((x+k)*HEIGHT + y +j)+1] = color_g[color];
                        fbAdr[3*((x+k)*HEIGHT + y +j)+2] = color_r[color];
                    }
                }
        }
        x+=8;
    }
}

It can be used to write to both displays because a pointer to a frame buffer is specified. If you want to write text in a certain 3D depth, you have to call the function for the left and the right frame buffer and adjust the position.
The parameter max_width seems unnecessary at first glance, because the width is basically fixed. However, in order not to have to have one function for the upper and one for the lower screen, I have included this parameter. I created the font myself and build it as a c data structure:

C:
u8 character [128][8][8] =
    {
        {    // space 32
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0}
        },
        {    // exclamation mark 33
            {0,0,0,1,1,0,0,0},
            {0,0,0,1,1,0,0,0},
            {0,0,0,1,1,0,0,0},
            {0,0,0,1,1,0,0,0},
            {0,0,0,1,1,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,1,1,0,0,0},
            {0,0,0,1,1,0,0,0}
        },
        {    //double quote 34
            {0,0,1,0,0,1,0,0},
            {0,0,1,0,0,1,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0},
            {0,0,0,0,0,0,0,0}
        },
        {    // hash 35
            {0,0,0,1,0,0,1,0},
            {0,0,0,1,0,0,1,0},
            {0,1,1,1,1,1,1,1},
            {0,0,0,0,0,1,0,0},
            {0,0,0,0,0,1,0,0},
            {1,1,1,1,1,1,1,0},
            {0,1,0,0,1,0,0,0},
            {0,1,0,0,1,0,0,0}
        },
                ...

This representation had the advantage for me that I could draw the letters and symbols directly in the text editor into the bit fields (when I had finished drawing the upper-case letters, I remembered that Star Raiders also only used upper-case letters and saved myself the mindless creation of lower-case letters).

text.PNG
 
Last edited:
Then I added functions to draw blocks and boxes:

C:
void draw_block (u8* fbAdr, int posx, int posy, int length, int width, int color, int max_width)
{
    int x,y;

    for (x=posx; (x < posx+length) && (x < max_width); x++)
        for (y=posy; (y < posy+width) && (y < HEIGHT);y++)
        {
            int idx;
            idx = ((x*HEIGHT)+y)*3;
            fbAdr[idx+0] = color_b[color];
            fbAdr[idx+1] = color_g[color];
            fbAdr[idx+2] = color_r[color];
        }
}

void draw_box(u8* fbAdr, int posx, int posy, int length, int height, int lwidth, int color, int max_width)
{
        draw_block(fbAdr, posx, posy, length+1, lwidth, color, max_width);
        draw_block(fbAdr, posx, posy + height, length+1, lwidth, color, max_width);
        draw_block(fbAdr, posx, posy, lwidth, height, color, max_width);
        draw_block(fbAdr, posx + length, posy, lwidth, height, color, max_width);
}

And so all the screens could be drawn: Menus, Cockpit, Galactic Map and the Attac Computer and I only had to include the functions for drawing the top and bottom screen in the main loop.

sc7.png
When I implemented the Hyperwarp Marker, it struck me that in a 3D view, the order of drawing did matter. If you draw the hyperwarp marker, which is supposed to be behind the crosshairs, after the crosshairs have already been drawn, you get impossible perspectives in the style of Escher.

The first "foreign" object I wanted to place in space was an asteroid. I chose the same approach as the original Star Raiders and created a bitmap similar to the letters, which was to be copied into the frame buffer at the given position. While the result looked great up to this point, the impression that resulted from this implementation was rather poor.

The original view in Star Raiders of an asteroid was like the first image, the first Star Cruiser 7 asteroid was on the 2nd image and not very 3DS-like:
Asteroid_orig.png Asteroid.png

Real 3D models were needed and I looked at the hardware shader examples. But what the hell is this code from a devkitPro shader example supposed to mean?

Code:
; Example PICA200 vertex shader


; Uniforms
.fvec projection[4]


; Constants
.constf myconst(0.0, 1.0, -1.0, 0.1)
.constf myconst2(0.3, 0.0, 0.0, 0.0)
.alias  zeros myconst.xxxx ; Vector full of zeros
.alias  ones  myconst.yyyy ; Vector full of ones


; Outputs
.out outpos position
.out outclr color


; Inputs (defined as aliases for convenience)
.alias inpos v0
.alias inclr v1


.proc main
    ; Force the w component of inpos to be 1.0
    mov r0.xyz, inpos
    mov r0.w,   ones


    ; outpos = projectionMatrix * inpos
    dp4 outpos.x, projection[0], r0
    dp4 outpos.y, projection[1], r0
    dp4 outpos.z, projection[2], r0
    dp4 outpos.w, projection[3], r0


    ; outclr = inclr
    mov outclr, inclr


    ; We're finished
    end
.end

Since I simply didn't understand the code from the examples and I couldn't find any useful documentation for the PICA200, I thought about programming the rendering of the 3D models myself. Some pages that explain the basics of 3D graphics helped me:

how i built a basic 3d graphics engine from scratch
3d rendering without shaders

It's funny that both articles are probably aimed more at the fact that you don't do this kind of thing anymore because it's taken over by hardware today. With the information from the two sites, the 3D renderer was soon ready and although I filed the 3D models, like the letters, as c structures and "designed" the models in my head, because I didn't want to learn Blender or a similar tool. I really liked the result. Here is an image of a 3D rendered asteroid, witch looks more like it should on a 3DS:

Asteroid_3D.png

The implementation is not complete, so backface culling is certainly missing, but that seemed impossible to me without software to create the 3D models and thus without normal vectors of the surfaces. Also, some of the surfaces are sporadically not drawn, I have no idea why and I find it only slightly bothersome. The only remaining problem is that render objects that are too close cause the FPS rate to plummet. I might have to optimise this.

After I added what felt like hundreds of functions and variables to check and save states and, above all, to make the enemies act in such a way that they posed a danger and could still be defeated in the virtual 3D world of space, Star Cruiser 7 was suddenly playable. And it felt like Star Raiders and it was fun to play!

full.gif

With this state I created the GitHub repository for Star Cruiser 7 and published it.

What really bothered me was the lack of sound. I had no knowledge of programming sounds, so I dug through dozens of examples and libraries. But all interfaces seemed to be built with me in such a way that they required a file path and then played that file. Sure, I could have somehow made sound with it, but that didn't feel right. I wanted to control the playback myself. When I looked at SoundExample3DS again, I had already discarded it because it "only" played a file, the libctru csnd calls reminded me of what I had already seen in the devkitPro audio filter example with the ndsp calls and then it wasn't that hard anymore. I used Audacity to convert some nice royalty-free sound samples to raw PCM16 mono format and put them into c data structures using srecord. This seems to be advantageous compared to files in romfs, because in that case you have to open the files, which are already loaded into memory, again and copy them into another memory. Raw sample files are relatively large, so with them PCM16 data in an c data structure I have only one copy in ram and this seems to me very efficient. Then I had to solve the problem of mixing sounds playing in parallel and found this wonderful pcm mixing function by Victor T. Toth in a stackoverflow article. With this the Star Cruiser 7 sound was perfect and they all lived happily ever after.

Even though the Star Cruiser 7 Source has become a real monster, parts can certainly be reused. The main loop does all the input processing, but I think it's still clear enough to understand. Here you have to be brave and just overlook the multitude of functions in main.c. The audio implementation in sound.c makes me very happy, because I find it very simple. I would be pleased if Star Cruiser 7 would inspire other people to develop their own programmes and games for the 3DS.
 
Last edited:
Back then, camcorder viewfinders had the advantage that they had an eyepiece and could therefore be placed directly in front of the eye. Today you just have displays. Due to the use of CRTs back then, the viewfinders were also quite long. I think such a device would have looked similar to a Neurocaster by Simon Stålenhag.
Simon Stålenhag neurocaster – Google Suche.png
I don't think it would have been as comfortable to wear, but it would have worked and it would have needed very few graphics and CPU resources.
 
Last edited:
Binary files can, as I wrote, be stored as data in c code with srecord. srecord is actually made for storing larger amounts of data together with a programme in EPROMs. It seems to me to be a kind of Swiss army knife for such purposes, the manual is sufficiently complex and very extensive. To convert raw pcm16 files into c code I used the following call:

Code:
srec_cat <sample_name>.raw -Binary -o <sample_name>.c -C-Array <sample_name>

The data is then stored byte by byte as
C:
const unsigned char[]
In the sound functions, the array identifiers are then cast as s16 pointers to better deal with them. Maybe srecord offers better options for this, but this was the easiest and fastest way for me.
 
Back
Top