JSnakeDevDiary
From Retrosoftware
(→Food over lap bug) |
|||
| Line 329: | Line 329: | ||
| - | + | {| | |
| - | + | |-valign="top" | |
| - | + | |{{#ev:youtube|NqqqVCGdnlQ}} <br clear="all" />'''JSnake WIP'''<br />''Posted: April 28, 2012''<br /><br /> | |
| + | |- | ||
| + | |} | ||
Revision as of 01:39, 28 April 2012
Contents |
JSnake Development Diary
by jbnbeeb
Discuss in the related forum thread here
Introduction
As a kid, I was proficient in BASIC on the Beeb, but never really got to grips with 6502 assembler. Since getting into retro computing in the last couple of years, I've seen a lot of "homebrew" games on many platforms (not least those on RetroSoftware) and have been inspired enough to have a go myself. I've started learning 6502 assembler, and picked a simple Snake clone as my first project. I believe Snake is the easiest game to code, and is therefore the best place to start, rather than going for the something that's too ambitious for an assembler beginner. JSnake will start off as a very simple game, and will hopefully be expanded as I learn.
18/02/2012
I started from scratch a couple of weeks back. This first entry is a review of where I've got to thus far. I daresay subsequent entries may be more brief as the changes will likely be smaller, incremental steps.
First steps: plotting pixels to the screen
It seems to me that both the pleasure and the pain of coding in assembler is that everything is done in tiny baby steps, i.e. it often takes a long series of (short) 6502 assembler instructions to accomplish something, whereas in a high level language, only one or two lines of code may be needed to do the same thing. I found myself returning to this thought many times as I worked out my first goal: plotting a series of pixels to the screen in a horizontal line. Yes, it's a very humble goal, but I've found that taking many small steps has resulted in achievable results, which gives me incentive to continue.
I learned that MODE 2 would likely be the easiest mode to use to move pixels around the screen. Why? Because in MODE 2, a byte in screen memory represents two pixels.. so you ought to be able to move two pixels at a time by manipulating bytes. It's genereally easier (and quicker) to add and subtract bytes than it is to manipulate bits in a byte.
Kees van Oss succintly describes MODE 2 and 5 pixels in his dev diary for Atom Galaforce. I've borrowed his diagram to illustrate:
His full dev diary for Atom Galaforce can be found at: http://www.retrosoftware.co.uk/wiki/index.php/GalaforceAtomDevDiary
Before even writing any assembler, to test the idea that simply storing a value in memory would result in pixel/s being drawn on the screen, I typed this:
>MODE 2 > ?&4000=&0C
This will plot two green pixels (horizontally) to the screen.
I then read about Beeb screen addressing with horror. Knowing that MODE 2 is a 20K screen mode starting at &3000, ending at &7FFF - I foolishly assumed that addressing would simply be consecutive bytes starting from top left of screen until bottom right is reached.. ie top line of screen would be &3000 to &304F, 2nd line &3050 to &30A0 etc. Sadly, it's more complicated. Instead, BBC screen modes (apart from 7) are based on a series of 8x8 pixel blocks running horizontally, left to right (and then on to the next row) from the top left of the screen down to the bottom right. This is because the 6845 CRTC video chip used by the BBC is primarily designed to display 8x8 pixel characters. This is a pain if I want to move a set of pixels across the screen as I can't simply add one to an address each time I want to write to a pixel to the right, for example.
This is illustrated below, where I want to write 8 pixels to the screen. Each box represents 8x8 bytes in MODE2, where each byte stores 2 pixels. Note that screen addressing in MODE2 starts at &3000, top left, but I've start at 0 for clarity. See how in this case, to plot 8 pixels I'd have to write to addresses 26, 27, 60 and 61. Not nice.
To make life easier, a handy routine could be written to return a screen address derived from X and Y coordinate input parameters (similar to the PLOT or DRAW BASIC instructions). Being such a newbie to 6502 I wasn't thrilled at the prospect of trying to work out such a routine at this stage. Fortunately, both assembler books I have referenced have such routines. Further, Steve O'Leary has written some liberally commented assembler routines that handle this issue and published it in the Sample Code section of this site: http://www.retrosoftware.co.uk/wiki/index.php/Calculate_Screen_Address
I chose to use Steve's code as it was more generic and easier to use than the examples in the assembler books - but the actual text in chapters 7 and 10 of "Mastering Assembler" and chapter 10 of "Creative assembler" are well worth the read for a very full explanation of screen addressing which helped me better understand Steve's routine.
Using Steve's Screen Address routine.. I was soon able to progress to these giddy heights:
Moving the snake around the screen
As mentioned earlier, I'm progressing in small steps.. so I'm not using sprites just yet. All I'm doing is plotting a 2x3 blob of green pixels to screen, as per this code snippet:
jsr ScreenStartAddress
lda #&0C ; &0C = 2 green pixels
eor (XYScreenAddress,X) : sta (XYScreenAddress,X); EOR &0C with contents of (screen)address. Store (i.e plot) result to screen address
sta snakehead_scrn_addr ; store EORed result of plotted pixel - ie it's colour. If it is green (&0C), no collision, otherwise, we have a collision.
inc YOrd
jsr ScreenStartAddress
lda #&0C
ldx #0
eor (XYScreenAddress,X) :sta (XYScreenAddress,X)
sta snakehead_scrn_addr+1
inc YOrd
jsr ScreenStartAddress
lda #&0C
ldx #0
eor (XYScreenAddress,X) : sta (XYScreenAddress,X)
sta snakehead_scrn_addr+2
dec YOrd
dec YOrd
So to move the snake, I have a routine called Move, which contains the above snippet. Move is called repeatedly from a loop. The snake is just a series of 2x3 pixel blobs which are plotted using above code.
The snake moves in increments of 2 pixels. The easiest way to animate the snake movement is to append a "new" head (pixel blob) to the existing one, and erase the tail pixel blob. I've reserved 256 bytes in memory (I may yet need more, but for now 256 bytes is easy to work with) to store X and Y ords for each pixel blob that forms part of the snake. This means I can store up to 128 X,Y coord pairs, each representing the top left pixel of a 2x3 blob.
The 256 bytes are used as a circular list. I have a snake tail pointer and the snake head pointer, which point to the addresses in the list that record the X and Y coords of the tail pixel blob and head pixel blob. This way, it easy for the Move routine to look up and update tail and head locations.
Collision detection
Once I had the snake moving around the screen, I also created (and cribbed) some more routines. I nabbed some of the keyboard routine from HyperViper and plotted some food to the screen. The food is currently a yellow square :)
Next, I need collision detection to check for:
- collision with self
- collision with food
- collision with screen boundaries
The routine probably needs updating. I've checked collision in two ways -
- I check the resultant colour of the head pixels following the EORing of head pixels to screen. If green, no collision (the snake is green), if red then collision with food, otherwise collision with self. If none of the above, then on to next collision check
- Check to see if snake head touches screen boundaries by comparing x,y ord of head with ords of screen edges
Routine is shown below. Would welcome critique from the experts amongst us (be gentle though :) )
ldx #3 .CollisionLoop dex lda snakehead_scrn_addr, X cmp #&0C ; head is green - no collision beq decCLcounter cmp #&03 ; check if head has hit yellow - collision with food bne CheckSelf lda #HitFood sta CollisionState rts .CheckSelf lda #HitSelf sta CollisionState rts .decCLcounter txa bne CollisionLoop jmp therest ; no collisions to food or self so jmp to more checks .therest ; check for collision with scrn boundary dec snakehead_ptr lda (snakehead_ptr,X) cmp #LBound beq CDEnd cmp #RBound beq CDEnd inc snakehead_ptr lda (snakehead_ptr,X) cmp #UBound ; problem here - each snake seg is 3 pixels deep.. so when snake moves up, y Ord is decreased by 3.. which means when we beq CDEnd cmp #BBound-3 beq CDEnd lda #HitNothing sta CollisionState rts .CDEnd lda #HitScrnBoundary sta CollisionState rts
Taking stock. Pseudocode to plan the rest of the code
At this point, I thought I'd better plan the rest of the routines I'd be needing. Here it is for better or for worse.
First draft pseudocode with approximate structure and routines. Routines with * are those that I haven't started on yet
.GameState
{
jsr.StartScreen*
jsr .InGame
jsr .HiScore*
.Ingame
{
jsr Init
jsr SnakeLive
.Init
{
jsr SetupGameScreen
jsr Draw Boundary *
jsr Draw Banner *
jsr Init Snake
jsr Init Score *
jsr Plot Food
}
.SnakeLive
{
.SnakeMoving
{
jsr.Keypress
jsr .Move
jsr .Collision Detect
If Food,
jsr .EatFood.
jsr .GrowSnake
jsr .UpdateScore*
jsr .UpdateBanner*
(loop if no collision
}
jsr .Crash
dec Lives
jsr Update banner*
}
(loop around Ingame until Lives = 0)
}
}
jmp Gamestate (ie after Hiscore table, go back to opening "welcome" screen of game)
.Draw Boundary*
.Draw Banner*
.EatFood*
.UpdateScore*
.UpdateBanner*
.RandomNumber
.PlotFood
.Crash *
<do a check to make sure food not colliding with anything before it is plotted>
.HiScore *
29/02/2012
OK, at this point, I had a moving snake, a yellow square of food and some rudimentary collision detection. What next? Well.. the food didn't even get replotted on screen once eaten, and it was plotted to the same place everytime.
Next goal was to plot food randomly on screen, grow the snake each time food is eaten and lose a life if it collides with self or boundary.
Randomness
Once again, I've nicked a routine! Random Number Generator by Richard Broadhurst works well .. though unmodified, it will create the same sequence of numbers every time. However if you jump to the related forum thread you will find a top tip from Richard Talbot-Watkins : "If you weren't worried about reproducibility, you could EOR with a timer at the end, e.g. EOR &FE44, which should add a bit more randomness!"
This worked a treat, I had randomness :)
So I could now create random XY coordinates from which to plot the new bit of food.
Making the snake grow
This is fairly trivial. I set a "Grow snake" counter to 4 (currently). If the counter > 0 , each time I enter the Move snake routine, I decrement the counter and skip the "erase tail" subroutine and jump straight to the bit where I'm going to plot the new head location. This way, having eaten a bit of food, the snake will increase in size by 4 blobs.
lda GrowSnakeCounter beq WipeTail dec GrowSnakeCounter jmp GetOldHead
Lives
Lives and snake grow are still a bit hack-y at the moment. Growsnake is just fixed at 4.. probably needs to vary - depending on food size like Acornsoft Snake?
Lives at the moment is simply the in-game loop (following a collision that is not food) looping 3 times until a counter = 0! THis was just to test lives working. I ought to have a proper routine which updates score, and banner etc.. so I will go on to write these routines next..
And that's where I'm currently at!
28/04/2012
Update
OK, since I last updated the diary at end of February, I’ve continued to work on Jsnake in bursts here and there. I’ve done quite a bit. The bare-bones game is mostly done, but there are still some bugs to fix. After they're fixed, I’ll start on what I’m calling the “window dressing” – nice to have stuff that will make the game more playable or prettier.
Here is a list of bugs I've encountered. Most are fixed
id desc 1 food boundary food overlaps screen boundaries. '''FIXED''' 2 food overlap food can be plotted over the snake. It shouldn’t be. '''FIXED''' 3 opposite dir die if you press opposite key to direction you are going in , you die. Harsh. '''FIXED''' 4 boundary needs to be defined properly Play area overlaps score banner 5 snake size limitation 256 bytes = 128 2x3 blobs. Not long enough snake. A kilobyte would = 512 2x3 blobs '''FIXED''' 6 eat food not += when eating food, growsnakecounter needs to be += food, not = '''FIXED''' 7 No pause between lives lost '''FIXED''' 8 still snake size limit snake is bigger (1kbyte).. Going >512 snake segs,pointer "wraps around". No range check /reset.. 9 snake speeds up snake goes faster when some direction keys are held down.
Bug id 9 is the one that's bugging me the most. I have had some useful suggestions in the forums to investigate, but still haven't managed to fix. I'll upload the source code and an ssd soon. I'll continue to try and fix the bug myself, but if stuck I shall once again cry for help on the forums :/
Food over lap bug
Bugs 1 and 2 was an interesting bug to tackle. I'm using EOR (exclusive or) of pixel colours for collision detection. Very primitive. The food is yellow. If I EOR yellow with black, result is yellow. If yellow is EORed with yellow, then I get black (useful in another routine for "removing" the food when it is eaten). If yellow EORed with any other colour, I get another colour.
So my solution to prevent plotting the food over the snake's body is to EOR food pixel colour with the screen, but I don't plot the pixels yet. If the result of the eor operation for any food pixel is anything other than yellow, I jump back to the top of the plot food routine which will choose another random area of the screen. This continues until an area of the screen is found that the food will fit on without plotting over any non-black pixel.
Here is a list of features that I'd like to implement:
id Feature title description f1 frame screen boundary draw a coloured frame around the game play area '''DONE''' f2 keep highest score f3 high score table f4 joystick support f5 nicer food make the food more pretty than just a square f6 speed up after x points, snake goes faster f7 different food sizes different sized food and different score and growth increment for each size
| JSnake WIP Posted: April 28, 2012 |

