Hello everyone,
I'm working on a port of Wizard of Wor to the NES.
Source code here:
http://github.com/tschak909/wowAbout
-------
I am an experienced software engineer with some 6502 experience, and I recently completed an Atari VCS game of Dodgeball, which can be found here:
http://github.com/tschak909/dodgeball/ .. Have been a member of AtariAge since 2006 (and the Stella mailing lists since 1998), but this is my first venture into the NES homebrew community.
I initially was going to try doing WoW for the ColecoVision, but it turned out that Opcode had beaten me to the punch
.. So... Didn't see one for the NES...and thought, why not?
I will be writing the primary game code in CC65, but for now, my concentration is on trying to make the graphics as accurate as possible. This is a challenge for me, as the original Astrocade based display runs at a different color clock, and therefore aspect ratio. (352x240) (As it happens, there are +2 more pixels in the horizontal direction, basically. so things need to be squeezed..also since I am targeting this for NTSC, there is less vertical space available.)
I've managed to take enough of a pass at the graphics to start laying out nametables so I can get an idea of scale (these static nametables won't be used as such, but hopefully will be generated programmatically).
Some of the chr's in yychr:
http://i.imgur.com/yCAV5KV.pnghttp://i.imgur.com/dS6gBmc.pngand in the middle of doing the first pass at the character screen. note the attribute clash.
http://i.imgur.com/rqKqq9T.pngI have been proceeding glacially at this for the last few weeks, but I am trying to be slow, patient, and precise. So i'll be working on the graphics for a while, until I can balance out accuracy for constraints... once I'm sure I can get everything squeezed in well enough, I'll start coding.
The target for this guy is an NROM-256. If I can do it smaller...great. I don't see a need to do a mapper.
Anyway, back to it. I hope all the American users have a good 4th of July.
-Thom
(edit: images were obnoxiously large due to my hi-dpi display. Turned them into URLs to be polite.)
Hey and welcome, that's cool. I like that game, I always play it whenever I find it somewhere. Something is neat about games on that old Bally hardware, I might have ported Space Zap if I hadn't had so many projects already. And that one is a lot simpler than Wizard of Wor.
I was just looking at the sample set from MAME, I don't know how deep into the audio you'd want to get, but it looks like that entire sample set, if it was encoded as DPCM @ ~22khz should fit into one of my GTROM boards. That's the basic-level board I make instead of NROM. I also have made a phoneme-based speech synth, that fits in 16kB. Anyways I'm just rambling, I'm just as fine seeing it ported in any form.
Last year I was working on a song in Famitracker and part of it reminded me of Wizard of Wor, so at the time I went ahead and did a little version of it. Attached it here just for fun.
Memblers wrote:
Hey and welcome, that's cool. I like that game, I always play it whenever I find it somewhere. Something is neat about games on that old Bally hardware, I might have ported Space Zap if I hadn't had so many projects already. And that one is a lot simpler than Wizard of Wor.
I was just looking at the sample set from MAME, I don't know how deep into the audio you'd want to get, but it looks like that entire sample set, if it was encoded as DPCM @ ~22khz should fit into one of my GTROM boards. That's the basic-level board I make instead of NROM. I also have made a phoneme-based speech synth, that fits in 16kB. Anyways I'm just rambling, I'm just as fine seeing it ported in any form.
Last year I was working on a song in Famitracker and part of it reminded me of Wizard of Wor, so at the time I went ahead and did a little version of it. Attached it here just for fun.
Wow, The music is awesome!
As for the PCB, what's the big difference between the NROM and GTROM? I do intend completely to do the voice bits..given that the votrax SC-01 sounds have very limited spectra, you could get away with a ridiculously low sample rate and it be intelligible.
Nukey Shay over at AtariAge pointed me to this blog, which has everything from the theory of operation manual, to level layouts (the theory of operation describes the level structure as an 11x6 grid of squares with 4 possible openings (that'll make the level data structure REALLY easy, I think) so I can hopefully do the levels programmatically.
http://wizard-of-wor-fan.blogspot.com/And a partial disassembly here at BalleyAlley:
http://www.ballyalley.com/ml/ml_source/WOW_Latest_working_copy_of_full_disassembly_022104.asmI couldn't find a way to handle the star-field, yet... as for the sparkly red text, i'll just do a palette switch in the NMI between the red and black...
But for now, I'm just doing all the graphic work to make it all fit onscreen.
http://i.imgur.com/R0LvVoq.png title screen should work nicely...
-Thom
I am currently trying to figure out whether I can actually fit the original level structures into the slightly smaller screen on the NES, versus the Astrocade hardware.
The levels in Wizard of Wor are actually created from a symmetrical pattern that is based on a grid of 11 horizontal squares, and 6 vertical squares, of which there can either be a wall or an opening on either side. I can represent this just fine with a data structure, and with the 2x3 metasprites I am using (to be as true to the game as possible) I can't make this fit...
Does anyone see a way, from these pictures or my description that I could make something that could fit? or should I just make my own level patterns?
(would anyone really give a #%()#@ if I used different maze patterns? I'd rather not, because I want to be as accurate as possible, but I do understand when brick walls are hit.)
http://i.imgur.com/uYzfqTx.pngFrom theory of operation:
Quote:
Each dungeon consists of an eleven by six matrix of squares, with a wall or opening on the sides of each square. The maze is symmetrical around the center vertical axis. Every maze has a side escape door located four squares up from the bottom, and on each side of the maze.
And the mazes are here:
http://wizard-of-wor-fan.blogspot.com/2 ... ction.html(as an aside, using the red emphasis bit, I am producing a palette that's close enough to the arcade game.)
Will continue to justkeep experimenting, until I can find something that sucks least.
-Thom
Based on the screenshots on
the game's KLOV page, the original game's cells are 24x24 pixels. It runs on a board whose pixel clock appears to be 7.16 MHz according to MAME's
astrocde driver. This makes its pixel aspect ratio 6:7, which is narrower than the 8:7 of an NES. A PAR-accurate scaling to ColecoVision/NES pixels would result in 18x24 pixel cells, or 2 1/4 by 3 tiles per cell.
Here are your practical options to make it fit:
- Each cell is 16x24 pixels or 2x3 tiles, and the 11x6-cell playfield is 176 by 144 pixels.
- Each cell is 20x24 pixels or 2.5x3 tiles, and the 11x6-cell playfield is 220 by 144 pixels.
This requires separate tiles for walls being in the left or right half of each cell. It also pushes the column of figures to the left and right side of the playfield into the overscan area, but it's fine if you're willing to move the figures below the playfield, as is done in CBS's Atari 2600 port of WOW.
And you might end up needing to draw new level maps anyway, as level maps and sprite graphics are the first thing that copyright lawyers would seize on.
I'm working on illustrations of what 16x24 and 20x24 pixel cels would look like inside the 256x240 pixel picture.
Here's what each cell size would look like
Nice arena…though it doesn't fit the original constraint of "horizontally symmetrical", alas.
18x24 is still doable, it just costs twice the CHR space of that 20 [or uses different circuitry; might be interesting to see a board HW-render the maze fed to it instead of using actual tile-layout logic…no, that'd still be extra, thanks to the 8-pixel buffer in the renderer. Of course, you don't want to use a mapper, so this is academic.]
I am currently slowly working through every possible permutation I can think of, to get things looking right.. will post when I have something workable.
-Thom
Also, isn't it correct that on NTSC PPU's the bottom two rows in the name table won't be visible (making what would be a 240 pixel output, essentially 224 pixels?) ... that makes the NES very much like traditional arcade hardware.
-Thom
The NTSC NES PPU outputs all 240 scanlines. Some just get cut off by the TV because of overscan.
In an arcade game, the arcade operator enters service mode, which displays various test patterns on the display, and adjusts the monitor's size and position controls to make the picture exactly fill the monitor. But in a game for a console, the developer has to put up with the picture size that the TV actually displays, and this varies from one SDTV to another. You can't change how much is cut off, but you can use scrolling to determine whether it's cut off at the top or bottom or a little of both.
There are various "safe area" definitions that approximate upper and lower bounds on the overscan size for typical TVs. See
"Overscan" on the wiki for these.
Seems like the 2x3 tile size may not allow for players going the vertical directions (in the arcade game, the player and enemy blitter objects are simply flipped center-axis)...
Still experimenting...
no matter what, I should have enough CHR-ROM space to have tile variations...
...i think.
(I do know I will have enough sprites, there are no more than 6 enemies active per whole dungeon, 36 sprites total, 48 sprites with the two active player sprites, leaving 16 sprites.. I can definitely get away with using tiles for the phasor cannon.)....
-Thom
I squished the sprites down to 16x16, but ... ugh. They lose their detail.
(The tongue action between Gor and Thor is completely coincidental)
-Thom
@Memblers: Have tried to find info both on the wiki and on the web about the rom mapper you mentioned, what can you tell me about it, or where can I read some tech info?
-Thom
viewtopic.php?p=158140#p158140I suppose I should transfer that information into the wiki.
Thanks!
Cheapocabra.. *chuckle*
-Thom
You may want to focus on not squishing detailed areas as much as non-detailed, if you go that route. Preserve hood/face over robe, legs can still be added to the fly without problem, you could probably shorten its body by some and get a bigger face.
Yeah, I'm trying all options. It's okay. I'm patient. It took me 20 years to have my first original game idea (Dodgeball for the 2600), and I'll just keep piecing away at it until I get something that I think is okay.
The scaling was simply a quick non-interpolated squish so I could see where potential detail problems could arise. I can only do that to the sprites, really...the tiles for text/maze can't be shrunk, or they lose their arcade sparkle.
I've already dropped the star field, as there isn't a secondary background plane, although...maybe there would be enough cycles to or the maze on top of a starfield in ram with another mapper...dunno...
also, the sparkly text i've already decided will be replaced with nmi palette changes that flicker (for both the red text and the wizard of wor kill)
-Thom
Star Force makes its starfield with low-priority (attribute 2 bit 5 set, late in display list) sprites.
Galaxian does the same. (It also twinkles the starfield by toggling the star sprites' X coordinate 128s bit every 8 vblanks)
Ah, didn't consider that. Thanks.
I do have 16 sprites left over in my OAM allocation, and that might just work...will try that when I get to code.
-Thom
So, I decided to do an exercise to analyse the speech in Wizard of Wor. Found all the phrases, there are over 70:
Quote:
Insert coin and game over mode :
1) Hey! Insert Coin!
2) Find me ...The Wizard of Wor.
3) I'm out of spite, ha ha ha ha!
Get ready or coin acceptance mode :
1) Get ready, worrior.
2) You better hope you don't find me, the Wizard of Wor.
3) Another coin for my treasure chest.
4) Ah good! My pets were getting hungry. Ha ha ha ha!
5) My worlings are very very hungry. Ha ha ha ha!
6) Welcome to my world of Wor.
7) So you've come to score in the world of Wor. Ha ha ha ha!
8) You're off to see the Wizard, the magical Wizard of Wor.
Dungeon start-ups :
1) Kill Worluk for double score.
2) You are in the dungeons of Wor.
3) I am the Wizard of Wor.
4) One bite from my pretties, and you'll explode, ha ha ha ha!
5) Worluk will escape through the door.
6) Watch the radar, worrior.
7) Thorwor is red, mean, and hungry for space food.
8) Remember, I'm the wizard, not you.
9) If you can't beat the rest, then you'll never get the best! Ha ha ha ha!
10) You'll never leave Wor alive! Ha ha ha ha!
11) If you destroy my babies, I'll pop you in the oven! Ha ha ha ha!
12) Burwor hasn't eaten anyone in months. Ha ha ha ha!
13) My babies breathe fire worrior.
14) I'll fry you with my lightning bolts.
15) Burwor, Garwor, and Thorwor will do you in.
16) My creatures are radioactive.
Bonus player awarded :
1) You'll get ...The Arena! Ha ha ha ha!
2) Another worrior for my babies to devour.
3) Keep going and you will find me.
4) A few more dungeons and you will be a Worlord.
5) Worrior fear, I draw near, each time I appear. Ha ha ha ha!
6) You won't have a chance for your dance worrior. Ha ha ha ha!
7) Now you're asking for trouble worrior.
8) Now I'm getting mad worrior.
First Garwor appears :
1) Now you get the heavyweights! Ha ha ha ha!
2) Garwor, go after them!
3) If you try any harder, you'll only meet with doom, ha ha ha ha!
4) If you get too powerful, I'll take care of you myself, ha ha ha ha!
5) My magic is stranger than your weapons, worrior.
6) Worrior, while you developed science, we developed magic.
7) Your bones will lie in the dungeons of Wor, ha ha ha ha!
8) Garwor and Thorwor, become, invisible! Ha ha ha ha!
Wizard kills worrior :
1) You've just been fried by the Wizard of Wor! Ha ha ha ha!
2) Bite the bolt worrior. Ha ha ha ha!
3) Wasn't that lightning bolt delicious? Ha ha ha ha!
4) And my teleporting spell can be even faster! Ha ha ha ha!
5) Now you know the taste of my magic, worrior.
6) Maybe you'll see me again worrior.
7) Your explosion was music to my ears! Ha ha ha ha!
8) I'll say it again, worrior fear, I draw near, each time I appear. Ha ha ha ha!
Worlord dungeon startups :
1) Worlord, be forewarned! You approach, The Pit!
2) Your path leads directly to The Pit! Ha ha ha ha!
3) Deeper, ever deeper into the dungeons of Wor.
4) Beware! You are in the Worlord dungeons!
5) Ah! You thought you could hide but I'm the dungeon master.
6) Thor Bur Gar! Dinner’s ready! Ha ha ha ha!
7) Hey! Your space boot's untied! Ha ha ha ha!
8) My beasts run wild in the Worlord dungeons. Ha ha ha ha!
Phrases for The Pit :
1) Now your only chance is your dance! Ha ha ha ha!
2) Are you fit, to survive The Pit? Ha ha ha ha!
3) Oops! I must have forgotten the walls! Ha ha ha ha!
4) Worlord, where are you going to hide now? Ha ha ha ha!
End of game :
1) Come back for more with the Wizard of Wor. Ha ha ha ha!
2) The dungeons of Wor await your return, worrior.
3) Deep in the caverns of Wor, you will meet me, worrior.
4) The Wizard of Wor thanks you.
5) You know you can do better, worrior.
6) Hurry back! I can't wait to do it again.
7) You can start anew, but for now you're through. Ha ha ha ha!
8) He he he ho ho ho ha ha ha! That was fun!
When you have reached the Worlord dungeons, the Wizard calls you Worlord, instead of worrior.
and I broke those down into individual words and ran the result through uniq and gathered the output with wc to get the total number of unique words:
Code:
$ wc -l wor_words_uniq.txt
527 wor_words_uniq.txt
527 unique words. GREAT GOOGLY MOOGLY! with each delta PCM word being approximately 500 to 800 bytes, I would need a pretty hefty map to do this right, and none of the existing players could really handle it...so it'd have to be custom....
not ruling it out...just making note of the magnitude of what needs to be stored.
-Thom
p.s. the Wizard is the ultimate troll.
Wizard of Wor's hardware has a speech synthesis IC on it (the votrax). Maybe it makes more sense to use the DPCM hardware on a phoneme-by-phoneme basis?
I expect having "Wor" be one thing regardless would save a bunch of space.
Have you looked at the C64 port? [I haven't] It might have some graphical and audio insights. Might.
lidnariq wrote:
Wizard of Wor's hardware has a speech synthesis IC on it (the votrax). Maybe it makes more sense to use the DPCM hardware on a phoneme-by-phoneme basis?
The problem here is that unlike other speech chips of the time, the SC-01 phoneme selection uses diphones, and there is considerable logic wrapped up in the target diphone selection (seriously...I looked at the gate level schematic on the SC-01 many years ago and was flabberghasted that there was so much logic concentrated to this task, in such a teeny tiny die. It was a tour de force of highly concentrated logic).
I will revisit this later, I just wanted to try the easy experiments, first.
-Thom
As I thought about it, I was like, "Surely I could just take an SC-01-A and route it to an audio pin..." 5 minutes of looking and I realize, Nintendo didn't provide an audio pin for the NES on the cartridge bus.. boooooo..... (SERIOUSLY, I THOUGHT _EVERYBODY_ DID THAT!)
now, I know I could literally take the audio output, and route it to a cable where you could plug in the original audio cable and have the combined audio (in a sort of duct taped banana like stacking monstrosity), but that's getting into insanely ridiculous territory...
ugh
WHY COULDN'T THEY HAVE JUST PUT THE DAMNED AUDIO PIN ON THE CARTRIDGE, INSTEAD OF THE EXPANSION BUS?!
-Thom
(Because they just haaaaaaaad to have those 10 pins for the CIC, didn't they?)
(and yeah, I know, they did put on on the cart bus on the famicom...what brain damage made Nintendo put it on the world's most unused connector?)
-Thom
Infiniteneslives is developing a dongle you can put in the expansion bus to re-route the audio pin from the cartridge into the audio mixer, so maybe this isn't quite an unfeasible task in the near future.
tschak909 wrote:
(what brain damage made Nintendo put it on the world's most unused connector?)
Snarky: The SNES expansion port connector is pretty underused, too.
Serious: Nintendo didn't even think, at the time, that there was any purpose other than connecting the Disk System to the NES. They got better (hence why there's an almost-completely unused audio input on the DMG)
So, after days of constant experimentation and careful study, I think this is about as good as I can get the dungeon cavity:
(I didn't turn on the R emphasis bit for this snippet)
This cavity produces enough for a 10x5 maze, which should be able to properly hold the sprites in both horizontal and vertical orientations. *cross-fingers*
You can see this and a few other screens in the attached test rom.
Attachment:
File comment: Testing graphic output to ensure proper screen fitting.
wow-titletest.nes [24.02 KiB]
Downloaded 124 times
Source code will always be available for all builds and releases @ github:
https://github.com/tschak909/wow(updating first post to reflect this)
-Thom
Also, did a quick bit of music, although I will need to either pare this down for Famitone2, or use the full blown tracker (it uses vibrato/tremolo)..grrr...
-Thom
Pently can do vibrato, just not looping envelopes
or portamento between two pitches.
I will definitely look at that engine. Looks great at first glance.
As for graphics, doing yet another pass. Was able to gain 6 pixels in both X and Y directions by pushing the lines in the line tiles toward the edge.
So I am pushing things back, and shuffling around tiles...
I really don't want to push sprites to 16x16...
I for damned sure won't change the text to 8x8, because it will lose all its character.
I hope to get this stable enough to where I can compress all the tiles (get rid of duplicates etc) so I can gain some much needed tile space, especially on the sprite side.
-Thom
Further refinement, turns out, I needed to completely push the Worriors off the screen to get a 10x6 matrix (which can be encoded as a symmetrical 5x3 array)...
But this leaves no room for the Worriors, at all, and with the gate timer, this makes any sort of lives counter be awkward as hell..so...
I may back away to a 8x6 matrix (4x3 symmetry)... OR
I squeeze the sprites to 2x2 instead of 2x3... *deep-breath* will try... I also need to add additional walk cycles and their 90 degree counterparts, I will run out of chr space if I'm not careful...
I have to get all this squared away before I can start writing the meat of the game as all the mazes are drawn programmatically.
-Thom
I think you can do well just cramming the lives count to the sides of the radar text, either with small icons or a number. Better to not compromise the playfield.
well, 10x6 isn't going to be the same as 11x6…
Myask wrote:
well, 10x6 isn't going to be the same as 11x6…
I see no way to get 11x6 without squishing the sprites. Which.. given my background in research... I'll do... I won't promise I won't be griping all the way through it, but hey...
-Thom
Currently giving myself a crash course in vram_set_update() and the different update vram functions in neslib..goal is to re-draw the dungeon in code, and work out a reasonably quick and compact data structure for the dungeon...now that I have a reasonably good idea of how the tiles in the nametable are to be laid out.
-Thom
Well gosh, that was easy.
Code:
vram_adr(NAMETABLE_A);
vram_unrle(wow_dungeon);
pal_bg(palette);
pal_spr(palette);
vram_adr(NTADR_A(0,0));
for (i=0;i<6;++i)
{
for (j=0;j<10;++j)
{
vram_adr(NTADR_A((j*3)+1,(i*3)+1));
vram_put(0x74);
vram_put(0x63);
vram_put(0x75);
vram_adr(NTADR_A((j*3)+1,(i*3)+2));
vram_put(0x65);
vram_put(0x00);
vram_put(0x66);
vram_adr(NTADR_A((j*3)+1,(i*3)+3));
vram_put(0x76);
vram_put(0x64);
vram_put(0x77);
}
}
ppu_on_all();
ppu_wait_frame();
Which produces:
Will make this more efficient, shortly..but it's nice to know that I understand the vram well enough.
Pushing code up to github now, calling it a night. it's 2am.
-Thom
Took a first crack at the dungeon rendering code, which renders from an array of data in the following format
Code:
ULDR TSWX
-------------
0000 0000
Where ULDR is Up, Down Left, Right which sides of the box are to be open, T indicates a teleport wall, S is an enemy spawn point, and W is a Wizard spawn point. Worluk is special as he always appears at one door, and traverses toward the other.
The first pass of the code is inefficient, and I will refine it as I think it through further, but the meat of it is:
Code:
// ULDR
// 1111
dungeon=(char*)dungeon1;
b=0; // dungeon array index
for (i=0;i<6;++i)
{
for (j=0;j<10;++j)
{
/* Tile 1 */
vram_adr(NTADR_A((j*3)+1,(i*3)+1));
if (( (dungeon[b] & 1<<7) ) && ( (dungeon[b] & 1<<6) )) /* UP AND LEFT */
{
if ( (dungeon[b] & 1<<3) ) /* LEFT WITH TELEPORT */
{
vram_put(0x73);
}
else
{
vram_put(0x74);
}
}
else if ( (dungeon[b] & 1<<7) ) /* UP */
{
vram_put(0x63);
}
else if ( (dungeon[b] & 1<<6) ) /* LEFT */
{
if ( (dungeon[b] & 1<<3) ) /* LEFT WITH TELEPORT */
{
vram_put(0x73);
}
else
{
vram_put(0x65);
}
}
else
{
vram_put(0x00);
}
// Tile 2
if (dungeon[b] & (1<<7)) /* UP */
{
vram_put(0x63);
}
else
{
vram_put(0x00);
}
// Tile 3
if (( (dungeon[b] & 1<<7) ) && ( (dungeon[b] & 1<<4) )) /* UP AND RIGHT */
{
if ( (dungeon[b] & 1<<3) ) /* RIGHT WITH TELEPORT */
{
vram_put(0x98);
}
else
{
vram_put(0x75);
}
}
else if ( (dungeon[b] & 1<<7) ) /* UP */
{
vram_put(0x63);
}
else if ( (dungeon[b] & 1<<4) ) /* RIGHT */
{
if ( (dungeon[b] & 1<<3) ) /* RIGHT WITH TELEPORT */
{
vram_put(0x98);
}
else
{
vram_put(0x66);
}
}
else
{
vram_put(0x00);
}
vram_adr(NTADR_A((j*3)+1,(i*3)+2));
// Tile 4
if (dungeon[b] & (1<<6)) /* LEFT */
{
if (dungeon[b] & (1<<3)) /* LEFT WITH TELEPORT */
{
vram_put(0x73);
}
else
{
vram_put(0x65);
}
}
else
{
vram_put(0x00);
}
// Tile 5 is always empty.
vram_put(0x00);
// Tile 6
if (dungeon[b] & (1<<4)) /* RIGHT */
{
if (dungeon[b] & (1<<3)) /* RIGHT WITH TELEPORT */
{
vram_put(0x98);
}
else
{
vram_put(0x66);
}
}
else
{
vram_put(0x00);
}
vram_adr(NTADR_A((j*3)+1,(i*3)+3));
// Tile 7
if (( (dungeon[b] & 1<<6) ) && ( (dungeon[b] & 1<<5) )) /* LEFT AND DOWN */
{
if ( (dungeon[b] & 1<<3) ) /* LEFT WITH TELEPORT */
{
vram_put(0x73);
}
else
{
vram_put(0x76);
}
}
else if ( (dungeon[b] & 1<<6) ) /* LEFT */
{
if ( (dungeon[b] & 1<<3) ) /* LEFT WITH TELEPORT */
{
vram_put(0x73);
}
else
{
vram_put(0x65);
}
}
else if ( (dungeon[b] & 1<<5) ) /* DOWN */
{
vram_put(0x64);
}
else
{
vram_put(0x00);
}
// Tile 8
if (dungeon[b] & (1<<5)) /* DOWN */
{
vram_put(0x64);
}
else
{
vram_put(0x00);
}
// Tile 9
if (( (dungeon[b] & 1<<4) ) && ( (dungeon[b] & 1<<5) )) /* DOWN AND RIGHT */
{
if ( (dungeon[b] & 1<<3) ) /* RIGHT WITH TELEPORT */
{
vram_put(0x98);
}
else
{
vram_put(0x77);
}
}
else if ( (dungeon[b] & 1<<5) ) /* DOWN */
{
vram_put(0x64);
}
else if ( (dungeon[b] & 1<<4) ) /* RIGHT */
{
if ( (dungeon[b] & 1<<3) ) /* RIGHT WITH TELEPORT */
{
vram_put(0x98);
}
else
{
vram_put(0x66);
}
}
else
{
vram_put(0x00);
}
++b;
}
}
And this is the base dungeon template, 60 bytes for now, 30 if I mirror one axis, 15 if I mirror both axes.
Code:
const unsigned char dungeon1[60]={0xC0,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x90,
0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,
0x48,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,
0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,
0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x10,
0x60,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x30};
Which produces the following output:
I'll draw the first real dungeon tomorrow (actually, this is the layout for "The Pit" which is the hardest dungeon, hehe.)
-Thom
Looking great.
I'd get rid of the *3's. Using "*" includes extra runtime code plus is slow. You can do the X*3 as (X+(X<<1)) for example. You can also precalculate the offsets. offsX, offsY are unsigned char. addr is unsigned int.
Code:
offsY = 1;
for (i=0;i<6;++i) {
offsX = 1;
for (j=0;j<10;++j) {
addr = NTADR_A (offsX, offsY);
vram_adr( addr );
// Here: 'vram_put's for the first row.
// Down a row in the nametable
addr+=32;
vram_adr (addr);
// Here: 'vram_put's for the second row.
// Down a row in the nametable
addr+=32;
vram_adr (addr);
// Here: 'vram_put's for the third row.
// Next tile:
offsX += 3;
}
// Next row of tiles
offsY += 3;
}
That way you calculate the base address, in the most simple way (just a shift and an addition, which is what NTADR_A does), just once per tile, simplifying your code, thus saving both bytes and cycles.
na_th_an wrote:
I'd get rid of the *3's. Using "*" includes extra runtime code plus is slow. You can do the X*3 as (X+(X<<1)) for example.
The compiler should be doing this shift conversion for you. If you can prove that it isn't, then either A. something in your code is of the wrong type or B. you need to file an issue.
Quote:
You can also precalculate the offsets. offsX, offsY are unsigned char. addr is unsigned int.
Code:
// ...
// Next tile:
offsX += 3;
This appears to be related to
strength reduction, where you can update addresses with a simpler operation based on the recursion that xtimes3[x + 1] = xtimes3[x] + 3. So instead of multiplying by 3 for each cell, add 3 to the destination VRAM address after drawing each cell.
All valid points, indeed. I will go through and refine the algorithm as I can.
A note, I am not familiar with CC65 internals. To be blunt, the common wisdom BITD was that the 6502 COULDN'T do a decent C without EMULATING ANOTHER PROCESSOR (p-system, and most 6502 C's that I used literally modelled an 8080, so CC65 to me seems like something of a minor miracle.), so I am not sure WHAT compiler optimizations are actually taking place yet (I need to sit down and look at the outputted source code.
-Thom
I
don't think CC65 has a generic capability to generate shift/add combinations instead of multiplication, but it does seem to have special case library functions for multiplying by a literal in the range 2-10? Here's the one for multiplying by 3:
https://github.com/cc65/cc65/blob/master/libsrc/runtime/mulax3.sSo... doing
(x<<1)+x is
slightly faster, I think, but there's a lot of encumbering inefficiencies anyway (stack temporaries, promotion to integer), so the difference is kind of pointless unless you want to write the multiply directly in assembly.
I think you're actually better off with
x*3 just because it will produce smaller code (and code size tends to be a serious problem with CC65).
I've been working for years with a fairly old version of z88dk, so I have the (bad?) habit of not trusting the compiler at all. I also have the bad habit of not checking, which I should start doing, as I was avoiding structs in cc65 'cause I mistakenly thought that they generated worse code when accessing the members than using plain variables. My bad.
In this case, the *3 would indeed be better 'cause this is not a time critical task, so saving bytes is a priority.
I stand, thus, corrected
tepples wrote:
This appears to be related to
strength reduction, where you can update addresses with a simpler operation based on the recursion that xtimes3[x + 1] = xtimes3[x] + 3. So instead of multiplying by 3 for each cell, add 3 to the destination VRAM address after drawing each cell.
I do this all the time, when appliable. It saves me tons of headaches, at least for my own mindset.
Okay, several hours of laying out a dungeon, and I'm ready to write a clicky editor.
Anyone got a suggestion on what to use to whip one up for Windows?
-Thom
I use the good ol' Mappy (mapwin). It can be tuned up to output the current map layer as a plain, headerless set of bytes, one per tile, which you can then process in a batch script to fit your needs.
http://www.mojontwins.com/warehouse/Mappy-mojono.rar
Thanks, but am using a special format here... One byte = a set of 9 tiles (a box) and any special functions it may have (4 of them) I'll just whip something up.
p.s. First dungeon drawn, and placed a few sprites in to double check sizing and proportion!
-Thom
I'm curious, how do you guys writing games in C keep score? WoW has a score max of 7 digits. which can't really be put nicely into a 16-bit quantity, but works well in a 24 or a 32 bit quantity..am curious if taking this pedestrian route is what's best here, or if somebody knows a more space efficient method?
-Thom
A lot of the time it's sensible to just store a score in decimal, rather than as a binary number that you'd have to convert, i.e. just store the 7 digit score as 7 bytes (each value 0-10) and write a routine that will add points to it (carrying anything that overflows 10 to the next one).
Also consider whether the lowest digits of the score are ever used. From a few google searches, it looks like the score increments by 100's. If that's the case, you don't need to store those last two digits.
Quote:
Code:
ULDR TSWX
-------------
0000 0000
Having optimized cellular automata dealing with edges of squares, storing ULDR for every square is redundant; you only need to store DR, really.
But your choice will leave you with much easier collision checks, and possibility of one-way passthrough walls.
I did consider storing only half of the data, but I liked the flexibility of storing everything.
Right now, am doing little experiments on sprite updating and vram updating (score etc). Things will slow down for a bit while I get the hang of the hardware.
-Thom
I have placed the following disclaimer in my code:
Code:
/**************************************************
* DISCLAIMER: This is not clean C code. It will *
* Never be clean C code. Everything here is done *
* So that the result will either work, or fit *
* inside an NES, being that C was never an ideal *
* language for the 6502. It is honestly a minor *
* miracle that CC65 exists and that it does as *
* well as it does. It is a testament to the *
* CC65 engineers to want to produce more than *
* a pidgin C compiler. Hats off to them, really. *
* -Thom Cherryhomes 07/16/2017 *
**************************************************/
-Thom
Can somebody with an NTSC NES and a CRT monitor/tv please send pics of the three screens in this attached test ROM? am curious.
-Thom
Attachment:
wow_screentest.nes [24.02 KiB]
Downloaded 126 times
tschak909 wrote:
I have placed the following disclaimer in my code:
To be honest, it seems unnecessary to apologize for "unclean" code in a game.
A source code library should have clean code because the product
is the code, and the users/customers are programmers.
With a game, though, the product is the game itself, and the customer is only a player. The cleanliness of the code is an internal implementation detail.
A clean code base is nice for the person who works with it, but it's normal to make at least a little mess in the process of developing and trying things. Cleanup takes time (and money), so it's pretty reasonable to
not do it if the game is ready to ship.
Anyhow, I'm not saying you shouldn't strive to write clean code. It's well worth developing good code practices. Just I don't think there's any reason to feel shame for making a compromise in that respect.
tschak909 wrote:
Can somebody with an NTSC NES and a CRT monitor/tv please send pics of the three screens in this attached test ROM? am curious.
Several emulators have good NTSC emulation (often derived from
Blargg's implementation), usually it's called a "filter". In FCEUX you can find an NTSC filter option under Config > Video, for example, but most emulators seem to have a similar option somewhere.
That emulation will show you pretty well what the NTSC signal does to fuzz up the picture.
The ways in which a CRT itself are different can't really be expressed well by taking a picture of a television. Mainly this has effects of overall brightness/gamma and saturation, and the "scanline" thing, but it doesn't really do much to degrade the image in ways that emulation of the NTSC signal alone won't.
(I don't currently have a CRT or I'd take a picture myself to show you, but in the meanwhile I'd recommend trying the emulated options.)
sigh. ...
(a) you missed my point on the disclaimer, but that's ok.
(b) I'm using blargg's filters in FCEUX. I'm just curious as to how the result frames on a tube.
-Thom
Ah, well sorry for misinterpreting, in light of how I must now reinterpret it I hope I didn't come across as patronizing.
Not at all.
But I do have an interesting head scratcher... I'm constructing an update buffer for set_vram_update(), and thus far, it has been going swimmingly well, except the very last update directive (a vertical update at y=29), is causing a little flotsam of the same tile elsewhere on the screen, am I doing something stupid, or is this a bug? (I'm betting it's the former)
given the update code:
Code:
static unsigned char update_buffer[256]={
/* Blue Door */
MSB(NTADR_A(1,19))|NT_UPD_HORZ,LSB(NTADR_A(1,19)),
3,
0x74,0x63,0x75,
MSB(NTADR_A(1,18))|NT_UPD_HORZ,LSB(NTADR_A(1,18)),
3,
0x76,0x64,0x64,
/* Yellow Door */
MSB(NTADR_A(27,19))|NT_UPD_HORZ,LSB(NTADR_A(27,19)),
4,
0x66,0x63,0x63,0x75,
MSB(NTADR_A(27,18))|NT_UPD_HORZ,LSB(NTADR_A(27,18)),
4,
0x64,0x64,0x64,0x77,
/* Blue Score */
MSB(NTADR_A(2,25))|NT_UPD_HORZ,LSB(NTADR_A(2,25)),
7,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,
MSB(NTADR_A(2,26))|NT_UPD_HORZ,LSB(NTADR_A(2,26)),
7,
0x00,0x00,0x00,0x00,0x00,0x00,0x11,
/* Yellow Score */
MSB(NTADR_A(23,25))|NT_UPD_HORZ,LSB(NTADR_A(23,25)),
7,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,
MSB(NTADR_A(23,26))|NT_UPD_HORZ,LSB(NTADR_A(23,26)),
7,
0x00,0x00,0x00,0x00,0x00,0x00,0x11,
/* Teleports */
MSB(NTADR_A(1,7))|NT_UPD_VERT,LSB(NTADR_A(1,7)),
3,
0x73,0x73,0x73,
/* [color=#FF0000]This part causes the red blips in the middle of the screen[/color] */
MSB(NTADR_A(30,7))|NT_UPD_VERT,LSB(NTADR_A(30,7)),
3,
0x78,0x78,0x78,
NT_UPD_EOF
};
I get:
Notice the extra red bits where they're obviously not supposed to be? if I remove that last directive, the bar on the right goes away, and so does the mess...
wtf?
-Thom
It's possible that ~68 bytes / 10 regions in the PPU update buffer might be too much to fit within the available vblank time with the library's NMI update handler. Maybe put a breakpoint on a write of $78 to $2007 (to catch those last 3 bytes going through) and check if it's happening within vblank, or too late. In FCEUX it'll say scanline 0 when it's too late (vblank runs in scanlines 241-260).
If the update write happens outside of vblank it will corrupt some stray part of the screen.
Aside from that, I don't see any obvious typos but I find the MSB(NTADR_A(1,19))|NT_UPD_HORZ,LSB(NTADR_A(1,19)), blocks really hard to visually read, so I could easily mistake those for correct. If you made two macros something like UPDATE_HORZ(1,19) and UPDATE_VERT(1,7) instead I wouldn't be as suspicious about misreading those?
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?
(sorry, if I am asking stupid questions, I am literally crash coursing myself through the NES hardware)
-Thom
So basically, I need to do the following sets of updates:
Scores: 28 tiles (two horiz rows of 7 tiles x 2)
teleports: 6 tiles (two vert rows of 3)
doors: 14 tiles (one set of two rows of 3 tiles, for the left, one set of two rows of 4 tiles for the right) (horizontal)
countdown: 4 tiles (2 sets of 2 rows) (each set in a different place)
lives: 4 tiles (same as countdown)
is this too much to handle in neslib's vram update list code all at once? Can I split this up?
-Thom
(irony of ironies, even here, away from the VCS, I am finding myself counting cycles)
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")
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.
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:
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:
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:
*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.
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
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.
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
Code that currently opens/closes the worrior doors
Code:
/**
* 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;
}
}
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:
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=tsEfDJwiPNYbut 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:
/**
* 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
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.
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:
// 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
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.)
Radar is implemented as background tiles, saw no need to waste perfectly good sprites for it.
-Thom
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
A little refactoring of my macros to make them saner, and the collision code became simpler to write, and the bugs went poof:
Code:
#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:
/**
* 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.
Attachment:
-Thom
Hello, everybody. A bit further, I have player movement working, except:
* of course, timer state needs to be extended a bit to have the pop-out action
* right now all box calculations are done with the top left x,y, which makes player movement with the sprite quite funny if you traverse an intersection and are not completely in the adjunct box.
I need to think through how best to solve the latter problem, if anyone has any ideas, feel free to reply. In the mean time, I've attached a ROM to show behavior.
Attachment:
wow-player-test.nes [40.02 KiB]
Downloaded 80 times
relevant code:
Code:
/**
* move_players()
*/
void move_players(void)
{
for (i=0;i<2;++i)
{
// Get the player's box.
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_XTRA_A(i)]==TRUE)
{
// Player inside box.
if (sec==0) // One second has elapsed.
{
stamps[STAMP_XTRA_B(i)]--;
if (stamps[STAMP_XTRA_B(i)]==0x00)
{
stamps[STAMP_XTRA_A(i)]=FALSE;
}
}
else
{
// do nothing, for now.
}
}
else
{
// Player outside box.
if (stamps[STAMP_XTRA_B(i)]==0x00)
{
// Eject player
stamps[STAMP_Y(i)]=PIXEL_BOX_Y(5); // Place inside playfield.
stamps[STAMP_XTRA_B(i)]=0xff; // Indicate player has been ejected.
if (i==0)
{
yellow_door_state=CLOSED;
}
else
{
blue_door_state=CLOSED;
}
}
else
{
// Player is free to move, move about.
if ((stamps[STAMP_STATE(i)] == STATE_PLAYER_RIGHT_IDLE) ||
(stamps[STAMP_STATE(i)] == STATE_PLAYER_LEFT_IDLE) ||
(stamps[STAMP_STATE(i)] == STATE_PLAYER_UP_IDLE) ||
(stamps[STAMP_STATE(i)] == STATE_PLAYER_DOWN_IDLE))
{
// Player is idle, do nothing, else...
}
else
{
// Player wants to move.
if (stamps[STAMP_STATE(i)]==STATE_PLAYER_RIGHT)
{
if (d&1<<4)
{
// Right wall nearby.
if (stamps[STAMP_X(i)]==PIXEL_BOX_X(a))
{
// don't do a damned thing.
}
else
{
stamps[STAMP_X(i)]++;
}
}
else
{
stamps[STAMP_X(i)]++;
}
}
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_LEFT)
{
if (d&1<<6)
{
// Left wall nearby
if (stamps[STAMP_X(i)]==PIXEL_BOX_X(a))
{
// Don't do anything
}
else
{
stamps[STAMP_X(i)]--;
}
}
else
{
stamps[STAMP_X(i)]--;
}
}
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_UP)
{
if (d&1<<7)
{
// Up wall nearby
if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b))
{
// Don't do anything
}
else
{
stamps[STAMP_Y(i)]--;
}
}
else
{
stamps[STAMP_Y(i)]--;
}
}
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_DOWN)
{
if (d&1<<5)
{
// Down wall nearby
if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b))
{
// Don't do anything.
}
else
{
stamps[STAMP_Y(i)]++;
}
}
else
{
stamps[STAMP_Y(i)]++;
}
}
}
}
}
}
}
-Thom
Day 3 of still trying to hammer through the controls, still stuck.
The intended behavior is to have the player traverse into an opening only if their feet are all the way at the extreme end of the box, if they aren't, then the player moves in the last successful direction until the feet touch the box extreme end.)
trying to do this without resorting to more pedestrian methods is making my head hurt.
-Thom
Oh, pac-man/Zelda1-style moves? That's fairly simple.
Code:
//pseudocode
//once per frame...
if (any direction pressed)
if (aligned with tile)
move (pressed direction)
lastmove = pressed
alignedWithIntersection = false
else if //not aligned
if (pressed direction is at right angle to last moved direction)
move (last moved)
else
move (pressed) //either toward or opposite last move
lastmove = pressed
//then check alignment with possible intersections
//often accomplished with modulo
//note that you only need check one coordinate
//based on what direction lastmove is
The modulo is the problem here, given an integer divide by 24 such as:
Code:
;; void __fastcall__ div24(unsigned char d);
_div24: lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
rts
How can I determine a modulo? Is it sitting here in temp at some point?
-Thom
No, the remainder is thrown away in the carry bits as it does the math.
That routine's actually calculating
TEMP = (arg/8)
TEMP += (arg/32)
TEMP += (arg/128)+!!(arg&64)
.... that's not ÷24, that's ÷6.
That's what I figured. I'm trying to see how I can handle the collision detection sanely, without needing to resort to a modulo, as each box is 24 pixels by 24 pixels (a 3x3 set of tiles).
-Thom
I am deathly afraid of how much code will be added if I use a modulo operator (%) here...
-Thom
I have a trivial PIC asm get-remainder routine that just works by manually doing to the long division and throwing away the quotient, something akin to
Code:
uint8 getremainder(uint8 dividend, uint8 divisor) {
uint8 count=1;
while (! divisor & 0x80) { divisor<<=1; count++; }
for (; count; count--, divisor >>=1) {
if (dividend > divisor) dividend -= divisor;
}
return dividend;
}
No idea if cc65's comparably efficient.
For ÷24 just unrolling that's probably not unreasonable.
The other way to do it is to represent all character positions in world space using units of 1/256 cell. This makes collision detection easier, as you have a whole number of cells and fraction of cells, and you can easily multiply by 24 for display:
Code:
; assume actor_x is in tiles and actor_xsub is in 1/256 tiles
lda actor_x,x
sta temp_hi
lda actor_xsub,x
asl a
rol temp_hi ; temp_hi:A = X*2
adc actor_xsub,x
sta temp_lo
lda temp_hi
adc actor_x,x ; A:temp_lo = X*3
.repeat 3
asl temp_lo
rol a ; A:templo = X*6, X*12, X*24
.endrepeat
; by now, A is pixel position of sprite
As for walking into a wall edge-on, I'd recommend walking toward the center of the cell instead of walking in the same direction as before. This way, if you slighly overshoot the area between walls, you'll back up into the cell close to you instead of the next cell up. Should I draw a diagram of this suggestion to illustrate?
Then just store the "distance to next cell", with 0 being aligned, decrement each time you move…and if you turn around halfway, set it to 23-last instead of decrementing it. No modulo necessary by keeping track of it (and it doubles as the "aligned" variable).
Attachment:
press_against_wall_options.gif [ 20.6 KiB | Viewed 2290 times ]
A: The character stops just past the center of the cell. Then the player presses toward a wall. But because movement is blocked by a wall, the character finishes entering the cell he was last entering before turning into the next hallway.
B: The character stops just past the center of the cell. Then the player presses toward a wall. But because movement is blocked by a wall, the character inches toward the center of the cell and then proceeds down the nearest hallway.
Myask wrote:
Then just store the "distance to next cell", with 0 being aligned, decrement each time you move…and if you turn around halfway, set it to 23-last instead of decrementing it. No modulo necessary by keeping track of it (and it doubles as the "aligned" variable).
Makes sense, I see this in an older page of my notes, I just didn't haven't been able to think it all the way through.
Collision detection code has always been my steep hill, and I always have to fully understand the conditions to states, before I can hammer it out.
Everybody has provided excellent info. I am going to digest it, and keep working through the problem.
Thanks,
-Thom
I should note that the (23-last) is assuming you're using 24-pixel cells [invert (24-last)and decrement combined], and the decrement in that and elsewhere assuming that you're moving the sprite at 1px/frame.
You'd have to add a compare vs 12 to go "should I turn around" on pressing a right-angle direction to get behavior B tepples diagrammed.
My suggestion was instead to "continue to next cell if a direction is held unless you explicitly turn around"…which I'm not sure is better. Pac-man, you rarely want to turn around because a ghost is behind you, and turning before the corner causing you to reverse course into a ghost would be bad. However, if you press it late and it doesn't turn you around, which my behavior C wouldn't, then that might also be bad.
Still working through it.. sigh.. I suck.
-Thom
tschak909 wrote:
The modulo is the problem here, given an integer divide by 24 such as:
Code:
;; void __fastcall__ div24(unsigned char d);
_div24: lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
rts
How can I determine a modulo? Is it sitting here in temp at some point?
-Thom
Modulo is easy, there are a few ways to do it. You'll have to figure out how to integerate it into C though. Here's a few ways:
Long slow division does not take too long in this case as 24 is a big divisor.
Code:
;24-94 cycles
_div24_mod24:
ldy #$FF
sec
_div24:
iny
sbc #24
bcs _div24
adc #24
rts ; Y = integer division result, A = modulo result
Using the unsigned integer division routine for div24, modulo24 can be found afterward with the help of a small look up table:
Code:
;54 cycles
_div24_mod24:
pha ; save original value
lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
tay ; integer division result
pla ; get original value
sec
sbc _multiply24,Y
rts ; Y = integer division result, A = modulo result
_multiply24:
.db 0,24,48,72,96,120
.db 144,168,192,216,240
If you don't want to trash Y, you could just multiply by 24 instead of using a look up table.
Code:
_div24_mod24:
pha ; save original value
lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
; C 76543210
sta intDiv24 ; x 0000xxxx integer division result (0-10 possible)
asl ; 0 000xxxx0 x2
asl ; 0 00xxxx00 x4
asl ; 0 0xxxx000 x8
sta TEMP
asl ; 0 xxxx0000 x16 (carry is always clear at this point)
adc TEMP ; x24
sta TEMP
pla ; get original value
sec
sbc TEMP ; A = modulo result, intDiv24 = integer division result
rts
Hope that helps!
Omegamatrix wrote:
tschak909 wrote:
The modulo is the problem here, given an integer divide by 24 such as:
Code:
;; void __fastcall__ div24(unsigned char d);
_div24: lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
rts
How can I determine a modulo? Is it sitting here in temp at some point?
-Thom
Modulo is easy, there are a few ways to do it. You'll have to figure out how to integerate it into C though. Here's a few ways:
Long slow division does not take too long in this case as 24 is a big divisor.
Code:
;24-94 cycles
_div24_mod24:
ldy #$FF
sec
_div24:
iny
sbc #24
bcs _div24
adc #24
rts ; Y = integer division result, A = modulo result
Using the unsigned integer division routine for div24, modulo24 can be found afterward with the help of a small look up table:
Code:
;54 cycles
_div24_mod24:
pha ; save original value
lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
tay ; integer division result
pla ; get original value
sec
sbc _multiply24,Y
rts ; Y = integer division result, A = modulo result
_multiply24:
.db 0,24,48,72,96,120
.db 144,168,192,216,240
If you don't want to trash Y, you could just multiply by 24 instead of using a look up table.
Code:
_div24_mod24:
pha ; save original value
lsr
lsr
lsr
sta TEMP
lsr
lsr
adc TEMP
ror
lsr
adc TEMP
ror
lsr
; C 76543210
sta intDiv24 ; x 0000xxxx integer division result (0-10 possible)
asl ; 0 000xxxx0 x2
asl ; 0 00xxxx00 x4
asl ; 0 0xxxx000 x8
sta TEMP
asl ; 0 xxxx0000 x16 (carry is always clear at this point)
adc TEMP ; x24
sta TEMP
pla ; get original value
sec
sbc TEMP ; A = modulo result, intDiv24 = integer division result
rts
Hope that helps!
OMEGAMATRIX! How are you, man? Good to see you over here from AtariAge. I just signed up here for my first NES project.
also, thanks for the fast divide routines. They're great.
-Thom
I'm well. Looking for to see this homebrew develop.
I'm getting closer, I've attached a ROM of the current behavior. It mostly works except in certain cases I can still go through walls, and am trying to figure out why this is:
code snippets:
Code:
/**
* handle_pad_idle()
*/
void handle_pad_idle(void)
{
// Change to idle state if pad is idle.
switch(stamps[STAMP_LAST_STATE(i)])
{
case STATE_PLAYER_RIGHT:
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT_IDLE;
break;
case STATE_PLAYER_LEFT:
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT_IDLE;
break;
case STATE_PLAYER_UP:
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP_IDLE;
break;
case STATE_PLAYER_DOWN:
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN_IDLE;
break;
}
}
/**
* handle_player_in_field()
* Handle when player is on the playfield
*/
void handle_player_in_field(void)
{
if ((stamps[STAMP_X(i)]==PIXEL_BOX_X(a)) && (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b)))
{
// We are aligned.
if (PLAYER_PAD_RIGHT(i) && !BOX_WALL_RIGHT(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_RIGHT;
else if (PLAYER_PAD_LEFT(i) && !BOX_WALL_LEFT(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_LEFT;
else if (PLAYER_PAD_UP(i) && !BOX_WALL_UP(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_UP;
else if (PLAYER_PAD_DOWN(i) && !BOX_WALL_DOWN(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_DOWN;
else if (PLAYER_PAD_IDLE(i))
handle_pad_idle();
}
else
{
// We are not aligned.
if (PLAYER_PAD_IDLE(i))
handle_pad_idle();
else if (PLAYER_PAD_RIGHT(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_LEFT)
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT;
else if (PLAYER_PAD_LEFT(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_RIGHT)
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT;
else if (PLAYER_PAD_UP(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_DOWN)
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP;
else if (PLAYER_PAD_DOWN(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_UP)
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN;
else
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)];
}
// Handle state movement
if (stamps[STAMP_STATE(i)]==STATE_PLAYER_RIGHT)
stamps[STAMP_X(i)]++;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_LEFT)
stamps[STAMP_X(i)]--;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_UP)
stamps[STAMP_Y(i)]--;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_DOWN)
stamps[STAMP_Y(i)]++;
// And set last state, if we aren't idle.
if (!PLAYER_PAD_IDLE(i))
stamps[STAMP_LAST_STATE(i)]=stamps[STAMP_STATE(i)];
}
/**
* handle_player_in_box()
* Handle when player is in box.
*/
void handle_player_in_box(void)
{
if (stamps[STAMP_XTRA_A(i)]>0)
{
if (stamps[STAMP_XTRA_B(i)] != 0)
{
stamps[STAMP_XTRA_A(i)]=0;
}
else
{
stamps[STAMP_Y(i)]=PIXEL_BOX_Y(6)-1; // 6 is the Y for the box.
if (sec==0) // 0 means approximately 1 second elapsed.
stamps[STAMP_XTRA_A(i)]--; // Decrement timer
}
}
else
{
stamps[STAMP_Y(i)]=PIXEL_BOX_Y(5); // Pop out of box.
if (i==0)
{
yellow_door_state=CLOSED;
}
else
{
blue_door_state=CLOSED;
}
}
}
/**
* move_players()
*/
void move_players(void)
{
for (i=0;i<2;++i)
{
get_current_box();
stamps[STAMP_XTRA_B(i)]=pad_poll(i);
if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(6)-1)
{
handle_player_in_box();
}
else
{
handle_player_in_field();
}
}
}
Some vars:
a=the x coordinate for box cell (0-9)
b=the y coordinate for box cell (0-5)
c=the entry in the dungeon table to get the walls for that cell
d=the value pointed to by c
PIXEL_BOX_X converts a to a screen pixel coordinate using a multiply by 24 and adding a center bias, effectively aligning sprite in the center of box.
PIXEL_BOX_Y is the same for the Y coordinate in b.
BOX_WALL_* are macros that check for a given bit set in D for a wall present in cell d
I'm so freaking close, it's %(#@@#(%@# grrrrr... heeeeeelp!
Of course, the code is in github, but I'm attaching a binary rom here in case somebody can derive the errant behavior:
Attachment:
wow-better-player-collision.nes [40.02 KiB]
Downloaded 90 times
-Thom
Adding this block in the aligned condition improves the collision detection to the point where I am no longer going through any walls, BUT, The player tries to change direction one box before the actual corner, which causes the player to not go through the wall, but hit the preceeding wall early and stop trying to round the corner. urgh...
Code:
if (PLAYER_PAD_RIGHT(i) && BOX_WALL_RIGHT(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT_IDLE;
else if (PLAYER_PAD_LEFT(i) && BOX_WALL_LEFT(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT_IDLE;
else if (PLAYER_PAD_UP(i) && BOX_WALL_UP(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP_IDLE;
else if (PLAYER_PAD_DOWN(i) && BOX_WALL_DOWN(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN_IDLE;
Attachment:
wow-better-player-coll-2.nes [40.02 KiB]
Downloaded 111 times
Setting it to last state on each if, replicates the through wall behavior, so it looks like I need to dig into last state setting.
-Thom
The lion-head enemies have a nice bounce to them.
rainwarrior wrote:
The lion-head enemies have a nice bounce to them.
Thanks, I am doing occasional tests on a front loader attached to a cheapo AV upscaler which is just butchering the field output, and would love to see a pic of this running on a real CRT. The NTSC emulation in various emulators just doesn't feel right at all.
I will say that it is on my list that the monsters will all eventually have have random frame starts, so they aren't all skipping in unison, but right now, I am trying to get player to wall collision happening correctly. It's almost there. grrrrr...
-Thom
It's been quiet here, as I've been sidetracked on doing some Raspberry Pi work, but I'm continuing back on this, as soon as I hit a stopping point with the work I'm doing on the Pi.
-Thom
and I'm back. Motion solved with the following code (to be optimized):
Code:
/**
* handle_player_in_field()
* Handle when player is on the playfield
*/
void handle_player_in_field(void)
{
if ((stamps[STAMP_X(i)]==PIXEL_BOX_X(a)) && (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b)))
{
// We are aligned.
if (PLAYER_PAD_RIGHT(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_RIGHT && !BOX_WALL_RIGHT(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_RIGHT;
else if (PLAYER_PAD_LEFT(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_LEFT && !BOX_WALL_LEFT(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_LEFT;
else if (PLAYER_PAD_UP(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_UP && !BOX_WALL_UP(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_UP;
else if (PLAYER_PAD_DOWN(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_DOWN && !BOX_WALL_DOWN(d))
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_DOWN;
else if (PLAYER_PAD_IDLE(i))
handle_pad_idle();
if (stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_RIGHT && BOX_WALL_RIGHT(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT_IDLE;
else if (stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_LEFT && BOX_WALL_LEFT(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT_IDLE;
else if (stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_UP && BOX_WALL_UP(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP_IDLE;
else if (stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_DOWN && BOX_WALL_DOWN(d))
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN_IDLE;
}
else
{
// We are not aligned.
if (PLAYER_PAD_IDLE(i))
handle_pad_idle();
else if (PLAYER_PAD_RIGHT(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_LEFT)
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT;
else if (PLAYER_PAD_LEFT(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_RIGHT)
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT;
else if (PLAYER_PAD_UP(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_DOWN)
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP;
else if (PLAYER_PAD_DOWN(i) && stamps[STAMP_LAST_STATE(i)]==STATE_PLAYER_UP)
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN;
else
stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)];
}
// Handle state movement
if (stamps[STAMP_STATE(i)]==STATE_PLAYER_RIGHT)
stamps[STAMP_X(i)]+=2;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_LEFT)
stamps[STAMP_X(i)]-=2;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_UP)
stamps[STAMP_Y(i)]-=2;
else if (stamps[STAMP_STATE(i)]==STATE_PLAYER_DOWN)
stamps[STAMP_Y(i)]+=2;
// And set last state, if we aren't idle.
if (!PLAYER_PAD_IDLE(i))
stamps[STAMP_LAST_STATE(i)]=stamps[STAMP_STATE(i)];
}
Also merged in a new cfg file and Makefile courtesy of polluks, which will work on newer CC65 compilers after 2.15.
Latest ROM here:
Attachment:
wow-working-player-collision.nes [40.02 KiB]
Downloaded 96 times
More to come shortly,
-Thom
is control okay for everybody? You should be able to move and round corners with ease. Let me know.
-Thom
Feels good to me. The bit where trying to walk into a wall makes you continue walking in the previous direction until you can turn strikes me as "surprising, but useful".
It's good. I like that you can hold diagonal and it will alternate. That's exactly how I'd want/expect it to work.
The only thing that's maybe weird to me is if I press up just a little bit then stop, now pressing either left or right will move me upwards until the next grid, which feels a little weird, but maybe not something that needs to be addressed at all. (It's easy to learn this quirk, and it resolves itself pretty quickly anyway.)
Like I think I'd probably make it so that you can't stop moving between grid points, only reverse direction, so there's never a situation where you could have stopped just a little bit out of grid and have left/right forced to move up/down. I don't know if you'd want to do that, though, I presume the original game did not. That's just what I'd do, because it seems like walking a little bit out of the grid is already a commitment to move in that direction, might as well complete it?
Right now the sprite overlaps right-hand walls by one pixel.
Also kinda wish that my feet didn't always point right when going up/down but that also seems to be that way in the original? (I suppose it would make just as much sense to be able to walk upside-down as well, but that one doesn't feel "missing".)
Ejection from the starting point seems weird (immediate pop out, and then one frame of changed direction), but I'll assume that's something you haven't worked on yet.
The rounding corners is quite fine.
But you've got some kind of serious glitch for the two players.
When player 2 changes to facing up or down, occasionally it affects player one. Let me know if you need an fm2 or something for debugging.
Edit: Oh, I appear to have missed the previous post saying there were problems with going through walls. Maybe the gif is useful for reproducing it, though.
WOW, that's a neat bug. I'll try to squash that today. Thanks for finding it!
-Thom
Ok, it has to be here, is there something stupid that I'm doing with regards to reading the player pads?
Code:
/**
* move_players()
*/
void move_players(void)
{
for (i=0;i<2;++i)
{
get_current_box();
stamps[STAMP_XTRA_B(i)]=pad_poll(i);
if (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(6)-1)
{
handle_player_in_box();
}
else
{
handle_player_in_field();
}
}
}
How is it generating
stamps[STAMP_Y(i)]? The thing I suspect is happening is that that equality stops being true after a WORRIOR has moved one pixel up, so it tries to handle their movement as though normally in-bounds instead of walking into the playfield, and says "hey you're outside where you should be" and clips the WORRIOR's Y, causing the other 23 to happen all at once.
This would happen if you're doing a modulo-24 to get "what's the current cell", as one guess.
rainWORRIOR wrote:
The only thing that's maybe weird to me is if I press up just a little bit then stop, now pressing either left or right will move me upwards until the next grid, which feels a little weird, but maybe not something that needs to be addressed at all. (It's easy to learn this quirk, and it resolves itself pretty quickly anyway.)
I guess he took my suggestion.
The macros that generate the appropriate data are here:
Code:
/**
* 8 objects on screen, two players and 6 enemies.
*/
#define STAMP_NUM_FIELDS 9 // 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 RADAR_SPR_OFFSET_X 88 // Radar sprite top-left offset X
#define RADAR_SPR_OFFSET_Y 176 // Radar sprite top-left offset 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_LAST_STATE(x) (STAMP_NUM(x)+4)
#define STAMP_FRAME(x) (STAMP_NUM(x)+5) // Stamp Field: Current frame
#define STAMP_DELAY(x) (STAMP_NUM(x)+6) // Stamp Field: Delay
#define STAMP_XTRA_A(x) (STAMP_NUM(x)+7) // Stamp Field: Extra A (Player Timer)
#define STAMP_XTRA_B(x) (STAMP_NUM(x)+8) // Stamp Field: Extra B (Player Pad Data)
#define PLAYER_PAD(x) (stamps[STAMP_XTRA_B(x)]) // Alias for reading stored player pad value.
#define PLAYER_PAD_RIGHT(x) (PLAYER_PAD(x)&1<<0) // is player pressing right?
#define PLAYER_PAD_LEFT(x) (PLAYER_PAD(x)&1<<1) // is player pressing left?
#define PLAYER_PAD_DOWN(x) (PLAYER_PAD(x)&1<<2) // is player pressing down?
#define PLAYER_PAD_UP(x) (PLAYER_PAD(x)&1<<3) // is player pressing up?
#define PLAYER_PAD_IDLE(x) (PLAYER_PAD(x)==0x00) // is player idle?
#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(x) (div24(x-STAMP_CENTER_BIAS_Y)) // Convert Stamp Y coordinates to Box Y
#define STAMP_X_TO_RADAR(x) RADAR_SPR_OFFSET_X+BOX_PIXEL_X(x)*8 // Convert box position to radar sprite position
#define STAMP_Y_TO_RADAR(x) RADAR_SPR_OFFSET_Y+BOX_PIXEL_Y(x)*8 // Convert box position to radar sprite position
#define BOX_WALL_RIGHT(x) (x&1<<4) // Box has right wall
#define BOX_WALL_DOWN(x) (x&1<<5) // Box has down wall
#define BOX_WALL_LEFT(x) (x&1<<6) // Box has left wall
#define BOX_WALL_UP(x) (x&1<<7) // Box has up wall
if handle_player_in_box(); is supposed to handle the entire walk out of the box, it's not got the right condition on it; it should be something more like if (stamps[STAMP_Y(i)]>PIXEL_BOX_Y(5)) (with a +1 in there on the RHS if ejection is how you handle walls rather than prevention)
handle_player_in_box is the routine that handles when the player is about to be ejected onto the playfield (with the countdown).
handle_player_in_field is used for everything else.
I may rename the former routine, if I can think of a better name for it... I was thinking "penalty box" like hockey
-Thom
to expand a bit, the macro PIXEL_BOX_Y takes a box # and converts it to a pixel offset (for sprite coordinates). Y=6 is a special box location that is only shared by the two penalty boxes.
STAMP_X and STAMP_Y are sprite pixel coordinates.
other bits of interesting code, handle_pad_idle() which handles when player motion stops:
Code:
/**
* handle_pad_idle()
*/
void handle_pad_idle(void)
{
// Change to idle state if pad is idle.
switch(stamps[STAMP_LAST_STATE(i)])
{
case STATE_PLAYER_RIGHT:
stamps[STAMP_STATE(i)]=STATE_PLAYER_RIGHT_IDLE;
break;
case STATE_PLAYER_LEFT:
stamps[STAMP_STATE(i)]=STATE_PLAYER_LEFT_IDLE;
break;
case STATE_PLAYER_UP:
stamps[STAMP_STATE(i)]=STATE_PLAYER_UP_IDLE;
break;
case STATE_PLAYER_DOWN:
stamps[STAMP_STATE(i)]=STATE_PLAYER_DOWN_IDLE;
break;
}
}
and handle_player_in_box():
Code:
/**
* handle_player_in_box()
* Handle when player is in box.
*/
void handle_player_in_box(void)
{
if (stamps[STAMP_XTRA_A(i)]>0)
{
if (stamps[STAMP_XTRA_B(i)] != 0)
{
stamps[STAMP_XTRA_A(i)]=0;
}
else
{
stamps[STAMP_Y(i)]=PIXEL_BOX_Y(6)-1; // 6 is the Y for the box.
if (sec==0) // 0 means approximately 1 second elapsed.
stamps[STAMP_XTRA_A(i)]--; // Decrement timer
}
}
else
{
stamps[STAMP_Y(i)]=PIXEL_BOX_Y(5); // Pop out of box.
if (i==0)
{
yellow_door_state=CLOSED;
}
else
{
blue_door_state=CLOSED;
}
}
}
sec is a variable that simply counts from 49 to 0 (I am using shiru's drop-frame consistent PAL tick rate).
get_current_box() is a function that sets a quad of variables, a,b,c,d:
Code:
/**
* get_current_box()
* Get the current dungeon box for player
* i = the stamp to return in a,b,c,d
* a = the X box
* b = the Y box
* c = the dungeon box #
* d = the box data.
*/
void get_current_box(void)
{
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];
score1[0]=div24(stamps[STAMP_X(0)]+8)+1;
score1[1]=div24(stamps[STAMP_Y(0)]+8)+1;
}
These are then used for collision detection. Since the whole maze is on a grid, and everything is bound to move along that grid, full bounding box collision detection is unnecessary for traversing the maze. (player to player/enemy detection will be another story)
I will also change the move_monsters() code to utilize the same code (with the exception that directions will be picked randomly), so that monster movement isn't so pedestrian. (Right now, monster movement is very rank and file).
It will also make computer AI easier, as all it needs to do is pick the closest monster, and attempt a straight line to it, while shooting when it's in the same lateral position on the grid.
Yes, I KNOW this code is really weird. I've never written C code that uses this many globals or goto's (and I've written CODECs!)... but you can't really write clean C code for a 6502 and have it fit or be somewhat efficient.
-Thom
oh, so the WORRIOR *is* supposed to instantly pop out of the box into the middle of the adjacent map cell? I thought they were supposed to walk in.
Myask wrote:
oh, so the WORRIOR *is* supposed to instantly pop out of the box into the middle of the adjacent map cell? I thought they were supposed to walk in.
You can witness the behavior by watching the arcade longplays on YouTube.
-Thom
tschak909 wrote:
Code:
#define STAMP_NUM(x) (x*STAMP_NUM_FIELDS) // Stamp Number
Side note: It's a good idea to get into habit of adding extra parenthesis around macro variables:
Code:
#define STAMP_NUM(x) ((x)*STAMP_NUM_FIELDS) // Stamp Number
Otherwise you can run into really nasty bugs with things like
STAMP_NUM(1+2) (expands to
1+2*STAMP_NUM_FIELDS).
Ah yes, good idea.
-Thom
Still debugging the glitch that Kasumi found, this one is a quantum bug, as it hides when being explicitly observed!
It's definitely a problem, and I'm suspecting that my i variable is being reset intermittently (and I mean very infrequently, I have yet to find a consistently reproducible pattern that causes this behavior!)
-Thom
Narrowed it down to the following bit of conditions:
* pressing up or down
* in a horizontal corridor (last state = left | right)
* state goes from not aligned to aligned
this one is weiird. I wonder what i'm doing to cause this funkiness?
-Thom
tschak909 wrote:
Myask wrote:
oh, so the WORRIOR *is* supposed to instantly pop out of the box into the middle of the adjacent map cell? I thought they were supposed to walk in.
You can witness the behavior by watching the arcade longplays on YouTube.
-Thom
Having looked at such…
The arcade game slides the WORRIOR into the playfield over ~0.1s (at least, 3 @youtube's 30FPS*, each with a new interpolated Y-value, so 6 frames on NTSC NES). Your version does not; it waits three frames after an input and then instantly moves the WORRIOR from inside the bullpen to the adjacent maze square.
Noted. I will try to refine this once I figure out WTF is going on with player 2 affecting player 1 motion.
-Thom
To anyone here, could use a second pair of eyes.
Man, this is bonkers, have been trying to debug the errant motion, and while I can reproduce it, I can't narrow down the errant code...
(for reference, code is here:
http://github.com/tschak909/wow.git)
If I do a diagonal motion, then the moment that I enter alignment into a given box, the yellow player will turn and idle in the selected vertical direction. This is downright goofy, as I do not modify the current player index at any point inside the loop, and yet, somehow i becomes 0 even when i is 1, and suddenly snaps back to 1.
I know this is because I am abusing globals, I'm trying to keep code generation down by not putting things in the stack frame...
-Thom
Oh my @#%(@# ... Wow, Finally squashed that one, turns out, it was in this part of the code:
Code:
void handle_player_in_field(void)
{
- if ((stamps[STAMP_X(i)]==PIXEL_BOX_X(a)) && (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b)))
+ if ((stamps[STAMP_X(i)]==PIXEL_BOX_X(a)) && (stamps[STAMP_Y(i)]==PIXEL_BOX_Y(b)))
{
// We are aligned.
if (PLAYER_PAD_RIGHT(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_RIGHT && !BOX_WALL_RIGHT(d))
- stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_RIGHT;
+ stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_RIGHT;
else if (PLAYER_PAD_LEFT(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_LEFT && !BOX_WALL_LEFT(d))
- stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_LEFT;
+ stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_LEFT;
else if (PLAYER_PAD_UP(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_UP && !BOX_WALL_UP(d))
- stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_UP;
+ stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_UP;
else if (PLAYER_PAD_DOWN(i) && stamps[STAMP_LAST_STATE(i)] != STATE_PLAYER_DOWN && !BOX_WALL_DOWN(d))
- stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(d)]=STATE_PLAYER_DOWN;
+ stamps[STAMP_STATE(i)]=stamps[STAMP_LAST_STATE(i)]=STATE_PLAYER_DOWN;
else if (PLAYER_PAD_IDLE(i))
- handle_pad_idle();
+ handle_pad_idle();
I was accidentally using (d) as an index into the stamps table, which in this case, literally was returning 0 if the opposing player had just aligned with the box.
*BLUSH*
welp, we all make mistakes, don't we?
Now it's on to shooting the phasor cannons and then to implement monster to cannon and monster to player collision. (I need to study the last one, it's not a bounding box detection, nor is it pixel detection, but I suspect it literally is a direct box alignment, need to observe.)
**WHEW**
-Thom
my OCD has gotten the better of me, and I'm spending the next couple of days shifting the maze downward, so that it can fit properly within NTSC overscan. which in my case means a minimum of one tile shift, but I'm doing two, so that the bottom of the dungeon will meet flush with the dungeon text, like it does in the arcade.
This does mean, that I had to make copies of the tops of my A-Z tiles, and paint horizontal rules above them to match the bottom of the dungeon. This also means that there currently is some attribute clash that I am working out, but the result will look a LOT better, when I'm done.
once this is done, I will start to implement player shooting.
-Thom
Could you adjust the Y scroll instead?
lidnariq wrote:
Could you adjust the Y scroll instead?
Not in this case, I'll post a before and after, and you'll see why i'm doing this.
-Thom
I was going to post this earlier, but nesdev was being moved.
So, I've managed to shift the playfield a bit down so that it looks a LOT better (there isn't that goofy gap between the dungeon and the dungeon name, and the radar, anymore.). I also color coded the player countdowns, and implemented teleporting (both players, as well as the enemies can utilize the teleport, which, given that it has a delay, definitely makes things interesting.)
https://www.youtube.com/watch?v=qYrk4bJcSeo-Thom
Now i've got an interesting problem, I'm running out of CHR-ROM space for my sprites. Shit.
Basically, I need space for two things:
* The Wizard of Wor needs 36 CHR tiles (2x3, all different, can't reuse bits, 3 frames horizontal, and 3 frames vertical.)
* The between level messages need approximately 32 tiles a piece, of which there are basically four.
I've made the concession that the between level messages always wind up on their own screen. (instead of in the arcade where some of the messages appear over the playfield)
So basically, I need...two more CHR ROM banks. How can I best pull this off?
-Thom
I've got a solution in mind, but to pull it off, I need to know the following:
- Are there any pairs of enemies that never appear at once?
- How much background manipulation are you doing during vblank?
- How much PRG ROM space is free?
The Worluk and the Wizard of Wor appear only once on the playfield, no other enemies present.
I'm doing my background manipulation very much interleaved, I spin between four background updating states:
* score (28 tiles)
* teleports (6 tiles)
* Doors (12 tiles)
* Box timers (up to 4 tiles)
and the memory map:
https://i.imgur.com/ZoXgCe7.png .. which shows 14K of PRG left (but I haven't implemented the rest of gameplay yet.)
-Thom
p.s. And maybe the sprite CHR rom has enough space, because I calculated the space for all 6 different sprites, with 6 tiles each, with 6 possible states + 36 tiles for the player shooting, so that means 252 tiles... which once you add in the 3 radar dots and the laser, that's all the cells....wheee.)
You should be able to store the CHR data in PRG ROM and copy it to CHR RAM, following
these instructions. Then in levels with normal enemies, you can load their tiles, and in levels with the bosses, you can load their tiles.
tepples wrote:
You should be able to store the CHR data in PRG ROM and copy it to CHR RAM, following
these instructions. Then in levels with normal enemies, you can load their tiles, and in levels with the bosses, you can load their tiles.
Cool! Thanks!
-Thom
Am A/Bing my game/vs the arcade...not happy with the walk cycles.. going to mess with the delay and the frames so that the walking tweens are close enough.
Yeah..yeah...procrastinating on the shooting...but this is part of me trying to make sure what I have so far will withstand later testing and polishing.
Some may think this is nuts, but I _really_ want this to be good.
-Thom
Am curious.
What do you guys do, to rekindle motivation? This year has been very stressful, and I'm having difficulty jumping back in to work on this, even though, I really want to see this completed. What do you guys do when you're faced with a deficit of motivation?
-Thom
Quote:
What do you guys do, to rekindle motivation? This year has been very stressful
Don't i know it. What i don't know is the proper thing to say here, but...
I do find it helpful to collaborate on projects. Not as much because it's a joint effort but because it feels more "real" than when doing something alone, and because i'm held accountable, which feels good in my case. The realness might be important or nonimportant to you, but it's a theory (i think it was Hegel perhaps?) that people need to "mirror" themselves in their productive/creative output in order to not feel alienated. Having someone you for certain know is interested in the project means you can help each other mirror back and forth. My individual projects may sag behind, but that's ok to me as long as i do
something with that sense of realness and recreation. I don't think everyone needs a partner like this, but i think that mirroring (even for oneself) and self-creation is pretty important. Does that resonate with you in some way?
Though if the underlying problem is too much background stress and it won't go away on its own sometime soon, i think the solution might be a structural one. No easy way around that. Not that it's an either or case, thankfully.
If tingering with wizards of wor gives you relief or has given you relief at some point from that stress, then that's good.
What made you think about porting it in the first place?
I keep my motivation by making the tasks on my todo list smaller.
Code:
Make enemy one
Make enemy two
Might be overwhelming.
Code:
Make enemy one
Add timer to stop it from smiling before attacking
Add data for its attack
create a new attack state
make it slide while attacking toward the player
Make enemy two
It's may seem more stressful because it's
longer, but you're much more likely to be able to "add a timer" than "make enemy one" in one sitting. And sometimes the reason I don't do something is like, "Well, I don't have time to do *task* right now, so I will do nothing." This makes *task* smaller, so it's more likely to fit in a smaller block of time.
Edit:
I guess it also helps to define what you actually want. Sometimes you think you know what you want for enemy one, but the vagueness of what's in your head is actually what's stopping you from making it. The small tasks force you to define what's needed in a real way. "Add ladders" vs "Handle these 5 real cases ladders will need." You're forced to think about the problem.
kasumi wrote:
but the vagueness of what's in your head is actually what's stopping you from making it.
This is so relatable, in my experience.
Also with a bit of luck, doing the tiniest/least threatening task you can find sometimes starts off an avalanche of creative pushes. Don't force it. A tiny task here, another there. Maybe with a lot of time in between, maybe not. When the avalanche comes, you rarely realize you're in it before the midpoint.
Damn, Kasumi...you might be right
-Thom
What I tend to do is take the next major task and then repeat the following iteration:
1. Find the first task on the list that I expect to take longer than 15 minutes.
2. If one exists, divide it up into smaller tasks.
3. If a task was split, and there are fewer than 12 tasks, go to step 1.
if I could, I'd hug you guys.
Thanks.
-Thom
Am back on WoW, have been so busy lately, and health issues caused me to have to take a break.
Right now, I am refactoring the code, and splitting bits out into seperate files.
In the main .h, I have a set of variables defined in my zeropage bss segment. The problem seems to be when I try to use them in other files. It seems:
Code:
#pragma bssseg (push,"ZEROPAGE")
extern unsigned char spr,i,frame_cnt;
#pragma bssseg (pop)
Produces an address mismatch:
Code:
ld65.exe: Warning: Address size mismatch for `_frame_cnt': Exported from wow.o, wow.s(25) as `zeropage', import in attract_monsters.o, attract_monsters.s(31) as `absolute'
ld65.exe: Warning: Address size mismatch for `_i': Exported from wow.o, wow.s(18) as `zeropage', import in attract_monsters.o, attract_monsters.s(30) as `absolute'
ld65.exe: Warning: Address size mismatch for `_spr': Exported from wow.o, wow.s(24) as `zeropage', import in attract_monsters.o, attract_monsters.s(29) as `absolute'
For some reason, it's ignoring my #pragma and using .import instead of .importzp :/ wtf? I know I'm doing something silly...
What's going on?
-Thom
err, turns out I literally had to do this:
Code:
extern unsigned char spr;
#pragma zpsym("spr")
extern unsigned char i;
#pragma zpsym("i")
extern unsigned char frame_cnt;
#pragma zpsym("frame_cnt")
Which is....really fucking awkward...but okay.
-Thom
Enough refactoring was done that I was able to start work on shooting the lasers for the player.
The logic has become quite complex, ensuring that all possible states are being taken care of (there are still a few stray state leaks that I need to plug up), resulting in very dense blocks of ternary conditionals...I will try to document these once they've stabilized.
I am currently using yellow's score to track his states (player left idle, player left, player left shooting, player left shooting idle, etc.), as well as his previous state, and whether the shooting latch is active.
There are still a ton of bugs, and I am working through them, but for now:
-Thom
First pass of player shooting implemented, now implementing monster shooting.
Latest binary here:
Attachment:
wow.nes [40.02 KiB]
Downloaded 142 times
I've now implemented the first pass of monster shooting.
I initially tried to be somewhat smart about monster shooting; only shooting if the monster sees a worrior in its field of vision. This wound up taking up too many cycles, slowing down the entire game, even if all I was doing was checking for the same X or Y box through wall boundaries, especially since I didn't want them to shoot continuously....so the implementation I wound up using was far more naive:
Code:
void monster_shoot(void)
{
// This is holy shit naive. The previous naive implementation took too much CPU time.
if (rand8()>0xC0)
{
if (((rand8())<0x08) && lasers[LASER_SHOOTING(i)]==0 && monster_laser_count<4)
{
monster_laser_fire(i);
}
}
if (lasers[LASER_SHOOTING(i)]==1)
{
get_current_laser_box();
if (lasers[LASER_DIRECTION(i)]==STATE_MONSTER_RIGHT)
{
if (BOX_WALL_RIGHT(h) && lasers[LASER_X(i)]==PIXEL_BOX_X(e))
monster_laser_stop(i);
else
lasers[LASER_X(i)]+=4;
}
else if (lasers[LASER_DIRECTION(i)]==STATE_MONSTER_LEFT)
{
if (BOX_WALL_LEFT(h) && lasers[LASER_X(i)]==PIXEL_BOX_X(e))
monster_laser_stop(i);
else
lasers[LASER_X(i)]-=4;
}
else if (lasers[LASER_DIRECTION(i)]==STATE_MONSTER_DOWN)
{
if (BOX_WALL_DOWN(h) && lasers[LASER_Y(i)]==PIXEL_BOX_Y(f))
monster_laser_stop(i);
else
lasers[LASER_Y(i)]+=4;
}
else if (lasers[LASER_DIRECTION(i)]==STATE_MONSTER_UP)
{
if (BOX_WALL_UP(h) && lasers[LASER_Y(i)]==PIXEL_BOX_Y(f))
monster_laser_stop(i);
else
lasers[LASER_Y(i)]-=4;
}
}
}
I will definitely have to be careful with the blue worrior auto-move implementation (when playing 1 player), as it's clear that I am low on non-NMI cycles.
Latest ROM here:
Attachment:
wow-monsters-shooting.nes [40.02 KiB]
Downloaded 134 times
I'm curious about how you were losing so many cycles on just a bounds check ... maybe memory pointers ?
lidnariq wrote:
I'm curious about how you were losing so many cycles on just a bounds check ... maybe memory pointers ?
I will revisit that later. I'm guessing that sometime soon I will need to do some significant code optimization. If you want to see where things are @ this point, the code as always is in github:
http://github.com/tschak909/wowNow i'm deliberating on whether to implement blue worrior ai logic or to implement the kill logic first.
-Thom
Initial pass of Worrior AI logic implemented. However, I need to figure out why the blue worrior's laser isn't showing...
-Thom
My 4yo daughter Nina helping me debug Wizard of Wor.
-Thom
Ok, so first pass of blue AI is implemented. It's very naive at the moment, it simply picks a monster and starts walking toward it, shooting intermittently.
There was an odd situation where update_lasers() was short circuiting an over eager check to see if a laser should update, which was causing blue worrior's laser to not be visible. Fixed...
The random number seed is currently reset every 5 seconds and the player X position is used, to try and randomize player positions.
Currently, since no kill logic is implemented, once the blue worrior catches up to his enemy, he simply keeps chasing it, close behind, shooting intermittently.
Once kill logic is implemented, I will alter this to try and shoot any monster in path while traversing toward enemy...
There is also a very occasional intermittent slowdown when multiple lasers are visible from enemies...need to start optimizing soon.
And yes, I know, some sprite priority issues. I need to actually work on a sprite scheduler, as I don't think I have the bandwidth to put the phasors into vram updates.
Latest ROM attached:
Attachment:
wow-blue-ai.nes [40.02 KiB]
Downloaded 139 times
-Thom
I've just drawn the explosion sprites. Am now implementing kill/explode logic:
I'm also fast running out of CHR ROM.
-Thom
Ok, major milestone, player to monster laser collisions implemented, along with explosions, monster death, and points.
Still some weird little sprite bugs that I need to track down there, as well. I'm having to tiptoe very well to make sure I don't run out of cycles doing e.g. bounding box comparisons, but it seems to be working ok, so far...
I'm happy this is starting to resemble a game.
Latest ROM build attached, and as always, code is available on github in the original post.
Attachment:
wow-monster-splosions.nes [40.02 KiB]
Downloaded 136 times
Moving onward.
-Thom
Am slowing down again, as I need to squash a whole bunch of bugs that have crept into the code.
-Thom
Wizard of Wor WIP: Worrior and monster collision detection fully implemented (both worriors and monsters able to shoot and kill each other, with appropriate points rewarded.) There still is at least one lingering bug with the laser code. But for now, I need to optimize all the bounding box checking code to gain back much needed cycles as the game slows down when everybody is on screen and shooting. Can't have that. Computer is playing blue.
And now, I need to take a break from new features, to drastically optimize the bounds checking code, as I am doing lots of multiplies and divides all over the code for ostensibly similar or same values. (at least I think), I need to do the calculations once, and just use them per frame, and that should free up more than enough cycles to finish the game play implementation.
Latest build is here:
Attachment:
wow-kills-splosions.nes [40.02 KiB]
Downloaded 365 times
-Thom
Wizard of Wor NES WIP:
I had initially planned to do three major optimizations. I have done two, and the result is dramatic. It seems I was at least spanning two or more frames worth of time to do my game logic. By simply re-arranging the game state arrays, and placing them into 6502 zero page, the game program logic as is, is running at full frame rate, speeding up by at least 200% ... WHAT A DIFFERENCE.
Basically before, I was building macros that did:
Code:
unsigned char stamps[NUM_FIELDS*NUM_STAMPS];
#define STAMP_NUM(x) (x*NUM_FIELDS)
#define STAMP_X (STAMP_NUM(x)+0)
#define STAMP_Y (STAMP_NUM(x)+1)
...
stamps[STAMP_X(i)]=new_stamp_x_position;
stamps[STAMP_X(i)]=new_stamp_y_position;
...
if (stamps[STAMP_X(i)]==... && stamps[STAMP_Y(i)]==... )
{
...
}
and so on...
Which was causing a 6502 software multiply (because no hardware multiply) on EACH AND EVERY read and write of game state, and I was doing this a total of about 220 times throughout the game logic.
I replaced this with:
Code:
unsigned char stamp_x[NUM_STAMPS];
unsigned char stamp_y[NUM_STAMPS];
...
stamp_x[i]=new_stamp_pos_x;
stamp_y[i]=new_stamp_pos_y;
...
if (stamp_x[i]==... && stamp_y[i]==...)
{
}
You can see, not only does this look cleaner, but it also runs much better, because the resulting calls literally become either direct X or indirect Y loads and stores. Which the 6502 loves to do..which is why I am KICKING myself for not doing it earlier. I KNOW this from doing 6502 assembler that it's better to keep arrays of the same data laterally together instead of in a c type struct or array, as it's simply an index change in the end.
I've pasted a copy of the latest ROM here, you can see it runs a fuckload faster, wowza!
Attachment:
wow-post-optimize.nes [40.02 KiB]
Downloaded 211 times
And of course, a GIF showing the new speed, it flies.. and I can now really start tuning the main game.
Damn, I feel good!
-Thom
LDA zpg,X is the same speed as LDA abs,X — at least as long as there's no zero crossing —so if you find there's memory pressure on zero page addresses you may be able to move arrays up.
now that everything is so smooth and zoomy-zoomy, I'm re-working the animation and delay routines to slow everything down, and slowly speed up as the level progresses (given a level #, adjust how fast the scaling happens, and the top speed value.)
This is happening in the initiial_tuning branch.
-Thom
Does anyone have a decent algorithm for a fractional delay? I need to apply both an animation cel delay, and a sprite position delay, and using frames for this seems to be too coarse.
-Thom
Add a 16bit number, but only use the high byte to display where it is.
Code:
lda poslow,x
clc
adc #$C0
sta poslow,x
lda poshigh,x
adc #0
sta poshigh,x
sta OAM,y
This will move the object a bit faster than one pixel every two frames (which would be adc #$80)
Kasumi wrote:
Add a 16bit number, but only use the high byte to display where it is.
Code:
lda poslow,x
clc
adc #$C0
sta poslow,x
lda poshigh,x
adc #0
sta poshigh,x
sta OAM,y
This will move the object a bit faster than one pixel every two frames (which would be adc #$80)
Thanks.
The problem I seem to be having, is that if I delay any amount, the delay seems asymmetrical, and I suspect this may be because of the code in the runtime that allows not only for detection of NTSC and PAL, but sets the same frame rate for both (50fps).. could this be the case? I'm going bonkers trying to see wtf is going on so I can do appropriate speed tuning.
-Thom
You game appears to skip running logic every sixth frame, on NTSC.
So on NTSC:
5 gameplay frames are run for every 6 "real" frames.
50 gameplay frames are run for every 60 "real" frames.
At 60 frames per second (close enough), 50 gameplay frames for every second.
And on PAL:
5 gameplay frames are run for every 5 "real" frames.
50 gameplay frames are run for every 50 "real" frames.
At 50 frames per second (close enough), 50 gameplay frames for every second.
So yes, your game is attempting to match NTSC and PAL gameplay speed. I'm unsure of if you're asking this question because you weren't aware it was doing that at all, or if you were totally aware and just want to do it a different way. (Or you don't want to do it at all, and want both versions to run 1 gameplay frame for every "real" frame with the NTSC character moving 60 pixels per second and the PAL character moving 50 pixels per second.)
I'm simply trying to determine why if I use e.g. a delay counter that decrements every 'frame' that I am seeing some frames go faster than others.
-Thom
Here's the code in _ppu_wait_frame:
(Comments mine)
Code:
lda #1;Tell the NMI the vram buffer is totally (rather than partially) updated (presumably)
sta <VRAM_UPDATE
lda <FRAME_CNT1;Load a counter changed in the NMI (presumably)
@1:
cmp <FRAME_CNT1;Compare to what's in A. When the NMI changes this, it'll be different
beq @1;and we'll stop looping
lda <NTSC_MODE;Assuming PAL is zero, we're done
beq @3;And branch
;If NTSC (non zero presumably)
@2:
lda <FRAME_CNT2;We check if this frame is a multiple of six
cmp #5
beq @2;If it is, keep waiting until it's not.
@3:
rts
So if you want it to not do that, you could do this:
Code:
lda #1
sta <VRAM_UPDATE
lda <FRAME_CNT1;Load a counter changed in the NMI (presumably)
@1:
cmp <FRAME_CNT1;Compare to what's in A. When the NMI changes this, it'll be different
beq @1;and we'll stop looping
rts
in theory. But that may have other effects, since I'm not too familiar with neslib.
ok, replaced my BOX_PIXEL_X and BOX_PIXEL_Y multiply by 24 macros with a straight table lookup, and this seems to have made everything extremely smooth, if fast. Debating on whether or not to replace the div24 routine, which is very fast, anyway.
-Thom
Looks like with removing the multiplies, things are smooth now that I am applying two types of delay, animation delay, and move delay. I can now build a set of tables to scale those up per level.
With this and the current tuning that I've done for laser speeds and player movements, I just need to implement monster speed scaling, and it'll be good for the first pass of tuning.
CC65's generalized multiply routines, are, understandably slower than grandma stuck in molasses in January going uphill in a fucking ice storm.
-Thom
If you are tight and want to ditch tables, notice that N*24 = N*8+N*16, or (N<<3)+(N<<4).
Temporarily on pause, as I have been pulled into an archaeological project bringing Atari computers onto the one surviving PLATO installation (cyber1.org)
-Thom