Programming NES games in C article

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Programming NES games in C article

Post by Shiru » Wed Jan 04, 2012 9:03 am

Last edited by Shiru on Sun Jan 29, 2012 2:02 am, edited 2 times in total.

User avatar
cpow
NESICIDE developer
Posts: 1097
Joined: Mon Oct 13, 2008 7:55 pm
Location: Minneapolis, MN
Contact:

Re: Programming NES games in C article

Post by cpow » Wed Jan 04, 2012 10:23 am

Shiru wrote:Read the article
Shiru, very nice article! One thing I'd added to NESICIDE is the ability to see mixed-mode source during debugging. That helps -- I think, anyway -- people that want to develop in C see exactly what the C compiler generates for any particular C source line of code. Example:

Large screenshot

Also, this statement:
Shiru wrote:The problem is that there are no comfortable debuggers for C code compiled into 6502 assembly around (yet) - ones that allow to put breakpoints on random C lines and see what the variables contain at the moment. Usually you only have an assembly level debugger in some emulators, and it is not very helpful with compiled code.
is not really true. NESICIDE can do that. I've been stepping through AlterEgo at the C level for quite a while now. Thank you for such a polished and useful example of programming for NES in C.

mrm78
Posts: 7
Joined: Wed Oct 05, 2011 10:24 am

Post by mrm78 » Wed Jan 04, 2012 10:54 am

@Shiru :D nice chasing game

User avatar
jwdonal
Posts: 719
Joined: Sat Jun 27, 2009 11:05 pm
Location: New Mexico, USA
Contact:

Post by jwdonal » Wed Jan 04, 2012 4:27 pm

Cool game dude! Awesome article as well! Would love to read/play more. :)

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Wed Jan 04, 2012 7:57 pm

cpow, I've changed that line in the text. It is great that NESICIDE has C level debugger. I can't really use it, though, because my PC is too slow to run the built emulator fullspeed (it is like 50% now), so I have to wait to the next PC upgrade.

mic_
Posts: 922
Joined: Thu Oct 05, 2006 6:29 am

Post by mic_ » Thu Jan 05, 2012 2:29 am

Nice info and cleanly written.
Do you know if CC65 handles cross-bank calls well (linking symbols that have been compiled into separate banks)? This was quite a hassle with SDCC (Z80) and I ended up having to write some custom tools for it, and it still didn't work perfectly.

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Thu Jan 05, 2012 2:54 am

I don't know, haven't get to this point yet - so far all my NES projects were NROM. I only read that you can put functions and data in different banks (so they are compiled into the same addresses), but have to switch banks before accessing them by yourself.

User avatar
Bregalad
Posts: 7951
Joined: Fri Nov 12, 2004 2:49 pm
Location: Chexbres, VD, Switzerland

Post by Bregalad » Thu Jan 05, 2012 4:00 am

Congratulations, that was interesting.

Something I don't understand so well :
Due to very limited NES resources, such as CPU speed, RAM and ROM size, writting a proper, clean C code isn't very effective. To make it faster and shorter you have to do things that otherwise aren't considered acceptable. They are disable some of C advantages, making the code more low level and less structured.

There are suggestions that will make your code more effective, but certainly less readable:
....
Then you write C code that is less portable and less readable, and more low level. I don't have anything against it but doesn't this kill the purpose of using C instead of assembly ?
Useless, lumbering half-wits don't scare us.

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Thu Jan 05, 2012 4:29 am

It is a bit low level this way, but certainly not down to the assembly level - it is still rather high level.

Random example, you have a level map that is larger than 256 bytes, lets say 32x32, and need to get a value from it using two 8-bit vars, mx and my. So, for C code:

Code: Select all

n=map[(my<<5)+mx]
Assembly code counterpart is something like this:

Code: Select all

 lda my
 sta ptr_l
 lda #0
 sta ptr_h
dup 5
 asl ptr_l ;<<5
 rol ptr_h
edup
 lda ptr_l
 clc
 adc mx
 sta ptr_l
 lda ptr_h
 adc #0
 sta ptr_h
 lda ptr_l
 clc
 adc #<map
 sta ptr_l
 lda ptr_h
 adc #>map
 sta ptr_h
 ldy #0
 lda [ptr_l],y
One line of the code is faster to write and easier to mantain than 20+. You probably could optimize the assembly version as it was written off the top of my head, but certainly not down to a single line.

User avatar
clueless
Posts: 498
Joined: Sun Sep 07, 2008 7:27 am
Location: Seatlle, WA, USA

Post by clueless » Thu Jan 05, 2012 6:46 am

I can see using a hybrid model of C and Assembly. Consider an RPG, where the battle logic is better expressed in C, but code the rest (NMI handler, tile display logic, sound engine, etc...) in asm.

In the past Tepples has stated that the Koei simulation games were written in C, and that is probably why they seem sluggish. I bet that we could do better.

I seriously considered making some sort of an RPG as my next nesdev project, and coding part of it in C, but I lack the time to take on such a project right now. :(

Shiru
Posts: 1161
Joined: Sat Jan 23, 2010 11:41 pm

Post by Shiru » Thu Jan 05, 2012 7:18 am

That's how it is done in my NES games - C code only works with hardware through special functions written in assembly. So NMI handler, tile updater, sound code is written in assembly, and game logic in C.

You'll run into the other problem with RPG battle logic in C - the resulting code will be huge, so the problem with bankswitching have to be solved somehow.

User avatar
Bregalad
Posts: 7951
Joined: Fri Nov 12, 2004 2:49 pm
Location: Chexbres, VD, Switzerland

Post by Bregalad » Thu Jan 05, 2012 7:33 am

Shiriu, your example just convicted me the usefulness of C language !
Useless, lumbering half-wits don't scare us.

User avatar
thefox
Posts: 3141
Joined: Mon Jan 03, 2005 10:36 am
Location: Tampere, Finland
Contact:

Post by thefox » Thu Jan 05, 2012 7:44 am

mic_ wrote:Do you know if CC65 handles cross-bank calls well (linking symbols that have been compiled into separate banks)? This was quite a hassle with SDCC (Z80) and I ended up having to write some custom tools for it, and it still didn't work perfectly.
Not sure what exactly you're asking here. As long as the runtime functions (pusha/popa etc) are placed in a fixed bank it should work OK. The switching itself has to be done manually of course.
Download STREEMERZ for NES from fauxgame.com! — Some other stuff I've done: fo.aspekt.fi

tepples
Posts: 22054
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Post by tepples » Thu Jan 05, 2012 8:32 am

mic_ wrote:Nice info and cleanly written.
Do you know if CC65 handles cross-bank calls well (linking symbols that have been compiled into separate banks)?
It doesn't happen automagically, but depending on the calling convention used, you may be able to write a bunch of stubs that make cross-bank calls using a trampoline in the fixed bank.

mic_
Posts: 922
Joined: Thu Oct 05, 2006 6:29 am

Post by mic_ » Thu Jan 05, 2012 11:08 am

Do you know if CC65 handles cross-bank calls well
Not sure what exactly you're asking here.
Let's say you split your code/data into separate banks which are compiled and linked separately (to avoid having duplicates of the same stuff for different combinations of banks) and then combined into a .nes file.

So e.g.

bank0.c -> bank0.obj -> bank0.bin
bank1.c -> bank1.obj -> bank1.bin
...
header + bank0.bin + bank1.bin + ... -> game.nes

If bank0 wants to call a function in bank1, can you tell CC65 to resolve the address of that function without actually putting a copy of the function in bank0 as well? Using hardcoded addresses is a PITA, and using a proxy function at a fixed address to delegate calls isn't really that nice either IMO.

With SDCC I ended up writing a tool that parsed the .SYM files generated by the linker and it would output a header file with named function pointers for any given bank that other banks could use. It didn't work when there were cross-dependencies (bank X and bank Y both wanted to call eachother), so in some instances I still had to use hardcoded addresses.

Post Reply