Programming for Nintendo DS. Simple game

    In this article, we will consider working with tile graphics, interrupts, a touch screen and a keyboard. Based on this, we will write to everyone since childhood a famous game - "tag".
    To begin with, we will examine in more detail the work with the DS video controller.

    Initialization of the video controller


    Almost all video modes use a "multi-layer" structure for organizing the output to the screen, that is, at the same time we can display up to 4 plans (background). I don’t know well which term is better to use; let there be a “plan” - a “background”.

    There are 6 types of backgrounds in total:
    • framebuffer - The simplest type of background. Each word (16bit) in the video memory is displayed as a pixel on the screen. (Used in the previous example);
    • 3D - The picture on the screen is formed by OpenGL-like commands;
    • text - The text background (aka tile) is divided into 8x8 pixel blocks, each of which displays one of the tiles;
    • rotation - A tile plan with the possibility of rotation and scaling;
    • extended rotation - Same as the framebuffer, but it also allows you to display the color depth of 8 bits per pixel, and also supports scrolling, scaling and rotation, in addition, it can use alpha bit;
    • large bitmap - Large 512x1024 or 1024x512 images with 8 bits per pixel.



    As already mentioned, both Nintendo DS graphics cores use shared video memory (656KB). It is divided into 9 banks of various sizes and purposes, named in Latin letters from A to I. Here is a complete list. In order for the video controller to be able to use these banks, we must map (“map”) them to a special area of ​​the address space starting with 0x06000000.
    Deployed more about the organization of video memory and the appointment of various banks can be read here .

    In the game we will use the zero graphic mode of the video controller (MODE_0_2D), in which there are 4 tile plans. On the bottom screen (by default, an additional core) in one of them the game will actually occur (moving chips), and the other is applicable for displaying a splash screen. The upper screen (the main core) is simply used to display textual information.
    videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE); //Видеорежим основного ядра
    videoSetModeSub(MODE_0_2D | DISPLAY_BG1_ACTIVE | DISPLAY_BG0_ACTIVE); //Видеорежим дополнительного ядра

    * This source code was highlighted with Source Code Highlighter.


    Now let's go a little deeper into the organization of video memory in the mode of tile plans. The picture on the screen in this mode is formed on the basis of the so-called tile card, in which the numbers of tiles are written, which should be displayed in squares of 8x8 pixels in the image. The tiles themselves are stored in a separate memory area. From which addresses the video controller will display the map and the tiles are determined by the Control Register (CR). For each of the 8 plans (4 on the main core and 4 on the secondary) there is a register. In our case, we need to initialize 3 of them: SUB_BG0_CR, SUB_BG1_CR and BG0_CR - one for each of the plans used.
    There is a little trick. The fact is that the control registers are 16 bit, and in them you need to store both the card address and the address of tiles and other parameters. In connection with this, 5 bits are allocated for the addresses. Thus, tiles can be stored at 32 base addresses with an offset of 16K, and cards at 32 addresses with a location of 2K.
    Despite the fact that they are stored in one memory bank, we have the following picture:

    We will need 2 tile cards for the bottom screen. They will be located at base addresses 0 and 1. We also need 2 sets of the tiles themselves. The zero base address of the tiles intersects with the used memory of the cards, so we won’t use it. From the base address No. 1, we will place tile tiles. They occupy 36 KB, so we will not use the base addresses 2, 3 and 4 either. Next, from address 5, we will place a set of tiles for the startup screen saver.
    For the top screen, we use the tile card with the base address of 0, and the tiles themselves, in which the Russian font will be placed, we will place from the address 1. The control register for the text will set libNDS during console initialization.
    Now initialize our control registers to use 16 color tiles (BG_COLOR16):
    int tile_base = 1;
    int map_base = 0;
    int tile_base_s = 5;
    int map_base_s = 1;
    int char_base = 1;
    int scr_base = 0;
    REG_BG0CNT_SUB = BG_COLOR_16 | BG_TILE_BASE(tile_base_s) | BG_MAP_BASE(map_base_s); //Заставка
    REG_BG1CNT_SUB = BG_COLOR_16 | BG_TILE_BASE(tile_base) | BG_MAP_BASE(map_base); //Игровые фишки

    * This source code was highlighted with Source Code Highlighter.

    In fact, it was possible to save a fair amount of video memory (32KB) by placing tiles and cards at other base addresses. However, in our case, such optimization is not required, because free memory is more than enough.

    Now we will convert the base address numbers to absolute video memory addresses in order to be able to work directly with it:
    u16* sub_tile = (u16*)BG_TILE_RAM_SUB(tile_base);
    u16* sub_map = (u16*)BG_MAP_RAM_SUB(map_base);
    u16* sub_tile0 = (u16*)BG_TILE_RAM_SUB(tile_base_s);
    u16* sub_map0 = (u16*)BG_MAP_RAM_SUB(map_base_s);
    u16* tile_char = (u16*)BG_TILE_RAM(char_base);
    u16* map_char = (u16*)BG_MAP_RAM(scr_base);

    * This source code was highlighted with Source Code Highlighter.


    Then copy the data of our tiles to the video memory:
    memcpy((void*)sub_tile, (u8*)tilesTiles, 192*192/2); //Тайлы фишек
    for (i=0; i < 16; ++i)
      BG_PALETTE_SUB[i] = tilesPal[i]; //Палитра
    memcpy((void*)sub_tile0, (u8*)startTiles, 256*192/2);//Тайлы заставки
    for (i=0; i < 16; ++i)
      BG_PALETTE_SUB[i+16] = startPal[i]; //Палитра

    * This source code was highlighted with Source Code Highlighter.


    And immediately fill the zero plan of the lower screen with splash screen tiles:
    for (i=0; i< 24*32; i++) //Выводим заставку
     sub_map0[i] = (u16)(i)|0x1000;

    * This source code was highlighted with Source Code Highlighter.

    Each word in the tile map except the tile number contains information about the palette and the reflections along the axes. In order to use the first palette, we set 12 bits to 1 for each element of the map.
    If we paint a bitmap element of a tile map, we will see the following:
    Bitsfifteen14thirteen12eleven109876543210
    AppointmentPaletteVetrik.otr.Horiz.Tile number

    We initialize the libNDS console:
    PrintConsole *console = consoleInit(NULL, 0, BgType_Text4bpp, BgSize_T_256x256, scr_base, char_base, true, true);
    ConsoleFont font;
    font.gfx = (u16*)pa_text2Tiles; //Наш шрифт
    font.pal = (u16*)pa_text2Pal; //Палитра
    font.numChars = 256;//Количество символов
    font.numColors = pa_text2PalLen/2;
    font.bpp = 4;
    font.asciiOffset = 0;
    font.convertSingleColor = false;
    consoleSetFont(console, &font);

    * This source code was highlighted with Source Code Highlighter.

    In the program, we use the font created by ClusterM for PAlib encoded in CP1251. Unfortunately, in the current version of the library, when trying to switch to Unicode, support for outputting the characters of the upper half of ASCII was broken, so you have to do without the Russian text. Although of course it can be displayed directly by writing character codes to the tile card.

    All tiles are created by converting from BMP using the grit program .

    Keyboard and touchscreen


    To read the state of keys without using libNDS, we would have to use not only the ARM9 processor registers, but also ARM7. Fortunately, the creators of the library will be lured to ignore this fact. We just use the scanKeys () function to update the state of the click information. And keysHeld () for determining which key is pressed, or pressing the touch screen. What exactly is pressed is determined in accordance with the bits of the value returned by the function:
    Key defineMask
    bit
    Associated input
    KEY_A1 << 0Button A
    KEY_B1 << 1B button
    KEY_SELECT1 << 2Select button
    KEY_START1 << 3Start button
    KEY_RIGHT1 << 4Button right
    KEY_LEFT1 << 5Left button
    KEY_UP1 << 6Up button
    KEY_DOWN1 << 7Button down
    KEY_R1 << 8R button
    KEY_L1 << 9L button
    KEY_X1 << 10X button
    KEY_Y1 << 11Y button
    KEY_TOUCH1 << 12Touch screen
    KEY_LID1 << 13Cover closed


    So we just do in a loop:
    scanKeys();
    held = keysHeld();
    * This source code was highlighted with Source Code Highlighter.

    And then, depending on the fact that in the variable held we carry out the necessary actions.
    If the KEY_TOUCH bit is set, then a touchscreen is detected and we can read the coordinates of the stylus using the touchRead function. It returns a touchPosition structure, in which we are interested in the px and py fields containing the coordinates of the pixel pointed to by the stylus:
    if (held&KEY_TOUCH){ //Нажатие на тачскрин
     touchRead(&touchXY);
     ...
    }
    * This source code was highlighted with Source Code Highlighter.


    Interruptions


    For the normal operation of most programs interacting with the user (our program is no exception), control of time intervals is required, which is usually provided by interrupts from timers. There are three registers for working with interrupts:
    NameAddressThe sizeDescription
    REG_IME0x0400020816 bitsMain interrupt enable register
    REG_IE0x0400021032 bitsInterrupt Enable Register
    REG_IF0x0400021432 bitsInterrupt Flag Register

    The Interrupt Master Enable Register provides the ability to turn on and off all interrupt handlers.
    The Interrupt Enable Register allows you to enable or disable individual interrupts. Each bit of the register is responsible for a certain interrupt:
    BitNames in libndsDescription
    0IRQ_VBLANKvertical reverse beam
    1IRQ_HBLANKhorizontal return beam
    2IRQ_YTRIGGERscanning the REG_VCOUNT line
    3IRQ_TIMER0Timer worked 0
    4IRQ_TIMER1Timer 1 worked
    5IRQ_TIMER2Timer 2 worked
    6IRQ_TIMER3Timer 3 triggered
    7IRQ_NETWORK?
    8IRQ_DMA0DMA 0
    9IRQ_DMA1DMA 1
    10IRQ_DMA2DMA 2
    elevenIRQ_DMA3DMA 3
    12IRQ_KEYSKey pressed
    thirteenIRQ_CARTGBA cartridge removed
    16ARM7 IPC interrupt triggered
    17Input FIFO is not empty
    18Output FIFO is not empty
    19IRQ_CARDDS card data completed
    20IRQ_CARD_LINEDS card interrupt 3
    21GFX FIFO interrupt

    The Interrupt Flags Register is set by hardware when an interrupt occurs. It contains an interrupt bit mask.

    We will not work with interrupts directly, but as usual, we will use the services of libnds.
    First, we set the interrupt handler along the "vertical reverse beam". This interrupt will be called when screen rendering is finished. We will output a picture to the handler of this interrupt to avoid flickering and tearing of the image:
    void IRQ_vblank(void){ //обработчик прерывания по обратному ходу луча
    ...Здесь выводим картинку...
    }
    ...
    irqSet(IRQ_VBLANK, IRQ_vblank); //Устанавливаем обработчик прерывания по обратному ходу луча.

    * This source code was highlighted with Source Code Highlighter.

    Next, set one of the timers to the desired frequency and set the interrupt handler on it. The libNDS library for this purpose provides a very convenient timerStart function. It is enough for us to call this function with the necessary divider, frequency and pointer to the interrupt handler in order to fully initialize the timer and interrupt.
    void timer0_function(void){
    ...Здесь отсчитываем время игры...
    }
    ...
    timerStart(0, ClockDivider_256, TIMER_FREQ_256(1000), timer0_function); //Таймер с делителем 256 на 1000Гц

    * This source code was highlighted with Source Code Highlighter.


    Finally, consider another function provided by the libnds library - swiWaitForVBlank. It stops the ARM9 processor until a vertical backward interruption occurs.

    Using all of the above, you can already write a simple game. Here you can take the source code of the game "tag", and here is the executable file.
    Screenshot:



    Also popular now: