WIP: Wizard of Wor

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

Moderator: Moderators

Post Reply
lidnariq
Posts: 11429
Joined: Sun Apr 13, 2008 11:12 am

Re: WIP: Wizard of Wor

Post by lidnariq »

You have about 2200 cycles per vblank (113⅔ · 20) for everything that needs to be sent to the PPU. Updating sprites (OAM DMA) takes roughly 1/4 of that. CC65 is probably adding some overhead, too.

Almost every game I've looked at has some kind of scheduler for PPU updates. Sometimes they're really minimal (e.g. some flip-screen games just change between "PPU is off, uploading new data" and "game is running")
User avatar
rainwarrior
Posts: 8731
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: WIP: Wizard of Wor

Post by rainwarrior »

tschak909 wrote:Yup, looks like that's the problem. The question I have is, is it safe to call set_vram_update(); when the PPU is on?

I need to literally update different areas of the screen, depending on different events, score change, worrior doors opening, teleport happening, etc. If I can't call set_vram_update() while ppu is active, will I need to roll my own little queuing and scheduler code for this?
I think you can call set_vram_update whenever you want, that's just setting a pointer and the actual PPU update is done by neslib's NMI handler (which will run at the next vblank). The problem is just that the NMI handler has no way of safeguarding against or helping to diagnose the "too much data takes too long" case.

I believe all the neslib functions you can't call while rendering is on are labelled as such in the header.

If you need all of those updates to happen in one frame, you could write a custom assembly NMI handler that can get that much data through in a single vblank, but the library's generic one is not efficient enough to do it.

Otherwise you can break up your updates so you only have to do some of it each frame.
na_th_an
Posts: 558
Joined: Mon May 27, 2013 9:40 am

Re: WIP: Wizard of Wor

Post by na_th_an »

The solution is making your vram_update array dynamic, fill it with what you need to change for that frame, and let the NMI handler do its magic. I'm sure you don't need to update everythign every frame, so this just works.

Reserve a big enough update buffer in BSS, such as:

Code: Select all

unsigned char update_buffer [UPDATE_LIST_SIZE*3+1];
unsigned char *ul;
Here, UPDATE_LIST_SIZE = 32 works for me (more than enough!). ul is a pointer we'll use to update the buffer.

Then your main loop would do something in the lines of:

Code: Select all

set_vram_update (update_buffer);
while (game_on) {
    ul = update_buffer; // Point to the beginning of the buffer
    ... do your game logic, write to ul, increment ul.
    *ul = NT_UPD_EOF;
    ppu_wait_frame ();
}
set_vram_update (0);
...
In your game logic, whenever you need to update something, just write everything to the update buffer through *ul, for example:

Code: Select all

*ul ++ = MSB(NTADR_A(1,19))|NT_UPD_HORZ,LSB(NTADR_A(1,19));
*ul ++ = 3;
*ul ++ = 0x74;
*ul ++ = 0x63;
*ul ++ = 0x75;
I'm sure just a number of things will fill the buffer each frame, so this will work and you won't run out of VBlank time.
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Ok. I'm already basically doing this, I'll just not try to do it in one big buffer. (I was doing 256 bytes in BSS and just creating update records for every single thing to update, and simply just changing the data...) :)

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

Re: WIP: Wizard of Wor

Post by tepples »

tschak909 wrote:I'm just curious as to how the result frames on a tube.
It depends on how the tube is calibrated. It's often joked that NTSC stands for Never The Same Color, but it can also mean Never The Same Crop. There's a diagram at "Overscan" on the wiki. For convenience and in case something happens to the wiki:
  • Danger Zone (outside Action Safe Area)
  • Action Safe Area, 256x224, (0, 8)-(255, 231)
  • PocketNES Safe Area, 240x212, (8, 16)-(247, 227)
  • Title Safe Area, 224x192, (16, 24)-(223, 215)
An LCD will probably show the Action Safe Area almost exactly. The average CRT will show somewhere between the PocketNES Safe Area and the Action Safe Area, but older CRTs or CRTs in poor shape might not show much out of the Title Safe Area or might show some of the Danger Zone.
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

oh I know the joke, all too well. :) It's especially poignant on the VCS, because you're literally generating half of the video signal yourself! :) (I have literally made black and white pixels artifact into color on the VCS on accident.) ;)

I just wanted to see how tight my tolerances are, since I do not have a toaster or a tube anymore. :)

(and yes, I have the overscan chart for the NES, I'm really bunching up against it. I just want to see how close to the cliff I actually am.)

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Code that currently opens/closes the worrior doors

Code: Select all

/**
 * set_door(player, openclose)
 * player = Player 0 (blue) or Player 1 (yellow) door.
 * openclose = 0 for open, 1 for close.
 */
void set_door(unsigned char player, unsigned char openclose)
{
  // Clear the update buffer
  clear_update_buffer();

  // Set the addresses for the two rows of tiles that make up the door
  update_buffer[0]=(player==0?MSB(NTADR_A(1,18))|NT_UPD_HORZ:MSB(NTADR_A(28,18))|NT_UPD_HORZ);
  update_buffer[1]=(player==0?LSB(NTADR_A(1,18)):LSB(NTADR_A(28,18)));
  update_buffer[2]=3;

  update_buffer[6]=(player==0?MSB(NTADR_A(1,19))|NT_UPD_HORZ:MSB(NTADR_A(28,19))|NT_UPD_HORZ);
  update_buffer[7]=(player==0?LSB(NTADR_A(1,19)):LSB(NTADR_A(28,19)));
  update_buffer[8]=3;

  // And then set the tiles for each update depending on desired door state.
  if (openclose==0 && player==0)
    {
      update_buffer[3]=0x65;
      update_buffer[4]=0x00;
      update_buffer[5]=0x00;
      update_buffer[9]=0x65;
      update_buffer[10]=0x00;
      update_buffer[11]=0x00;
    }
  else if (openclose==1 && player==0)
    {
      update_buffer[3]=0x76;
      update_buffer[4]=0x64;
      update_buffer[5]=0x64;
      update_buffer[9]=0x74;
      update_buffer[10]=0x63;
      update_buffer[11]=0x63;
    }
  else if (openclose==0 && player==1)
    {
      update_buffer[3]=0x00;
      update_buffer[4]=0x00;
      update_buffer[5]=0x66;
      update_buffer[9]=0x00;
      update_buffer[10]=0x00;
      update_buffer[11]=0x66;
    }
  else if (openclose==1 && player==1)
    {
      update_buffer[3]=0x64;
      update_buffer[4]=0x64;
      update_buffer[5]=0x77;
      update_buffer[9]=0x63;
      update_buffer[10]=0x63;
      update_buffer[11]=0x75;
    }

}
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Doors and teleports working, next, scores... but I need to make a little queuing mechanism and make the functions use it... still thinking... in the mean time, a bit of silliness:

Image
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Scoring now implemented. I had a bit of a trip trying to be a bit too clever with the algorithm,

https://www.youtube.com/watch?v=tsEfDJwiPNY

but it's there, and now I'm implementing the sprite structure, a simple array of 8 entries with 5 elements each (40 bytes) containing:

Code: Select all

/**
 * 8 objects on screen, two players and 6 enemies.
 *
 * [0] - Sprite X position
 * [1] - Sprite Y position
 * [2] - Sprite Type - (worrior, burwor, thurwor, gorwor... Bit 7 means empty.)
 * [3] - Dungeon X coordinate
 * [4] - Dungeon Y coordinate
 */
So, onward and upward. (and yes, this structure will change, i'm slowly adding what I need and then rewriting it if I need to tighten it up)

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Needed more room in my sprite chr bank, so I used NESST's remove duplicates feature. Fantastic feature indeed..in fact, both YYCHR and NESST are fantastically indispensable tools, hats off to Shiru and YY.

Now, assembling metasprites is like a twisted jigsaw puzzle. yay.

Image
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Now status is slowing down a bit, as I slog through entering tables and data for animation and start implementing the frame loop for the main part of the game.

I am currently using a very naive vram update scheduler, interleaving parts of the vram to be updated across four different frames, as it isn't critical that all of these updates happen with hair-pin precision:

Code: Select all

     // VRAM update scheduler

      a=frame_cnt&0x03;
      switch (a)
        {
        case 0:
          update_doors();
          break;
        case 1:
          update_scores();
          break;
        case 2:
          set_teleport(teleport_state);
          break;
        case 3:
          break;
        }

      // End VRAM update scheduler
I will spread the radar update into 4 sections, so that I can have enough cycles to update the radar screen (really not much to do other than just read the dungeon x and y boxes for the 6 enemy state slots, and set the tile to use accordingly, but there are 10x6 tiles to update, so I have to slice it up a bit)

If I need something better, i'll write it.

I've gone through and implemented almost all of the sprites, save for the wizard, as I am needing to decode the bitplane format used on the Astrocade, to grab the blitter object for the wizard from the ROM, as I have tried to frame grab it, like the other character objects, and as it happens, the blitter object is munged a bit to blur it, making extraction of anything other than the one visible object on the character description page difficult. Worst case, I will just #@($#@ draw it based on the blurry (#@$#@ images.

I've also implemented the animation state tables which will be used to select which metasprites to use for a given state, and have enough to where I can do a naive first pass at filling in the OAM on each frame. I believe the game uses four frames for each animation state, with two of those frames duplicated to create a sort of slight cyclical pause that is visible at slower speeds...it certainly makes it easy to cycle through the frames in the state list, as I just have to AND it off.

I've also implemented the red/blue dungeon palette swap that is needed to differentiate certain states (worluk/wizard visible, or you're now in a Worlord dungeon), as well as the Double Score win color cycling.

Slowly but surely...

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Implemented first passes of initial enemy placement, and radar. (need to re-order radar bits a bit to make it easier to select the correct tile for display.)

Image

Radar is implemented as background tiles, saw no need to waste perfectly good sprites for it.

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

A quick video status report:

https://www.youtube.com/watch?v=BLIaZFthB5o

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

Looks like enemy collision detection needs a bit of work (I need to add a bit of fudging to allow the monsters to go into the box, as well as some bias correction, but if you look at the radar output, you'll see the collision code is acting as expected)

https://www.youtube.com/watch?v=QV_Tf5Mdmf4

-Thom
tschak909
Posts: 142
Joined: Mon Jul 03, 2017 4:37 pm
Contact:

Re: WIP: Wizard of Wor

Post by tschak909 »

A little refactoring of my macros to make them saner, and the collision code became simpler to write, and the bugs went poof:

Code: Select all

#define STAMP_NUM_FIELDS     8                    // Number of fields in each stamp slot
#define STAMP_NUM_SLOTS      8                    // Number of slots in stamp structure
#define STAMP_CENTER_BIAS_X  12                   // Offset to apply to box multiply to center sprite (X)
#define STAMP_CENTER_BIAS_Y  10                   // Offset to apply to box multiply to center sprite (Y)
#define STAMP_NUM(x)         (x*STAMP_NUM_FIELDS) // Stamp Number
#define STAMP_X(x)           (STAMP_NUM(x)+0)     // Stamp Field: X pixel position
#define STAMP_Y(x)           (STAMP_NUM(x)+1)     // Stamp Field: Y pixel position
#define STAMP_TYPE(x)        (STAMP_NUM(x)+2)     // Stamp Field: Type
#define STAMP_STATE(x)       (STAMP_NUM(x)+3)     // Stamp Field: state (which frames to use).
#define STAMP_FRAME(x)       (STAMP_NUM(x)+4)     // Stamp Field: Current frame
#define STAMP_DELAY(x)       (STAMP_NUM(x)+5)     // Stamp Field: Delay
#define STAMP_FINE_X(x)      (STAMP_NUM(x)+6)     // Stamp Field: Fine X offset relative to box (not used, will be repurposed)
#define STAMP_FINE_Y(x)      (STAMP_NUM(x)+7)     // Stamp Field: Fine Y offset relative to box (not used, will be repurposed)

#define PIXEL_BOX_X(x)       ((x*24)+STAMP_CENTER_BIAS_X)             // Convert Box X coordinates to pixels
#define PIXEL_BOX_Y(x)       ((x*24)+STAMP_CENTER_BIAS_Y)             // Convert Box Y coordinates to pixels
#define BOX_PIXEL_X(x)       (div24(x-STAMP_CENTER_BIAS_X))           // Convert Stamp X coordinates to Box X
#define BOX_PIXEL_Y(y)       (div24(x-STAMP_CENTER_BIAS_Y))           // Convert Stamp Y coordinates to Box Y
and the relevant bit of collision code:

Code: Select all

/**
 * move_monsters()
 * Move the monsters
 */
void move_monsters()
{
  for (i=2;i<STAMP_NUM_SLOTS;++i)
    {
      // Get the box in a&b
      a=div24(stamps[STAMP_X(i)]-8);
      b=div24(stamps[STAMP_Y(i)]-8);
      c=(b*10)+a; // C is now the box #
      d=dungeon[c];

      if (stamps[STAMP_STATE(i)] == STATE_MONSTER_RIGHT)
        {
          if (d&1<<4)
            {
              if (stamps[STAMP_X(i)]==PIXEL_BOX_X(a))
                {
                  stamps[STAMP_STATE(i)]=rand8()&0x03;
                }
              else
                {
                  stamps[STAMP_X(i)]++;
                }
            }
          else
            {
              stamps[STAMP_X(i)]++;
            }
        }
      else if (stamps[STAMP_STATE(i)] == STATE_MONSTER_LEFT)
        {
          if (d&1<<6)
            {
              if (stamps[STAMP_X(i)]==PIXEL_BOX_X(a))
                {
                  stamps[STAMP_STATE(i)]=rand8()&0x03;
                }
              else
                {
                  stamps[STAMP_X(i)]--;
                }
            }
          else
            {
              stamps[STAMP_X(i)]--;
            }

        }
      else if (stamps[STAMP_STATE(i)] == STATE_MONSTER_UP)
        {
          if (d&1<<7)
            {
              if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b))
                {
                  stamps[STAMP_STATE(i)]=rand8()&0x03;
                }
              else
                {
                  stamps[STAMP_Y(i)]--;
                }
            }
          else
            {
              stamps[STAMP_Y(i)]--;
            }
        }
      else if (stamps[STAMP_STATE(i)] == STATE_MONSTER_DOWN)
        {
          if (d&1<<5)
            {
              if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b))
                {
                  stamps[STAMP_STATE(i)]=rand8()&0x03;
                }
              else
                {
                  stamps[STAMP_Y(i)]++;
                }
            }
          else
            {
              stamps[STAMP_Y(i)]++;
            }
        }
    }
}
This implements a very simple "patrol my space" AI, which I may want to change to alter direction when there are no walls in a box, or something similar.

Have posted a quick build for others to enjoy looking @, code is on github, and am continuing on.
wow-monster-to-wall-collision-test.nes
(40.02 KiB) Downloaded 220 times
-Thom
Post Reply