Copyright © 2006-2008 Jaeden Amero
| Revision History | ||
|---|---|---|
| Revision 6.0 | 03 Jan 2008 | jna |
| This manual now covers additional aspects of Nintendo DS Programming including multiple sprites, multiple backgrounds, multiple palettes, the touch screen, Slot-1 devices, and more. The manual's case study has undergone a significant rewrite and covers more ground than ever before. | ||
| Revision 5.0 | 31 Mar 2007 | jna |
| Made Editing improvements. Manual is now DocBook formatted. | ||
| Revision 4.0 | 31 Oct 2006 | jna |
| Added VRAM Appendix, updated page layout, fixed even more typos. | ||
| Revision 3.0 | 23 Sep 2006 | jna |
| Added a sound chapter | ||
| Revision 2.2 | 12 July 2006 | jna |
| Various typo fixes | ||
| Revision 2.1 | 13 May 2006 | jna |
| Fixed some code, spelling, and stupid little extra words. | ||
| Revision 2.1 | 21 Mar 2006 | jna |
| Finalized many chapters and improved code. | ||
Table of Contents
List of Figures
List of Tables
Table of Contents
You love playing games on your Nintendo DS. Every game you've played has been a wonderful experience, each one leaving a lovely aftertaste on your gaming tongue. You may have wondered to yourself what it'd be like to create games, to offer your own software up for licking. You've drawn diagrams of games you'd love to make, worlds you want to share with others. But how to go about it? You think and are lost: you are stuck. Where do I start? Whats going on inside that pretty little dual-screen box?
This manual is designed to help you get an idea of whats going on inside the Nintendo DS. With a bit of effort and time, you'll be on your way to creating your own games. Join us, the homebrew community. You'll have a great time giving others a great time, collaborating on projects, and feeling the rush of intense and under pressure coding for numerous programming competitions competitions.
This manual is the start of the solution. In it, I will cover the basics of programming the Nintendo DS starting with an explanation of the politics behind the homebrew movement and through the emergence of passthrough devices, how to choose between a Slot-1 device or a Slot-2 device, , how to choose a passthrough device, setting up the programming environment, displaying backgrounds, using sprites, and basic game programming techniques. All these things will be discussed in the context of the creation of a simple game I concocted one weekend entitled "Orange Spaceship."
I assume you know a bit of C or C++ coding. If not, spend at least 20 hours making a variety of programs on your own in addition to completing some tutorials. It doesn't matter what you write, so long as you make sure you have a solid understanding of structs, classes, the heap, loops, bitwise and logical operators. I recommend the following tutorial and reference as a great place to get started learning, http://www.cplusplus.com/doc/tutorial/ and http://www.cppreference.com/.
Next, just read through the chapters one by one, making sure you understand the current chapter before moving to the next. Code listings will be on a gray background. Follow along with the code listings, writing your own code based on the listings.
Table of Contents
Since the Nintendo DS debut, Nintendo enthusiasts ranging from pre-pubescent kids to 30-year-old college dropouts have been wanting to develop their own games and applications for the Nintendo DS. Nintendo has stated that the DS stands for "Developer's System". For those worthy enough to land a nice developing contract with Nintendo, it truly is. However, most people will never receive this contract, special permission from Nintendo to commercially produce games for the Nintendo DS. In order to obtain a contract with Nintendo, you must prove your worthiness by showcasing an amazing game or other piece of software to them. You must have a stable financial history and expected stable financial future. You must have ample funding to buy all the official Nintendo equipment to develop for the system. Most game development houses don't even get that far. Most games on the market today are put out by what is referred to as a publisher. Game development houses will produce their game partially, show it to a publisher, and the publisher (who already has this development contract with Nintendo) will fund the game development house and market their game. All this bureaucracy makes it very difficult for the common person to produce their own, personal-use games.
This is where the homebrew movement comes in. Dedicated hobbyists spend weeks reverse engineering the Nintendo DS hardware, just to make it possible for common people to produce for the system (by providing a cheap alternative to official Nintendo development). These dedicated hobbyists come from all walks of life and cultures, many from Europe and the U.S., and bring with them great knowledge. These people make up the homebrew movement.
Homebrew is legal for a number of reasons. You own the hardware you reverse engineer, so you are free to do with it as you will so long as you don't break the law. Breaking the law would include breaking proprietary copy protection, pirating video games, publishing illegally obtained trade secrets, or otherwise trying to profit off someone else's hard work. Homebrew poses no threat to the official developer kit, as it is so primitive in comparison. Even if you made something to compete with officially produced software, it would be near impossible to publish it. Companies often benefit from homebrew communities. Although software pirates often steal from homebrew discoveries to pirate software, the homebrew community abhors piracy and takes a strong stance against it.
When you buy a piece of hardware, you own it. This means that you are free to break it open, dive into it, reverse engineer it, and so forth. You may void your warranty, but thats the price for learning the intimacies of any system. The only illegal things on this line would be to put into production and sell products made with patented features (without negotiating a production deal with he patent owner), bypassing or breaking copy-protection, or stealing software code. Reverse engineering to learn about how the hardware works and to make something fun for the community is totally fine.
The homebrew tools available for game programming are far behind anything the game company who produced the system could provide (the official development kits). Game system developers have an intimate knowledge of the hardware, as they developed it. The homebrew community has only outsider knowledge through experimentation with the hardware.
It would be close to impossible to publish a game made with homebrew tools. Nintendo would not license your game. It would be hard to find another publisher who would try to publish something made with homebrew tools against Nintendo's will. On other systems besides the Nintendo DS, this is also true.
Companies often don't have a problem with homebrew because it increases the demand for their gaming systems and helps them to learn more about their consumer base. One example of this is with the Xbox. The Xbox homebrew community made the Xbox do things that Microsoft never thought consumers wanted like playindie games, emulate classic game systems, run the Linux operating system, and so forth. Microsoft then included a lot of these features (excepting Linux, of course) in their most recent gaming console, the Xbox 360 via a system called XNA Game Sudio and Xbox LIVE Arcade (XBLA). If a company wants to squash homebrew developers for whatever reason, they'll be smashing an essential fan base that loves that company's hardware design and has the potential to improve it (all at no cost to the company). Homebrew caused such a high demand for the Xbox that it would not have been in Microsoft's best interests to ignore or punish it.
The downside of homebrew is that software pirates often steal from the discoveries of homebrew and use that information to bypass copy-protection and to pirate games. Some companies may take a stance against homebrew for this reason, but doing so is unproductive. Piracy is regrettably inevitable in any industry. It is extremely destructive, annihilating game development houses because publishers will no longer publish their games due to a high piracy rating on the platform the game developers are developing for. Homebrew developers know this, and as the amateur brothers of the official game developers, they share the pain. Homebrew will usually keep all information regarding copy-protection in high secrecy; even if they know how to copy games, they will not share the information. The homebrew community does not want to see the the system they so dearly love come to an early death.
Table of Contents
The purpose of the pass through is to allow the running of programs on the Nintendo DS through the Game Boy Advance (GBA) cartridge slot. This is done because normal Nintendo DS games, which run from the Nintendo DS (NDS) card slot, are encrypted. Since it might be illegal to break that encryption, as it is a form of proprietary copy protection, we have to get the Nintendo DS to run code from a different place than the NDS card slot. Also, it is much easier to bypass the encryption than to try and break it.
When the Nintendo DS first boots, it reads a header from the Nintendo DS card inserted into the NDS card slot. The Nintendo DS will read a small block of information from the beginning of the game, known as the header, which contains information about the game (including the title of the game, date it was released and by who, the icon, and some hardware information). This header contains a pointer to a location in memory to begin executing code. The passthrough's job is to read this header and modify it to point to a location someplace on the GBA cartridge inserted into the GBA cart slot. What location would this be? Why, our code of course.
DarkFader (Rafael Vuijk) may have been the first to create a passthrough device. He designed his passthrough with an FPGA (Field Programmable Gate Array) and later packaged it into a single CPLD. Many other people (including Kraln, Firefly, and Natrium42) have developed a passthrough independently from DarkFader, but he is self-credited as its initial creator.
Over the next few months following the creation of the passthrough, Lynx and Natrium42, both major players in the early homebrew community, started their own online stores selling pre-made passthrough devices called "PassMe"s. Many budding DS programmers bought their first passthrough from Lynx or Natrium42's stores (myself included). Lynx's store, DSPassme.com, is still up and running today, providing a great option when looking to purchase a passthrough device.
The PassMe was the first device invented to allow homebrew software to be run on the Nintendo DS. After Nintendo decided to crack down a little harder, in efforts to prevent piracy, these devices no longer worked on new Nintendo DS systems. The community then developed the PassMe 2. This device was more fancy in how it bypassed the copy protection of the Nintendo DS and also required the operator to have more resources (a GBA flash cart with SRAM) and go through a more involved process. After a while, a device called the NoPass surfaced.
A NoPass pretends to be a real Nintendo DS game card. It contains code that is encrypted in the same manner as a real Nintendo DS game. The encryption was broken by Martin Korth, who discovered a way to dump the Nintendo DS BIOS. He then reverse engineered the encryption information from the BIOS code. NoPass devices are technically not considered passthroughs, since they don't bypass the copy protection of the Nintendo DS.
Recently, due to Martin Korth's work, homebrew engineers have discovered a way to create a device that runs homebrew software from the Nintendo DS card slot (Slot-1). These new devices contain small passthrough devices (of the NoPass style) inside them that allow them to bypass copy protection as well some means of providing memory to the Nintendo DS (via built-in flash memory or a microSD card slot). These devices are called "Slot-1 devices". The older style devices that plug into the GBA cart slot are referred to as "Slot-2 devices".
Today, many passthrough and NoPass devices are on the market. It is highly recommended by the homebrew community to buy one from a fellow developer and not a company that commercially produces hardware meant for pirating Nintendo DS software. Thus, one should not buy a SuperPass or SuperKey from SuperCard, a MagicKey or MK4-Mini from NeoFlash, a PassKey from G6Flash, a PassKey or Passcard from M3Adapter, or a Max Media Launcher from the horrid Datel (a very unprofessional company run by immature anti-Nintendo kiddies). Each of these companies manufactures hardware that is used to pirate Nintendo DS software and should be avoided at all costs. The best place to buy a passthrough device today is from DSPassme.com.
It is recommended that you purchase a NoPass or a Slot-1 homebrew device, as these work on all Nintendo DS systems with no troubles or other complications whatsoever. The older passthroughs, like PassMe, don't work on all Nintendo DS systems, and the PassMe 2 requires extra work and resources to use, not to mention they both stick out of your Nintendo DS like a sore thumb and are hard to keep in your pocket.
Slot-1 devices also have many other advantages over Slot-2 devices and passthroughs. You definitely want to get a Slot-1 device. For instance, Slot-2 devices suck up a lot more power than Slot-1 devices do. This means that using a Slot-1 device will give you more battery life than using a Slot-2 device. Many Slot-1 devices also double as NoPass devices, so you don't need to buy both a passthrough and a Slot-2 device anymore.
If you still want to buy an old fashioned style passthrough, there are a number of things you'll need to consider. Depending on what kind of Nintendo DS you have, you can buy either of two different types of passthrough devices. The first one, akin to the original made by DarkFader, is the "PassMe". The PassMe will probably work on most early "phat" (non-Nintendo DS Lite) Nintendo DS systems.
However, if your Nintendo DS is newer (both phat and Lite systems can be new), you will most likely need a "PassMe 2". The need for a PassMe 2 came about because of a change in the firmware of newer Nintendo DS systems. These newer systems do not allow the header to be changed to point to code in the GBA slot. However, it is possible to point to code in the GBA carts SRAM still. The PassMe 2 points to some certain instructions that have been loaded into memory by the currently inserted DS game. This location is an SWI (software interrupt) call to the SRAM on the GBA port. The GBA carts SRAM contains the necessary code to jump to code lo- cated on GBA cart. The DS then is told to run this code and thus redirects to code in the GBA slot. Each PassMe 2 has to be programmed to work with one specific game, as the certain code to redirect to the GBA slot is found in different places within different DS games.
There are a few tricks you can pull to determine your firmware version. It involves pictochat and pulling a cartridge out of your Nintendo DS. The screen will change a certain color. Based on this color you can determine if you need a PassMe 2 or not. Table 2.1, “When ejecting the game, you'll discover your firmware version as shown.” will help you discover which firmware version you have.
Procedure 2.1. Checking the Firmware Version
Insert and Nintendo DS game into the DS card slot.
Turn on the Nintendo DS. (If you have auto start enabled, hold down Start and Select when you boot the system.)
Start Pictochat by pressing the Pictochat logo on the menu screen.
Join any room you wish.
Remove the Nintendo DS game inserted into the back of the system.
You will notice either Pictochat freezing or both screens will change color.
Table 2.1. When ejecting the game, you'll discover your firmware version as shown.
| Observed Results | Firmware Version |
|---|---|
| Pictochat froze | Version 1 |
| Both screens turned periwinkle | Version 2 |
| Both screens turned dark green | Version 3, iQue, or Flashme |
| Both screens turned marigold | Version 4 |
| Both screens turned magenta | Version 5 |
| Both screens turned dark blue | Version 6 |
It is recommended that you purchase a NoPass instead of a PassMe or PassMe 2 passthrough device if you are unsure which to get, as they are guaranteed to work with all Nintendo DS systems, past, present, and future. If you have a friend with a newer DS who may need to borrow your NoPass to play your games, it's always helpful to have a universal method of running them.
When ordering a PassMe 2, be sure to select one pre-programmed to a common game that you own, such as the Metroid Prime Hunters Demo or Super Mario 64 DS. Each PassMe 2 must be programmed to a specific game and it is best to select a game that is in large supply. Also, you'll need a GBA flash cart with some SRAM on it. Most GBA flash carts have SRAM, but be sure to check your cart before trying to obtain a PassMe 2. Some newer GBA flash carts might only support save games on flash, so simply knowing that your flash cart supports save games isn't enough.
Using a passthrough device is quite simple. Simply insert a game into the card slot on the passthrough and plug the device into the DS card slot on the top of your DS, like any other game. Figure 2.2, “The PassMe inserted into the DS card slot” illustrates what this looks like.
One of the best things to do once you get your passthrough is to install a patch to your Nintendo DS firmware known as "FlashMe." This firmware patch allows for the running of unsigned wifi binaries as well as the redirecting of the Nintendo DS to boot from the GBA slot in NDS mode. This means that after installing FlashMe, you no longer need your PassMe. FlashMe is the preferred and recommended method of running your own software. You no longer have to lug around a passthrough or play with a large block sticking out of the back of your DS. Also, you can send your code via wireless multiboot (WMB), if you so desire, and avoid having to use a flash cart. In addition to all those things, in case you ever come across some malicious code that zaps your firmware or otherwise messes up your DS, FlashMe keeps a recovery program in the protected firmware space that you can run to save your DS. Because of this feature alone, FlashMe is better than the standard firmware. FlashMe can be obtained from http://home.comcast.net/~olimar/flashme/.
Table of Contents
There are a few ways of getting your code into the Nintendo DS. The first of which is the simple GBA flash cart. These flash carts are generally quite expensive, have a low availability, and don’t hold very much memory. They fit into the phat Nintendo DS systems perfectly and do not stick out from the bottom, except on the Nintendo DS Lite, as other things do. The second way of running code is on a removable memory device. These kinds of devices come in two flavors: Slot-1 and Slot-2. Slot-2 devices, such as the M3 Adapter, G6 Flash, NeoFlash, SuperCard, and the GBA Movie Player, generally support both Nintendo DS and GBA software. Slot-1 devices, such as the M3 Simply, R4DS, M3 Real, DS-X, NinjaDS, and the Cyclo DS Evolution, generally only support Nintendo DS software. Slot-1 devices also fit into the Nintendo DS perfectly. The first four of those Slot-2 devices are produced by supporters of piracy and should be avoided. It is recommended to use the GBA Movie Player (GBAMP) if you want to use a Slot-2 device. With Slot-1 devices, we don't have too much of a choice but to choose the lesser of many evils.
As there are no homebrew produced devices made by hombrew people for homebrew people (yet), we regretfully have to buy devices designed and sold for piracy when buying NoPass or Slot-1 devices. That said, the most important feature to consider when buying one of these devices is DLDI support.
DLDI stands for dynamically linked device interface. In order for the homebrew filesystem, known as libfat, to work, you'll need a device which supports the DLDI standard. DLDI specifies a system by which DLDI modules can be dynamically loaded into homebrew software. This allows developers to release one version of their homebrew which the software end users simply patch and then run on a device of their choice. If you buy a device that does not support DLDI, you will be out of luck when wanted to run a majority of the homebrew applications and games out there. Not to mention that the company producing the device is essentially saying, "We don't need to spend time catering to the homebrew community since our device is designed just for piracy." We'd be better off not supporting that kind of company with our business.
You can learn more about DLDI and how to use it at the DLDI Wiki website, http://dldi.drunkencoders.com/index.php?title=Main_Page.
There are so many Slot-1 devices on the market today. Choosing one of them has become very difficult, as the choices are so many. In this manual, I'll discuss the devices I'm familiar with, which nifty features they propport, and how they compare to one another.
This device is built well. It is the most sturdy of the three. It uses microSD cards for storage and supports DLDI. This means that you can run homebrew software and use the microSD card's filesystem. With the most recent firmware, it can even automatically patch generic DLDI homebrew software with the proper DLDI driver for the R4DS on the fly. This means that you don't have to use your PC to patch your homebrew software before placing it onto your microSD card. The R4DS cannot use microSDHC cards, however. Some people have complained about the spring loaded microSD slot, so the makers of R4DS have removed it from current models. The R4DS is a great device overall and well supported by the Nintendo DS homebrew community, but it does not support running GBA software unfortunately.
This device comes in varying configurations for your various needs. It comes with both a Slot-1 device and a Slot-2 device. The Slot-2 device is what differs between the three M3 Real configurations. In the first and cheapest configuration, the Slot-2 pack is simply a rumble pack. In the second and more expensive version, the Slot-2 pack is a dual rumble and RAM pack. This configuration will also run GBA homebrew. The flash cart size is kept secret by M3, for some reason. Natrium42 was kind enough to inform me that, after doing a few tests on the pack, that it is at least 8MB in size, but lacks SRAM. The third and most expensive configuration, dubbed the "GBA Expansion Package", provides a Slot-2 pack with rumble, SRAM, and 32MB of RAM.
The M3 Real supports DLDI as well. I'm not sure if it can automatially patch homebrew with its own driver on the fly, however. The homebrew software may need to be patched on a PC before being transferred to the SD card.
The M3 Real supports microSD and microSDHC cards, unlike the R4DS. So if you are in the mood for massive storage, the M3 Real can be your friend.
This device has very recently come to market, but is already making a big impression in the community. The Cyclo DS Evolution has the best looking and functioning menu system of the three devices, in my opinion. And if you don't like it, the menus are skinnable with a number of skins already available for the device. You might be thinking, "Why would I care what my menus look or act like?" Well, when you use the device to load your software over and over and over again, bad menus really piss you off. When you can't use the touch screen to scroll up and down and select items and have to use the keys, but holding down the keys doesn't make the menu scroll by and you have to press the up and down buttons a lot to get to your software, you get pissed off. And yes, there really are menus that are this bad.
Aside from the pleasant menus, the Cyclo DS Evolution supports DLDI. It also features on the fly DLDI patching. I dislike having to patch my games on my PC before loading them onto my microSD card, so this is a great feature.
My favorite feature of the Cyclo DS Evolution is the "remember what I loaded last time and load that same program again" feature. Simply holding down L and R at boot will boot the last thing that was booted. This enables me to avoid navigating the menu system when I'm debugging a program and running it repeatedly for testing.
The Cyclo DS Evolution also has a NoPass mode where it will act as a NoPass to boot your GBA flash carts, a GBA Movie Player, or any other Slot-2 device.
As you've probably noticed, as I've written the most about the Cyclo DS Evolution, that I like it the best of the three devices. It is competitively priced with the others, which is good. While it doesn't support running GBA software, it feature rich on the Nintendo DS side of things. I recommend the Cyclo DS Evlolution to meet all of your Slot-1 needs and desires.
Out of the many places to buy these devices, I've been most happy with electrobee. electrobee is run by a trusted member of the homebrew community and ships worldwide from good ol' Canada (as opposed to who knows where). Their prices are quite often the best, too. When you are ready to purchase a Slot-1 device, I recommend that you visit http://electrobee.com/.
If you decide that GBA software development is important for you, you might want to consider getting a NoPass and a Slot-2 device. The NoPass will allow you to run Nintendo DS software from your Slot-2 device. The Slot-2 device, on its own, will be able to run GBA software. If you only care about Nintendo DS software, a Slot-1 device will meet your needs well.
The GBA Movie Player is a wonderful device which can run your software from a Compact Flash card. Compact Flash cards are very cheap and in high supply. If you have more SD cards than CF cards around, unfortunately, the only way to run NDS software from an SD card at this time is with the SD versions of the SuperCard or M3 Adapter. The GBAMP also sticks out from the bottom of the Nintendo DS a little, as shown in Figure 3.1, “Comparison of GBAMP (left) and a GBA flash cart (right) inserted into a DS”
With the GBA flash cart, the process of loading your programs into memory is a bit slow. Also, each GBA flash cart writer is specific to certain carts and often have closed source drivers. This means that most flash carts will be incompatible with Linux or Macintosh computers. In actuality, the only good thing about a GBA flash cart is that it does not stick out from the Nintendo DS, as can be seen in Figure 3.1, “Comparison of GBAMP (left) and a GBA flash cart (right) inserted into a DS”.
If you chose to go with the GBA flash cart, you can use a utility called Darkain’s MultiNDS Loader to load multiple programs onto your flash cart. This way, you don’t have to re-flash your cart each time you want to run a different application.
If you chose to go with the GBAMP, then I’d highly recommend DragonMinded's DSOrganize. It supports booting multiple programs, text editing, address books, calendars, and more. It's a great application for the DS. You can get it from http://www.dragonminded.com/?loc=ndsdev/DSOrganize. However, you cannot use a stock GBAMP to run NDS programs. You have to flash it with some custom firmware. Instructions and firmware are available from http://chishm.drunkencoders.com/NDSMP/index.html.
As for Slot-1 devices, these all usually come with their own menu systems (often incorrectly referred to as operating systems). You won't have to do anything special to run multiple software titles.
Table of Contents
devkitPro brand name, like Microsoft or Adobe. You don't use devkitPro to build your Nintendo DS software anymore than you write your letters with Microsoft or edit photos with Adobe. devkitPro produces a collection of toolchains for homebrew developers. Toolchains are available for Game Boy Advance, GP32, Playstation Portable, GameCube, and the Nintendo DS. The toolchain we are most interested in is known as devkitARM.
devkitARM is a specific toolchain of devkitPro. It allows the compiling of ARM binaries from most all computers. It is based on gcc, the gnu compiler collection. devkitARM includes everything you'll need to create software for the Nintendo DS, GBA, and GP32; all of which are run by the ARM processor. However, we will be using something to make our job much easier in addition to just devkitARM.
libnds, the library for Nintendo DS, started out its life as NDSLIB. NDSLIB was a simple library created by joat (Michael Noland) and dovoto (Jason Rogers). The name was changed to libnds over the course of a few months and the maintainer has been changed to WinterMute (Dave Murphy).
NDSLIB started out as a collection of defines for common memory locations in the DS. This is useful, as you can simply reference BG_BMP_RAM instead of 0x06000000. Eventually, the library began to include structs and unions and other useful constructs that help to simplify the programmers job and abstract certain portions of the hardware from the programmer.
Today, libnds is an incredibly useful library that over 96% of the Nintendo DS homebrew community uses.
Installing devkitARM is quite simple. Directions are already documented on their website. Visit http://www.devkitpro.org/setup.shtml for directions. Although more geared towards Windows, the installation is fairly straight forward. Automated installers are available for Windows, Macintosh, and Linux.
libnds's source install is less documented than devkitPro's source install, but is also quite simple in comparison. libnds is automatically installed by the automated installers of devkitARM. However, if you want to view the libnds source code, you'll have to install it from source.
Procedure 4.1. To install libnds from source
Simply download the latest source from SourceForge.net.
Extract it to $DEVKITPRO/libnds.
patater@patater.com:~$mkdir
$DEVKITPRO/libnds
patater@patater.com:~$mv
libnds-src-*.tar $DEVKITPRO/libnds/
patater@patater.com:~$cd
$DEVKITPRO/libnds
patater@patater.com:~$tar
-xvjf libnds-src-*.tar.bz2 $DEVKITPRO/libnds
Change your current directory to
$DEVKITPRO/libnds and type
make.
patater@patater.com:~$cd
$DEVKITPRO/libnds
patater@patater.com:~$make
If devkitARM is installed properly, libnds will compile in a matter of seconds and you'll be on your way to developing software for the Nintendo DS.
Now that you have devkitARM and libnds installed on your computer, you have everything you need to start coding, excepting perhaps a bit of knowledge on how to code specifically for the unique harware of the Nintendo DS. In the next chapter, we'll cover the basics of displaying a bitmap on the screen.

Table of Contents
Since the beginning of time, humans have used raster displays to draw electronic images. Raster images aren't used too much anymore, in practice. However, most all displays still act like raster displays from a programming perspective. Each physical display on the Nintendo DS is actually an LCD screen which works nothing like a raster display in actuality, but the electronics that control it provide an interface to the graphics engines on the Nintendo DS that is very much like a raster display.
So what is a raster display? Put simply, it is just like a television display. A beam of electrons blasts away at the back of a phoshor coated screen in a deterministic way (known as a raster scan). The beam, from the perspective of a person watching the television, travels from left to right, one scan line at a time. The beam never blast electrons from right to left. After the beam reaches the right edge of the screen, it turns off and moves to the right and down one line. When the beam finally reaches the bottom line of the screen, it travels back up to the upper left of the screen and begins this drawing process all over again. Figure 5.1, “The Raster Display” illustrates this process in an exaggerated fashion.
Two things are important to remember here. First, that the period of time during which the beam goes from right to left (not drawing anything) is called the horizontal blanking period, or hblank. Second, that the period of time during which the beam goes from bottom to top (again, not drawing anything) is called the vertical blanking period, or vblank. Knowing about vblank is useful for us as Nintendo DS programmers because it is the period of time in which we will tell the Nintendo DS to draw things. If we didn't, then the Nintendo DS display might be in the middle of drawing the screen when we tell it what to draw. This would give us strange artifacts and is generally undesirable.
The Nintendo DS has two screens. As such, it has two graphics engines, one for each screen. The first engine is referred to as the "main" engine, while the second is called the "sub" engine. Both of these engines can put in charge of either screen. By default, libnds tells the main engine to operate the top physical screen and the sub screen the bottom physical screen.
Each engine supports up to four backgrounds and up to 128 sprites. They operate independently of each other, each with their own control registers, backgrounds, sprites, graphics data, and palettes. To use an engine, we must first turn them on. Then we place the engine into a certain mode of operation. Next, we map memory for use with the engines. Finally, we load their memory with graphics data and palettes and they automatically just draw what we've set them up to do.
Each of these graphics engines can be placed into one of seven different modes that support different features. In this manual, we'll cover one of the more common graphic modes, Mode 5. It is commonly used because it is very flexible and allows for some amazing special effects. We'll learn how to display multiple 15-bit color 1-bit alpha affine backgrounds in this chapter. Mode 5 consists of four different backgrounds each with their own capabilities. Table 5.1, “Mode 5 Information” shows how flexible Mode 5 can be.
Table 5.1. Mode 5 Information
| Background | Purpose |
|---|---|
| 0 | Tiled Mode, 2D with 3D support |
| 1 | Tiled Mode, 2D |
| 2 | Extended Rotation Background |
| 3 | Extended Rotation Background |
An affine background, also known as an external rotation background or exrot background, is one that has the ability to be transformed by an <glossaryterm>affine transformation matrix</glossaryterm>. Since linear algebra is beyond the scope of this manual, I will cover briefly how to set up a basic affine background, but not get into rotating, scaling, or sheering it. For this manual, we'll just apply an identity transform, a transform that doesn't actually change the background, to our backgrounds. With an affine transformation matrix, you can rotate, scale, and sheer an affine background, among other interesting effects. If you want to learn about the cool effects you can do with an affine background and an affine transformation matrix, I recommend you look at Cearn's tutorial on affine transformations at http://www.coranac.com/tonc/text/affine.htm.
Since this is the first time in this manual where we'll start
to write code, we should be aware of the resources available to
assist us in following along with this manual. If you haven't
already done so, download the sources that accompany this
manual from the manual homepage at http://patater.com/manual. After extracting the
sources, you'll find a folder called code.
The manual provides a template for you to use with this manual
and any other Nintendo DS programming projects you might wish
to start developing. This template is located in the
code folder and is itself a folder called
chapter_0-starting_system. The
code folder also contains project folders
for each chapter. If at anytime you get stuck or if you want to
skip a chapter, feel free to refer the completed project for
the chapter you are stuck on or to grab the completed project
for the chapter prior to the one you wish to skip to. To follow
along with this manual, copy the
chapter_0-starting_system folder to a nice
place you wish to work from (I'd copy the folder to my
~/projects directory and name the copy
manual) and open
source/main.cpp with your favorite text
editor. (My favorite text editor is gvim.) Let's get going!
In order to get the hardware to do what we want, we have to
first initialize it. This means turning on the 2D core and
setting up a VBlank IRQ
handler. This is where we first fall in love with
libnds. (We'll fall in love with it repeatedly over the course
of this manual.) libnds makes it incredibly simple to do these
two things. Add the following code to your new
main.cpp C++ code file.
#include <nds.h>
int main() {
/* Turn on the 2D graphics core. */
powerON(POWER_ALL_2D);
/*
* Set up interrupts.
*
* We don't really get into what these do exactly at this point in the
* manual, but we still need to do them for now.
*/
irqInit();
irqEnable(IRQ_VBLANK);
return 0;
}
After we get the basic setup done, we now have to tell the
graphics engine where to get its display data from. The two
graphics engines share the same VRAM; i.e. There are not two
VRAM A banks, one for the main screen and one for the sub
screen. We'll use these memory locations when we load the
graphics later. Let's make a function called .
initVideo
void initVideo() {
/*
* Map VRAM to display a background on the main and sub screens.
*
* The vramSetMainBanks function takes four arguments, one for each of the
* major VRAM banks. We can use it as shorthand for assigning values to
* each of the VRAM bank's control registers.
*
* We map banks A and B to main screen background memory. This gives us
* 256KB, which is a healthy amount for 16-bit graphics.
*
* We map bank C to sub screen background memory.
*
* We map bank D to LCD. This setting is generally used for when we aren't
* using a particular bank.
*/
vramSetMainBanks(VRAM_A_MAIN_BG_0x06000000,
VRAM_B_MAIN_BG_0x06020000,
VRAM_C_SUB_BG_0x06200000,
VRAM_D_LCD);
/* Set the video mode on the main screen. */
videoSetMode(MODE_5_2D | // Set the graphics mode to Mode 5
DISPLAY_BG2_ACTIVE | // Enable BG2 for display
DISPLAY_BG3_ACTIVE); //Enable BG3 for display
/* Set the video mode on the sub screen. */
videoSetModeSub(MODE_5_2D | // Set the graphics mode to Mode 5
DISPLAY_BG3_ACTIVE); // Enable BG3 for display
}
There are nine VRAM banks in total on the Nintendo DS. See Table 5.2, “VRAM Bank Information” for details about them. Our 16bit background images take up 128KB of memory each. Thus, each background has to have one whole VRAM bank assigned to it. Not all VRAM banks can be used for all purposes, however. Refer to Appendix A, for more detailed information.
Table 5.2. VRAM Bank Information
| VRAM Bank | Control Register Address | Control Register | VRAM Bank Size |
|---|---|---|---|
| VRAM_A | 0x04000240 | VRAM_A_CR | 128KB |
| VRAM_B | 0x04000241 | VRAM_B_CR | 128KB |
| VRAM_C | 0x04000242 | VRAM_C_CR | 128KB |
| VRAM_D | 0x04000243 | VRAM_D_CR | 128KB |
| VRAM_E | 0x04000244 | VRAM_E_CR | 64KB |
| VRAM_F | 0x04000245 | VRAM_F_CR | 16KB |
| VRAM_G | 0x04000246 | VRAM_G_CR | 16KB |
| VRAM_H | 0x04000248 | VRAM_H_CR | 32KB |
| VRAM_I | 0x04000249 | VRAM_I_CR | 16KB |
libnds helps us once again by provide a nice <glossaryterm>API</glossaryterm> for accessing the affine transformation matrix of a particular affine background. libnds provides access to a background's affine transformation matrix through four variables. Figure 5.2, “libnds Affine Background API” shows the names of these variables and which part of the affine transformation matrix they align with.
What we'll do now is add three backgrounds. We'll put a splash screen on the top physical screen, a starfield on the bottom physical screen, and a planet placed atop the starfield background. To do this, we'll use SUB_BG3 (although we could use SUB_BG2) for the splash screen and both backgrounds 2 and 3 on the main screen for the planet and starfield respectively. In order to make sure the planet shows up above the starfield as opposed to below it, we give the planet a priority number less than that of the starfield's priority number. Relatively lower priority numbers place backgrounds relatively above other backgrounds. There are only four priority numbers per graphics engine that we can assign to backgrounds (priority numbers 0-3).
We'll now use that nice API libnds provides us for both the
background control registers and the affine transformation
matrix. Let's proceed to make a function called
which will set up our affine backgrounds. Explanations of what
is going on is the comments.
initBackgrounds
void initBackgrounds() {
/* Set up affine background 3 on main as a 16-bit color background */
BG3_CR = BG_BMP16_256x256 |
BG_BMP_BASE(0) | // The starting place in memory
BG_PRIORITY(3); // A low priority
/* Set the affine transformation matrix for the main screen background 3
* to be the identity matrix.
*/
BG3_XDX = 1 << 8;
BG3_XDY = 0;
BG3_YDX = 0;
BG3_YDY = 1 << 8;
/* Place main screen background 3 at the origin (upper left of the screen)
*/
BG3_CX = 0;
BG3_CY = 0;
/* Set up affine background 2 on main as a 16-bit color background */
BG2_CR = BG_BMP16_128x128 |
BG_BMP_BASE(8) | // The starting place in memory
BG_PRIORITY(2); // A higher priority
/* Set the affine transformation matrix for the main screen background 3
* to be the identity matrix.
*/
BG2_XDX = 1 << 8;
BG2_XDY = 0;
BG2_YDX = 0;
BG2_YDY = 1 << 8;
/* Place main screen background 2 an interesting place
*/
BG2_CX = -(SCREEN_WIDTH / 2 - 32) << 8;
BG2_CY = -32 << 8;
/* Set up affine background 3 on sub as a 16-bit color background */
SUB_BG3_CR = BG_BMP16_256x256 |
BG_BMP_BASE(0) | // The starting place in memory
BG_PRIORITY(3); // A low priority
/* Set the affine transformation matrix for the sub screen background 3
* to be the identity matrix.
*/
SUB_BG3_XDX = 1 << 8;
SUB_BG3_XDY = 0;
SUB_BG3_YDX = 0;
SUB_BG3_YDY = 1 << 8;
/*
* Place main screen background 3 at the origin (upper left of the screen)
*/
SUB_BG3_CX = 0;
SUB_BG3_CY = 0;
}
What's up with all those "<< 8" thingies? Those are fixed point numbers. The Nintendo DS expects many values to be in varying fixed point formats. A fixed point number is simply a way of specifying multiples of fractions to represent non-integer values. For example, if we use an integer variable to store a US Dollar amount, we might initially think we can only represent whole dollar values. However, if we decide to store US pennies in the integer as opposed to whole dollar values, we can all of a sudden represent fractions of US Dollars with our integer. We can have 1.21 US Dollars by saying we have 121 pennies.
Common notation for fixed point types looks like 1.31, 1.7.24, 8.8, and etc. To interpret this notation, we read from right to left. First, we find the number of bits used to specify fractional parts. The next number will be the number of bits for whole number parts. And, if there is another number, it will usually indicate the sign of the number as being either positive or negative with one bit. Figure 5.3, “Integer variables can be used to represent fractions.” illustrates the concept of fixed point numbers.
DMA stands for Direct Memory Access. DMA allows the reading and writing of memory independently of the CPU. The Nintendo DS has special, dedicated DMA hardware to do quick and moderately effcient moving of memory. DMA is not very efficient for memory fill operations, however, as the data to fill with needs to be read once for every write. Libnds provides us with a few functions to make use of the DMA hardware in the Nintendo DS.
Whenever you have the opportunity to use DMA, you should. It is
always better to use DMA than to use a for loop to copy data.
When using DMA to copy from main memory, do not forget to flush
main memory before using DMA. The DMA doesn't use the cache
where the relevant memory may currently be stored, so flushing
to main memory guarantees that DMA sees the correct data.
Another issue to consider would be that in the middle of a DMA,
the main CPUs are prevented from using certain memory
hardware. This can
cause awkward bugs with interrupt handling. For this reason,
and may be
safer, and is not too much slower. The safest bet is always
swifastcopy and
memcopy, if you
are running into some bugs.
memset
The declaration of
from libnds is as follows.
dmaCopyHalfWords
static inline void
dmaCopyHalfWords( | uint8 channelconst void * sourcevoid * destuint32 size); |
uint8 channel
const void * source
void * dest
uint32 size
;
In our program, we will use
to
load some graphics
into memory. We use the function
dmaCopyHalfWords
instead of
dmaCopyHalfWords
because it is more explicit as to
how it is copying. We'll use the same channel (channel 3) that
the ordinary dmaCopy uses,
though. It also let's us specify
which DMA channel to use when copying. Let's start out by
writing some functions to display our backgrounds. Since we've
already set up the hardware to display the data in the desired
manner, right after the copy we will get some nice images
displayed on our screens. If we didn't set up our backgrounds
first, we'd most likely get garbage on the screen until we
eventually did set up the background control registers.
dmaCopy
/* Select a low priority DMA channel to perform our background
* copying. */
static const int DMA_CHANNEL = 3;
void displayStarField() {
dmaCopyHalfWords(DMA_CHANNEL,
starFieldBitmap, /* This variable is generated for us by
* grit. */
(uint16 *)BG_BMP_RAM(0), /* Our address for main
* background 3 */
starFieldBitmapLen); /* This length (in bytes) is generated
* from grit. */
}
void displayPlanet() {
dmaCopyHalfWords(DMA_CHANNEL,
planetBitmap, /* This variable is generated for us by
* grit. */
(uint16 *)BG_BMP_RAM(8), /* Our address for main
* background 2 */
planetBitmapLen); /* This length (in bytes) is generated
* from grit. */
}
void displaySplash() {
dmaCopyHalfWords(DMA_CHANNEL,
splashBitmap, /* This variable is generated for us by
* grit. */
(uint16 *)BG_BMP_RAM_SUB(0), /* Our address for sub
* background 3 */
splashBitmapLen); /* This length (in bytes) is generated
* from grit. */
}
The default template makefile will turn your graphic files into object files for linking into your program. Never include data as a header file.
The graphics must be in a lossless image format, such as gif,
tif, bmp, or png in order to work with the provided template
makefile. I prefer the png graphic format. Image conversion is
usually done by a program called grit. The
provided template makefile will ask grit to
convert images in the gfx folder of your
project root to a format ready for the Nintendo DS.
The provided template makefile, adapted from the default libnds
template makefile, is a good base for most all projects. It
will look in a folder called gfx (in the
same directory as the makefile) for your graphics. If any are
found, it uses a special bin2o rule to tell grit to turn your
images into .o files, according to grit rule files (with the
.grit files extension), which can be linked into your program.
grit will create a header file (.h) for your
data. The name format for them works like so: if a file is
called orangeShuttle.png the header file
will be called orangeShuttle.h. Inside
this header file will be a reference to the data in the .o,
named orangeShuttleTiles and orangeShuttlePal or
orangeShuttleBitmap, depending on how the grit file specifies
which format to convert your image into. It will also include
the length in bytes of the data references as
orangeShuttleTilesLen and orangeShuttlePalLen or
orangeShuttleBitmapLen.
For our project, we'll be putting the our graphic files and
grit rule files into the gfx directory and
having the makefile use grit on them.
With the emergence of the awesome and multi-platform tool, grit there is no reason to not learn how to use it. It is now a standard tool in Nintendo DS and GBA development where aforeto there was no universal method of image conversion.
To use grit, we make a grit rule file. This file has the .grit file extension an contains important information telling grit how we want our image converted. You can learn about how grit works by reading its built-in help by running the command grit with no parameters. I've also provided commented grit rule files for all images we will use in this manual. These make good examples and can help you get the hang of using grit.
For more information regarding grit, you can visit the project homepage and read the grit manual at http://www.coranac.com/projects/grit/ and http://www.coranac.com/man/grit/html/grit.htm respectively. You can also download the latest version of grit from the grit project homepage.
Let's now put these functions into our main() function to get everything working together.
#include <nds.h>
#include "starField.h"
#include "planet.h"
#include "splash.h"
/* Other functions we've made are here. */
int main() {
/* Turn on the 2D graphics core. */
powerON(POWER_ALL_2D);
/*
* Set up interrupts.
*
* We don't really get into what these do exactly at this point in the
* manual, but we still need to do them for now.
*/
irqInit();
irqEnable(IRQ_VBLANK);
/* Configure the VRAM and background control registers. */
lcdMainOnBottom(); // Place the main screen on the bottom physical screen
initVideo();
initBackgrounds();
/* Display the backgrounds. */
displayStarField();
displayPlanet();
displaySplash();
return 0;
}
Check over your code, referring to the included examples if
needed. Make sure you have the graphics files and the grit rule
files in the gfx directory in your project
directory. Bring up the command line and set your current
working directory to the directory which contains the makefile
for your project. Type make and if all goes
well, you'll have good success. See Figure 5.4, “The program should look like this when run.”. Copy your program to your DS
using the method you have chosen.
Table of Contents
No, sprites are not magical fairies. It is a term in 2D graphics programming which refers to an image or animation. The Nintendo DS has dedicated hardware for dealing with sprites. This makes the system very useful for 2D. Most gaming systems do not have a 2D core, and all sprites and other 2D graphics have to be handled manually by painting 2D images on the side of a quad within 3D space.
The OAM manages all the sprites. This is an immense job, most of it done behind the scenes for us. After using it, you might think it magical. It's not as magical as it seems, however. OAM stands for Object Attribute Memory. It is the place in memory we use to keep track of and control our sprites. The OAM works with a SpriteEntry and a SpriteRotation struct to manage the attributes of our sprites.
On the Nintendo DS, we can have up to 128 sprites. Only 32 of those sprites can be affine transformed (rotated, scaled, skewed, etc.). We are also only allowed to address 1024 different tiles to comprise all of our sprites, per engine. Sprites can be made of tiles that use 16 colors or of ones that use 256 colors. Tiles that use 256 colors are twice as large as tiles that use only 16 colors. Another advantage of using 16-color sprites is the ability to use 16 different palettes. When using 256-color sprites, each sprite (and tile) must use the same palette. When using 16-color sprites, we can have one sprite use one 16-color palette and another sprite use a different 16-color palette, even though both use the same tile data. Games often use this trick to make different colored enemies that look the same. The sprites still use the same tile data, but they use different palettes.
Sprites are broken into 8x8 pixel pieces. This is called tiling. When drawn to screen, the hardware pieces these tiles together, like a puzzle where the pieces have no distinguishing edges. There are two ways to tile sprites, 1D and 2D. In a 2D layout, the sprite memory is treated like a giant image from which sprite tiles are obtained by making a box the size of the sprite and placing it over the giant image. In a 1D layout, the sprites are layed out in a linear fashion, as discussed in Figure 6.1, “ The upper text shows information as it would be on a non-tiled background. The lower text shows the same data, tiled, for use in tiled graphic modes. ”.
The conversion process is very similar to that for backgrounds.
We simple make grit rule files that tell grit how we want our
images converted, and it generates a .o and a header file for
us. The grit rule files and image files go into the same place
as background images, the gfx> folder.
Figure 6.1. The upper text shows information as it would be on a non-tiled background. The lower text shows the same data, tiled, for use in tiled graphic modes.
const u16 data[] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F,
0x2020, 0x2020, 0x2020, 0x2020, 0x2020, 0x2020, 0x2020, 0x2020,
0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F,
0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040, 0x4040,
0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F,
0x6060, 0x6060, 0x6060, 0x6060, 0x6060, 0x6060, 0x6060, 0x6060,
0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F,
0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080, 0x8080,
0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F,
0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0,
0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF,
0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0,
0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF,
0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0,
0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF};
const u16 data[] = {
0x0000, 0x0000, 0x0000, 0x0000, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F,
0x2020, 0x2020, 0x2020, 0x2020, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F,
0x4040, 0x4040, 0x4040, 0x4040, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F,
0x6060, 0x6060, 0x6060, 0x6060, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F,
0x0000, 0x0000, 0x0000, 0x0000, 0x0F0F, 0x0F0F, 0x0F0F, 0x0F0F,
0x2020, 0x2020, 0x2020, 0x2020, 0x2F2F, 0x2F2F, 0x2F2F, 0x2F2F,
0x4040, 0x4040, 0x4040, 0x4040, 0x4F4F, 0x4F4F, 0x4F4F, 0x4F4F,
0x6060, 0x6060, 0x6060, 0x6060, 0x6F6F, 0x6F6F, 0x6F6F, 0x6F6F,
0x8080, 0x8080, 0x8080, 0x8080, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F,
0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF,
0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF,
0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF,
0x8080, 0x8080, 0x8080, 0x8080, 0x8F8F, 0x8F8F, 0x8F8F, 0x8F8F,
0xA0A0, 0xA0A0, 0xA0A0, 0xA0A0, 0xAFAF, 0xAFAF, 0xAFAF, 0xAFAF,
0xC0C0, 0xC0C0, 0xC0C0, 0xC0C0, 0xCFCF, 0xCFCF, 0xCFCF, 0xCFCF,
0xE0E0, 0xE0E0, 0xE0E0, 0xE0E0, 0xEFEF, 0xEFEF, 0xEFEF, 0xEFEF};
Sprites have three attribute variables associated with them. With them, sprites can spin and flip and mosaic and perform all sorts of fun hardware effects. Each attribute is multi-purpose and requires some amount of bit twiddling and masking to achieve the affects we want. That is, it would if it weren't for this nice SpriteEntry union which libnds provides for us. In most cases, we will use this union to do what we'd like to with our sprites, but there are special cases where this is not possible.
We will cover how to update, initialize, and rotate using bit twiddling methods, as libnds doesn't yet provide ways of doing this otherwise. libnds provides the means to do other things that we'll be doing with sprites though, like move them, we'll be doing. We'll use the libnds API wherever possible. Let's write some functions to do each of those, in that order.
I provide you with a header file, located with the acompanying
source at
code/chapter_6-sprites/include/sprites.h
that contains a collection of sprite related function
declarations. We'll be working on the definitions for each of
these functions together in a new file called
sprites.cpp. Our first step will be to
create a that new source code file. Put a new file called
sprites.cpp into the
source folder of your project directory.
Instead of keeping track of things directly in the OAM, we'll
manage a copy of the OAM in memory that we will copy over every
frame using . The
reason we do this is because writes to OAM are locked during
all times but during vblank. We will wait for vblank to occur
before copying our copy of the OAM into the actualy OAM.
updateOAM
Updating the OAM is very straightforward. We do this by copying our copy of the OAM to the real OAM. Our copy of the OAM might still be stuck in the cache and may not have made it into main memory yet, so we first flush local memory (a must whenever performing a DMA operation) to ensure that DMA sees the correct data. The data we want to copy might be stuck in cache still and may not have made it into main memory yet. Then, we tell the OAM to look into the tOAM instance we will create later for information about each one of our sprites.
void updateOAM(tOAM * oam) {
DC_FlushAll();
dmaCopyHalfWords(SPRITE_DMA_CHANNEL,
oam->spriteBuffer,
OAM,
SPRITE_COUNT * sizeof(SpriteEntry));
}
The first thing we do when initializing the OAM is to clear all
the sprite data in the OAM. After that, we'll make a call to
our afore written function.
updateOAM
void initOAM(tOAM * oam) {
/*
* For all 128 sprites on the DS, disable and clear any attributes they
* might have. This prevents any garbage from being displayed and gives
* us a clean slate to work with.
*/
for (int i = 0; i < SPRITE_COUNT; i++) {
oam->spriteBuffer[i].attribute[0] = ATTR0_DISABLED;
oam->spriteBuffer[i].attribute[1] = 0;
oam->spriteBuffer[i].attribute[2] = 0;
}
for (int i = 0; i < MATRIX_COUNT; i++) {
/* If you look carefully, you'll see this is that affine trasformation
* matrix again. We initialize it to the identity matrix, as we did
* with backgrounds
*/
oam->matrixBuffer[i].hdx = 1 << 8;
oam->matrixBuffer[i].hdy = 0;
oam->matrixBuffer[i].vdx = 0;
oam->matrixBuffer[i].vdy = 1 << 8;
}
/* Be sure to wait for vblank before trying to update the OAM. */
swiWaitForVBlank();
updateOAM(oam);
}
Let's get to spinning. This is a bit more difficult than what we've done before, but still fun. It's nice to not have to make a separate sprite for each rotation position the sprite will be presented in. However, doing so may be necessary for some applications as the Nintendo DS rotation won't always look as pretty as if the rotations had been done elsewhere.
The libnds's sin and cos lookup tables employ a 512 degree system. Humans usually use a 360 degree system or radians. The angle we'll work with in this function will be one that is part of the 512 degree system. You'll have to convert your radians or weird 360 degree value (Seriously, why 360? Silly Babylonians.) to the 512 degree system before using this function in order to see the results you are expecting.
We have to use a transformation derived from our time spent playing with an affine transformation matrix. The sprite's affine transformation matrix is used slightly differently from the background affine transformation matrix. If you have a background in linear algebra, I’d recommend reading up on this portion of the hardware at http://www.coranac.com/tonc/text/affine.htm.
void rotateSprite(SpriteRotation * spriteRotation, u16 angle) {
s16 s = SIN[angle & SPRITE_ANGLE_MASK] >> 4;
s16 c = COS[angle & SPRITE_ANGLE_MASK] >> 4;
spriteRotation->hdx = c;
spriteRotation->hdy = s;
spriteRotation->vdx = -s;
spriteRotation->vdy = c;
}
Up until now, we haven't used the fancy SpriteEntry union included in libnds. It allows us to avoid thinking about bit twiddling and masking in most cases. In the case of showing and hiding sprites, we still need to be thinking about these bits due to an oddity in the Nintendo DS hardware: the hiding bit of a sprite (bit 9 of sprite attribute 0) is also the double bound bit of a sprite if the sprite is an affine sprite (bit 8 of sprite attribute 0). Follow along with the comments and code as we formulate a solution to writing a function which shows and hides all kinds of sprites.
void setSpriteVisibility(SpriteEntry * spriteEntry, bool hidden, bool affine,
bool doubleBound) {
if (hidden) {
/*
* Make the sprite invisible.
*
* An affine sprite cannot be hidden. We have to turn it into a
* non-affine sprite before we can hide it. To hide any sprite, we must
* set bit 8 and clear bit 9. For non-affine sprites, this is a bit
* redundant, but it is faster than a branch to just set it regardless
* of whether or not it is already set.
*/
spriteEntry->isRotoscale = false; // Bit 9 off
spriteEntry->isHidden = true; // Bit 8 on
} else {
/* Make the sprite visible.*/
if (affine) {
/* Again, keep in mind that affine sprites cannot be hidden, so
* enabling affine is enough to show the sprite again. We also need
* to allow the user to get the double bound flag in the sprite
* attribute. If we did not, then our sprite hiding function would
* not be able to properly hide and restore double bound sprites.
* We enable bit 9 here because we want an affine sprite.
*/
spriteEntry->isRotoscale = true;
/* The double bound flag only acts as the double bound flag when
* the sprite is an affine sprite. At all other times, it acts as
* the sprite invisibility flag. We only enable bit 8 here if we want
* a double bound sprite. */
spriteEntry->rsDouble = doubleBound;
} else {
/* Bit 9 (the affine flag) will already be off here, so we don't
* need to clear it. However, bit 8 (the sprite invisibility flag)
* will need to be cleared. */
spriteEntry->isHidden = false;
}
}
}
Now for some real fun. Moving sprites in hardware, and not having to worry about clipping, buffers, or anything, is such a wonderful feeling. To move a sprite, we simply change the SpriteEntry properties posX and posY. This is a case where we don't have to think much about the underlying arrangement of this information. The libnds union SpriteEntry describes the organization of data to the compiler for us and the compiler then figures out the best way to operate on that data; the compiler does the bit twiddling and masking for us. Since this is so simple, we don't even need to write a function to do it for us. So just take note of what we do here and remember it for later; you don't have to write a function for it, but if you want to I'd recommend making it an inline function.
/* This is what we'd do if we wanted to move a sprite. */
spriteEntry->posX = 0;
spriteEntry->posY = 0;
The ability to set a sprites priorty is essential when dealing with multiple sprites, as we will be doing. As such, we'll now discuss sprite priorities and how to set them.
A sprite, like a background, has a priorty which determines how the sprite will appear with respect to other sprites and to backgrounds. A sprite with the same number priorty as a background will appear above it. A sprite with a lower priorty number than another sprite will appear above that other sprite. There are four priorities per graphics engine available for sprites, similar to background priorities.
To set the sprite priority, we'll simply set the SpriteEntry property objPriority to one of four different values, depending on which priority we wish to assign: OBJPRIORITY_0, OBJPRIORITY_1, OBJPRIORITY_2, or OBJPRIORITY_3. The following code listing shows an example of what this looks like.
spriteEntry->objPriority = OBJPRIORITY_3;
Now that our sprites.cpp file is finished, let's get on to how
sprites are stored in memory, how to load them, and so forth.
So put away your sprites.cpp file into the source directory of
your home folder and let's get back into our
main.cpp file.
We'll need to make a place for our sprite data to live in VRAM. Since we will be using sprites on the main graphics engine, we can use VRAM bank E for our sprites. VRAM bank E is smaller than the other VRAM banks we've dealt with so far, as it is only 64 KB in size. However, this is more than enough to store 1024 unique 16-color tiles.
In our , we need
to map VRAM bank E for use with sprites on the main graphics
engine. Then, we need to tell the main engine to enable sprites
of the tiling style we want. We will use 1D tiled sprites. The
resulting initVideo function, post-modifications, is presented
below .
initVideo
void initVideo() {
/*
* Map VRAM to display a background on the main and sub screens.
*
* The vramSetMainBanks function takes four arguments, one for each of the
* major VRAM banks. We can use it as shorthand for assigning values to
* each of the VRAM bank's control registers.
*
* We map banks A and B to main screen background memory. This gives us
* 256KB, which is a healthy amount for 16-bit graphics.
*
* We map bank C to sub screen background memory.
*
* We map bank D to LCD. This setting is generally used for when we aren't
* using a particular bank.
*
* We map bank E to main screen sprite memory (aka object memory).
*/
vramSetMainBanks(VRAM_A_MAIN_BG_0x06000000,
VRAM_B_MAIN_BG_0x06020000,
VRAM_C_SUB_BG_0x06200000,
VRAM_D_LCD);
vramSetBankE(VRAM_E_MAIN_SPRITE);
/* Set the video mode on the main screen. */
videoSetMode(MODE_5_2D | // Set the graphics mode to Mode 5
DISPLAY_BG2_ACTIVE | // Enable BG2 for display
DISPLAY_BG3_ACTIVE | // Enable BG3 for display
DISPLAY_SPR_ACTIVE | // Enable sprites for display
DISPLAY_SPR_1D // Enable 1D tiled sprites
);
/* Set the video mode on the sub screen. */
videoSetModeSub(MODE_5_2D | // Set the graphics mode to Mode 5
DISPLAY_BG3_ACTIVE); // Enable BG3 for display
}
We'll be using the same memory alignment (boundary) as the GBA
uses for our sprites. Tile VRAM addresses must be aligned to 32
bytes. If you feel shorted by this, since you can't use all
1024 addressable tiles when using 256 color tiles, for
instance, then you can look up how to use other alignments at
http://nocash.emubase.de/gbatek.htm#dsvideoobjs.
You'll have to set REG_DISPCNT (via videoSetMode) with a value
defined in libnds/include/nds/arm9/video.h
akin to DISPLAY_SPR_1D_SIZE_XXX (the default, and the method
the GBA and we use, is DISPLAY_SPR_1D_SIZE_32).
To compute the address to copy tiles to, we basically need to know two things: the memory alignment we are using and the tile numbers we want to assign data to. Using the formula from Martin Korth's GBATEK, "TileVramAddress = TileNumber * BoundaryValue", and libnds's SPRITE_GFX define we can compute the address of any tile as follows.
static const int BOUNDARY_VALUE = 32; /* This is the default boundary value
* (can be set in REG_DISPCNT) */
static const int OFFSET_MULTIPLIER = BOUNDARY_VALUE /
sizeof(SPRITE_GFX[0]);
uint16 * tileVramAddress = &SPRITE_GFX[shuttle->tileIdx *
OFFSET_MULTIPLIER];
We usually want to copy more than one tile into vram at a time, however. Luckily, when converting images to sprites with grit, it will tell us the length in bytes of our tile data for that sprite. After we have the length in bytes, we can use dmaCopyHalfwords (which uses byte lengths for copying) to copy the tile data into VRAM. We can also calculate how many tiles an image uses from its length in bytes by diving the length in bytes by how many bytes a tile takes up. In our case, as we'll be using 16-color tiles, a tile (8x8 pixels) takes up 32 bytes.
Now, to see a sprite in action. Let's load in the orangeShuttle
graphic and the moon graphic. Make a new function called . Place
it after the initSprites
function. Make sure to include
initBackgroundsorangeShuttle.h and
moon.h now. They contain information about
our sprites as generated by grit.
I've also create a new struct type called "SpriteInfo". This struct contains information about sprites that aren't explicitly contained in the SpriteEntry struct. We'll be using it to help us manage our information about sprites better.
Procedure 6.1. To Create a Sprite
We'll begin by filling in our SpriteInfo struct for the sprite. Each sprite will have its own SpriteInfo struct. The first thing we need to do is assign an OAM ID to the sprite. This number will help us keep track of which OAM entry our sprite is associated with. We'll also use it to compute other offsets, although we don't have to use it for that purpose in many cases.
Assign the width, height, and angle of the sprite.
Select an OAM entry to associate with this sprite.
Configure attribute 0.
Configure attribute 1.
Configure attribute 2.
Copy tile data to VRAM.
Copy the palette data to VRAM.
We'll just follow these steps twice when writing the initSprites function for our sprites. You can follow along with the comments and the code below.
void initSprites(tOAM * oam, SpriteInfo *spriteInfo) {
/* Define some sprite configuration specific constants.
*
* We will use these to compute the proper index into memory for certain
* ti