Twin Dragons - open-source platformer written in Millfork

A place where you can keep others updated about your NES-related projects through screenshots, videos or information in general.

Moderator: Moderators

Post Reply
User avatar
Garydos
Posts: 5
Joined: Mon Sep 30, 2019 10:26 am

Twin Dragons - open-source platformer written in Millfork

Post by Garydos » Sat Nov 16, 2019 1:33 pm

Hey everyone,

So I made a post in the NESDev forums a while back, and said I would make a small platformer game to test out how viable making a full NES homebrew project in Millfork would be. I've come back today to let everyone know that for the most part, it is viable! (With some caveats as well, however.)

Image

Click here for the link to the github repository, and click here for an up-to-date version of the compiled .nes file

I'll talk a bit about what it's like building a project like this in Millfork (rather than assembly) later in the post, but for now I'll list out some of the key features in this game I was able to implement:
  • - Full 2D platformer physics (collision detection, sub-pixel movement, etc...)
    - Standard game system (loading levels, preparing gamestates, etc...)
    - 16x16 pixel metatile system for background tiles
    - Run-length encoding compressed maps
    - Left and right scrolling system with no screen-edge glitches (except when game is slowed down)
    - Separate game and NMI threads for handling slowdown properly
    - Generalized animation system
    - Sprite object manager system
    - Enemy manager system (that I will probably change to a general object manager system)
    - Famitone5 sound system (Famitone2 compatible)
    - Level/map editing using the Tiled map editor!


The last feature is one that I'm really excited about. I've made a Tiled python plugin script to quickly and easily make/export maps to the necessary file format for use in this engine, but you could also use this as a base to implement your own file formats for Tiled to export to.

In addition, I've tried to make this game fairly easy to add on to, whether it be adding music/sfx, animations, enemies, etc..., I have provided READMEs and instructions on how to easily add in new objects and hook them up to the game engine. There's still some documentation that I have to fill out, and I will definitely be refactoring some of the code that's a bit cumbersome to add on to, but for the most part the current base engine can be fairly quickly added on to. To prove this, take a look at the version from 2 days ago and the latest version. I was able to add in a new enemy, finish up the logic for an existing enemy, and make a whole new map in just a little over a day. As I said before, I do still plan to work on the engine and improve its use/efficiency, so if you plan to tinker around with it, keep in mind that I may change things in the future.
So anyways, onto the Millfork stuff.

A Few Words On Millfork:

For those of you who don't know what Millfork is, it's a higher-level programming language for 6502 and other processors. I'd recommend reading up about it on its website, but the gist is that it's meant to be a C-like language for old processors that is somewhere in-between assembly and high-level languages like C.

The reason I chose to use it for this project is that I wanted the code to be more readable than assembly, while still retaining most of the performance. With that in mind, here are the pros and cons I've encountered that are relevant to developing NES homebrew in this language:

First, the pros:
  • - Easy to read/maintain : The biggest advantage of anything other than assembly is, of course, that it's not assembly. In Millfork's case especially, its C-like syntax makes it a breeze to learn if you're familiar with any modern C-like language.
    - Still in active development : Millfork receives regular updates, and bug reports are fairly quickly addressed by the lead developer. Whenever I encountered a compiler bug and reported the issue on the main github page, the lead dev always fixed it super quickly.
    - Fairly efficient (in terms of speed) : I haven't ported this game to any other languages to do an official benchmark, but for the most part Millfork's optimizer does a good job in translating your code to decently quick machine code. Much of the slowdown in Twin Dragons may be attributed to my own programming, so it's kind of hard to guage, but the fact that it doesn't slow down at all in most cases makes me believe that most projects won't have any speed issues related to the language itself.

Now, the cons:
  • - Occasional compiler bugs : Since Millfork is still in active development, you may come across bugs in your game that occur by no fault of your own. This means you'll probably still have to read the outputted assembly using a debugger (like in fceux) in order to track down bugs. During the development of this game/engine, I ran into a couple of game-breaking bugs that I managed to track down to the compiler and not my own coding. I reported these bugs to the Millfork dev and they fixed them fairly quickly, so as long as you're willing to go bug hunting in assembly this shouldn't be ~too~ big an issue, but that also sadly means that I can't really recommend Millfork for beginners until it becomes stable.
    - Inefficient in terms of space : This may be due to my own programming faults, but I found that by the end of this project almost an entire PRGROM chip was taken up by just the code. This left me with only enough room for 3 levels, which is why the demo ends at level 3. Again, I don't want to attribute this solely to Millfork's compiler/optimizer, but it's something to keep in mind.
So in summary, after spending ~1-2 months working on a project entirely in Millfork, I'd rate the experience as being a whole lot better than programming in assembly but with a few pitfalls you need to watch out for. I recommend this for people who already have some exposure to assembly, but don't want to deal with making a big project entirely in assembly.

I plan to continue to add to this project with more refactoring/general improvements, with my next big milestone being to port this over to a different mapper, most likely UxROM. So if you want to use this as a base to create your own project (which is what I actually intended!), keep in mind that you may want to just read over the code but hold off on modifying it until I release a more stable version.

Thanks for reading, and I hope this was useful to you!

User avatar
pubby
Posts: 549
Joined: Thu Mar 31, 2016 11:15 am

Re: Twin Dragons - open-source platformer written in Millfor

Post by pubby » Sat Nov 16, 2019 11:32 pm

That's interesting! Thanks for posting.

I'm curious what the generated assembly looks like. Can you post an example?

User avatar
Garydos
Posts: 5
Joined: Mon Sep 30, 2019 10:26 am

Re: Twin Dragons - open-source platformer written in Millfor

Post by Garydos » Sun Nov 17, 2019 8:48 pm

pubby wrote:That's interesting! Thanks for posting.

I'm curious what the generated assembly looks like. Can you post an example?
Whoops, I meant to say "assembled code" rather than assembly. The compiler itself can't generate compilable assembly, but it can export debug files for various emulators (including fceux), which is what I used to read the assembled output.

Upon reading through more of the assembled code, it seems that a lot of the space issues can be attributed to my programming... I have some optimizing to do, but in the mean time here's an example of a function that is fairly well optimized:

Millfork Code

Code: Select all

void blank_nmi_no_sprites() {
    //do nothing except basic updates (also clear the sprites)
    init_sprites()
    ppu_oam_dma_write(oam_buffer.addr.hi)
    ppu_set_scroll(0,0)
    FamiToneUpdate()
    ppu_ctrl = %10010000
    ppu_mask = %00011110
    new_frame = false
}
Compiled Code (viewed through fceux's debugger)

Code: Select all

blank_nmi_no_sprites:
 00:87DE:20 AC 9D  JSR $9DAC
 00:87E1:A9 02     LDA #$02
 00:87E3:8D 14 40  STA OAM_DMA = #$02
 00:87E6:A9 00     LDA #$00
 00:87E8:A2 00     LDX #$00
 00:87EA:20 61 9B  JSR $9B61
 00:87ED:20 47 C1  JSR $C147
 00:87F0:A9 90     LDA #$90
 00:87F2:8D 00 20  STA segment.chrrom.heapstart = #$90
 00:87F5:A9 1E     LDA #$1E
 00:87F7:8D 01 20  STA PPU_MASK = #$1E
 00:87FA:A9 00     LDA #$00
 00:87FC:8D DE 04  STA $04DE = #$00
 00:87FF:60        RTS -----------------------------------------
This routine was the nmi routine used by the title screen and loading screen I believe.

Here's another more involved example:

Millfork

Code: Select all

inline void draw_metatile_second_column(byte metatile) {
    pointer metatile_ptr
    
    if metatile == $FF {
        //$FF is a special metatile, just a solid color
        ppu_write_data($FF)
        ppu_write_data($FF)
    }
    else {
        metatile_ptr = metatiles
        metatile_ptr += metatile << 2
        
        ppu_write_data(metatile_ptr[2])
        ppu_write_data(metatile_ptr[3])
    }
}
Assembly

Code: Select all

draw_metatile_second_column:
 00:8800:AA        TAX
 00:8801:C9 FF     CMP #$FF
 00:8803:D0 09     BNE $880E
 00:8805:A9 FF     LDA #$FF
 00:8807:8D 07 20  STA PPU_DATA = #$00
 00:880A:8D 07 20  STA PPU_DATA = #$00
 00:880D:60        RTS -----------------------------------------
.el__00291:
 00:880E:A9 37     LDA #$37
 00:8810:85 00     STA cap_phys_obj_vel$obj_ptr = #$00
 00:8812:A9 BF     LDA #$BF
 00:8814:85 01     STA $0001 = #$00
 00:8816:8A        TXA
 00:8817:0A        ASL
 00:8818:0A        ASL
 00:8819:18        CLC
 00:881A:65 00     ADC cap_phys_obj_vel$obj_ptr = #$00
 00:881C:85 00     STA cap_phys_obj_vel$obj_ptr = #$00
 00:881E:90 02     BCC .ah__00297
 00:8820:E6 01     INC $0001 = #$00
.ah__00297:
 00:8822:A0 02     LDY #$02
 00:8824:B1 00     LDA ($00),Y @ binary_search_word$search = #$00
 00:8826:8D 07 20  STA PPU_DATA = #$00
 00:8829:C8        INY
 00:882A:B1 00     LDA ($00),Y @ binary_search_word$search = #$00
 00:882C:8D 07 20  STA PPU_DATA = #$00
.fi__00292:
 00:882F:60        RTS -----------------------------------------
You may notice that there are some weird variable names in the assembly version that don't appear in the original millfork code, this is because the millfork optimizer re-uses local variables (when they're not being used) as temporary variables in order to save space. So you can basically code regular C-like functions with a few local variables and not have to worry about allocating temporary variables. For most part, that aspect of the optimizer has worked really well in my experience.

Zonomi
Posts: 60
Joined: Wed May 09, 2007 12:45 pm

Re: Twin Dragons - open-source platformer written in Millfor

Post by Zonomi » Tue Nov 19, 2019 3:04 am

So, you created a game with the same name as another one, with the same assets (I know they are free).
Image

Maybe you should change something... :roll:

User avatar
Garydos
Posts: 5
Joined: Mon Sep 30, 2019 10:26 am

Re: Twin Dragons - open-source platformer written in Millfor

Post by Garydos » Tue Nov 19, 2019 9:55 am

Zonomi wrote:So, you created a game with the same name as another one, with the same assets (I know they are free).
Image

Maybe you should change something... :roll:
Huh, I can't believe I never once googled "twin dragons nes". Well, the main goal of my project is to provide an open-source base for other projects to build upon, so this specific demo being similar to another product isn't really a big concern. I'll be mainly focusing my efforts on building up the base engine rather than the game-play specific to this game from now on though, so thanks for informing me of this.

Post Reply