Skool Daze

Want to talks about games you like, would like to see developed on the Oric, it's here.
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Skool Daze

Post by Chema »

Hi all.

You may already know that, after experimenting in other areas such as isometric or 3D games, I was interested in digging deeper in other areas. One of those areas was tile-based games (I mean 2D) with many sprites around because I always wondered how they did that on the old 8-bit computers, mostly on those lacking hardware sprites.

The other area I was interested in was how to implement complex AIs (basically scripted ones) and handle them in such a game, beyond simple basic and repetitive movements or actions.

And there is one game that has both, which I enjoyed playing a lot and which is also quite well documented (there are commented sources on the web): Skool Daze.

For those not knowing about it (it was very popular in the Spectrum, but not so on the C64, even if such a version exists), have a look in youtube for some videos or google it to find some reviews and screenshots.

In the game you are a student in a typical school of the old days, and you suspect your report is not going to be nice this year, so you decide to steal it (the process is quite strange, starting for hitting the shields with your catapult or with your hand). And you have to do this while behaving and attending your schedule of classes, or you will be given lines as punishment. If you reach 10000 lines, you will be expelled and the game is over.

In addition you have to survive your mates: stereotypes such as a swot or a bully, who will make your task a bit more difficult.

The game is very addictive and full of very nice details, a real masterpiece released from Microsphere.

So I started working on it, with the idea of experimenting if such a thing could be done in an Oric. I was quite reluctant of posting anything about it, because I am not sure if I will ever be able to make something decent, but as the forum has been quite silent lately, I think I'd better change my mind.

Even I may start writting some posts about the technical details of how things are done, if I am in the mood :)

You know that I always leave the graphic details for the latest stages, and I am far from that, so the next two screenshots are not intended to be eye-candy, but to show that many many things are already programmed and working. In fact they are nearly monochrome... They should be transformed into something nicer with AIC mode... we will see...

Image
Image

There are many things yet to be done: you are not given lines for many actions (such as not being where you should be), you cannot hit shields (so cannot open the safe), many "special" things are still missing and the whole thing needs tweaking, but the engine is working and the (really impressively complex) AI system works!

So, all in all I am learning and having a good time with this... :)

Cheers
User avatar
ibisum
Wing Commander
Posts: 1643
Joined: Fri Apr 03, 2009 8:56 am
Location: Vienna, Austria
Contact:

Post by ibisum »

Chema - you make me so excited to see things like this being developed on the Oric! Honestly, where do you get the time? I wish I had time for some coding like this .. WOW!
Antiriad2097
Flying Officer
Posts: 158
Joined: Tue May 09, 2006 9:42 pm
Location: Aberdeen, UK
Contact:

Post by Antiriad2097 »

Its looking great Chema. Might I suggest a mono mode is available, as it looks very crisp in its current form. Colour is nice, but as many Spectrum games show sometimes the lack of colour can work in your favour.

Knowing the game, I'm sure there's still some easy options to splash colour around on less busy parts of the screen where there's no character movement (the shields for example).

Looking forward to updates with this one, its another of those landmark games that can be used to show the Oric can hold its own against the other 8 bits.
User avatar
kamelito
Flying Officer
Posts: 182
Joined: Sun Jan 08, 2006 6:34 pm
Location: Nantes, France

Re: Skool Daze

Post by kamelito »

Hi Chema,

Is AIC mode explained somewhere?
Thanks
Kamel
/kml
skype pseudo : kamelitoloveless
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Re: Skool Daze

Post by Chema »

kamelito wrote:Hi Chema,

Is AIC mode explained somewhere?
Thanks
Kamel
You should ask Twilighte... the Master of Oric Graphics... :)

Basically it is having the ink colour changing every other row and cleverly using the inverse bit to get more colours with no other attribute codes in the screen. Something like alternating cyan/green (there are other combinations), leaving paper as black and using the inverse bit to obtain red/blue.

I was never able to do anything decent with this, but Twilighte seems to have a special Gift, and is able to do wonders with that, such as the score panel in 1337, the object pictures in Space:1999, or all the wonderful graphics in IM or Stormlord.

I was only able to use it to make the crosshair in 1337 (which has red color on it without clashing with the rest of graphics) or the box you can see above (when the teachers give lines), which needs no attributes... just set the inverse video and my black ink gets white and the cyan paper gets red with no problems or clashing.

Oh, and about getting a mono mode... I agree that sometimes mono graphics may look prettier, but maybe it is possible to add some patterns and basic colors using AIC without destroying the detail... Will look into it later on.

Cheers
User avatar
kamelito
Flying Officer
Posts: 182
Joined: Sun Jan 08, 2006 6:34 pm
Location: Nantes, France

Re: Skool Daze

Post by kamelito »

I think I already ask Twi on another thread, he was too busy, one day maybe he'll update the wiki.

Kamel
Chema wrote:
kamelito wrote:Hi Chema,

Is AIC mode explained somewhere?
Thanks
Kamel
You should ask Twilighte... the Master of Oric Graphics... :)

Basically it is having the ink colour changing every other row and cleverly using the inverse bit to get more colours with no other attribute codes in the screen. Something like alternating cyan/green (there are other combinations), leaving paper as black and using the inverse bit to obtain red/blue.

I was never able to do anything decent with this, but Twilighte seems to have a special Gift, and is able to do wonders with that, such as the score panel in 1337, the object pictures in Space:1999, or all the wonderful graphics in IM or Stormlord.

I was only able to use it to make the crosshair in 1337 (which has red color on it without clashing with the rest of graphics) or the box you can see above (when the teachers give lines), which needs no attributes... just set the inverse video and my black ink gets white and the cyan paper gets red with no problems or clashing.

Oh, and about getting a mono mode... I agree that sometimes mono graphics may look prettier, but maybe it is possible to add some patterns and basic colors using AIC without destroying the detail... Will look into it later on.

Cheers
/kml
skype pseudo : kamelitoloveless
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Well as this forum is quite silent, I'll start commenting out some internals about this project:

Chapter 1: Choosing the tile size
With a modern PC a 2D game such as Skool Daze, could be done by having the graphic of the School in memory and using a big backbuffer with the whole screen area. Sprites will be drawn there and a technique such as “dirty-rectangles” will be used to update the areas of the screen which need to be redrawn by simply dumping them from that backbuffer. This is not possible in an 8-bit computer of the old era, due to memory and speed constraints.

A tile-based game divides graphical data into small regions, called tiles, which are then rendered into the screen forming the playing area. This is an excellent way to minimize memory usage, as we will store just the identifiers of the tiles for a given position, instead of the whole graphic, as long as the graphic of the tiles is repeated enough times throughout the playing area. One can have a tile to represent a chair, or a section of a door or a wall or a tree…, and then repeat the graphic many times without actually repeating the graphical data.

This can be done in isometric games (as Space:1999) as well as in 2D games, such as it is done in Skool Daze.

Usually tiles are of a fixed size (though this is not strictly necessary) and a few big tiles will use little memory, but produce a quite repetitive play area, while a lot of small tiles will use more memory, but probably much richer graphics. Due to the Oric’s funky display, the tile width will be a multiple of 6 pixels (to use an integer number of bytes). Also to optimize the drawing a good tile height could be 6 pixels. This way we can use a 6x6 pixel tile, which is square and also can be rendered quite fast with a code such as the next one. Let the zero page variable tmp be a pointer to the screen position where we want to dump a tile graphic (which is stored as a set of 6 consecutive bytes starting at address labeled “backbuffer”:

Code: Select all

	ldy #0				
	lda backbuffer			
                sta (tmp),y				
	ldy #40	
	lda backbuffer+1
	sta (tmp),y				
	...
	lda backbuffer+4
	sta (tmp),y
	ldy #200
	lda backbuffer+5
	sta (tmp),y				
Of course what we will have in “backbuffer” is not just the background graphic – it will include any sprite portion that overlaps with that tile (more on that later). You can write more compact code, of course, but we will need all these drawing routines to blaze.

So there you are. Just design your graphics as composition of tiles (the more they are re-used, the lower the memory needs) and store the tile number (or identification) in the map, for instance as an array: char map[ntiles_x, ntiles_y] (that will give you as much as 255 different tiles). More on efficient storing and accessing will come.

The problem comes when trying to port an existing game. Most 8-bit computers have 8-pixels per byte of graphical data (such as the speccy), so expect those games to use 8x8 or 16x16 tiles (32x32 for the classical Filmation games, such as Knight Lore). And what does that mean, you may ask? Problems. That is what it means.

The playing area in Skool Daze is 768x168 (a total of 16128 bytes, way too much!), divided into a grid of 96x21 (2016 bytes) tiles of 8x8 pixels. Though we can split this image in tiles of 6x6, resulting in a grid of 128x28 (3584 bytes), tile borders will not match walls, floors and objects as you expect. So either you redesign graphics completely or you’ll have problems. And it can be even worse. A character is made of a grid of 3x4 tiles (24x32 pixels). The width is again a multiple of 6, but the height isn’t. Try to redesign such small graphics so they fit into 24x30… and even if the result is good, you’ll have other problems. The character position now is not in the second column, but half in the 2nd and half in the 3rd. And also you will have to update and move (and store) more data, so everything will be slower and the data size will also be bigger.

So what did I do? As I am not good at drawing graphics and I also wanted the final game to look as Skool Daze, I decided to use 6x8 tiles and made small tweaks to the school background graphic to make walls, stairs, etc. match tile borders. You can see the results in the pics of my first post: the characters are the same and the school is nearly the same (though internally I had to break my head to solve several problems).

Dumping a tile therefore becomes:

Code: Select all

	ldy #0					
	lda backbuffer			
	sta (tmp),y				
	ldy #40	
	lda backbuffer+1		
	sta (tmp),y	
	...
	ldy #240
	lda backbuffer+6
	sta (tmp),y				

	; Unfortunately, there is one scan left
	lda tmp					
	clc	
	adc #240				
	bcc nocarry				
	inc tmp+1				
nocarry
	sta tmp					
	ldy #40					
	lda backbuffer+7		
	sta (tmp),y


The whole process to update a tile (tx,ty) in screen involves a little more than just this. The backbuffer is used to avoid flickering and there are three main steps:

1- Find the tile graphical data from the tile id of the map (map(tx,ty)) and dump into the backbuffer. The pointer to the data is put in the lda instruction with self-modifying code to make it faster (smc_udgp+1,+2).

Code: Select all

	ldx #7
loop
smc_udgp
	lda $1235,x
	sta backbuffer,x
	dex
	bpl loop
2- For each sprite see if any of its tiles is at the tx,ty position and, if so, get pointers to both the graphic and mask of that tile and dump it into the backbuffer. Again self-modifying code is used to alter the addresses at smc_mask_p+1,+2 and smc_graphic_p+1+2 to gain some cycles:

Code: Select all

	ldy #7
loopcopy
	lda backbuffer,y

#ifdef AIC_SUPPORT
	bpl ScreenNoInverse
	eor #63
ScreenNoInverse
#endif

smc_mask_p
	and $1234,y
smc_graphic_p
	ora $1234,y
	sta backbuffer,y
	dey
	bpl loopcopy
In both cases the tilecode zero means “blank” and is special-cased. The code between the #ifdef AIC_SUPPORT #endif directives is the trick posted by Twilighte to support AIC mode coloring…

3- Dump the backbuffer at the screen position, as explained before.

And this is all basically. Quite fast in the end… this is something mandatory if you want to be able to move many characters in a big playing area at the same time.

Of course it is vital to be able to get the pointer to the graphic data as fast as possible for a given tile position, which means an efficient storage of the map in memory. Also, to avoid running out of memory a backbuffer of just one tile (8 bytes) will be used, and only tiles which need to be updated will be at each frame, and just once of course. But that will be explained on a subsequent post.

Don't hesitate on commenting... you can have a look at the sources in the repository here (file engine.s):
http://miniserve.defence-force.org/svn/ ... skooldaze/

Cheers.
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Chapter 2: Rendering the play area

Well, now that we know the tile size and how we are going to draw each tile on the screen, it is time to have a look at how we can update the play area as necessary. As I told in the post before, in a “modern” system we would probably draw everything on a big backbuffer and dump only those areas which changed. Usually this is done by calculating the bounding rectangles of anything that has changed, merging those which overlap, and then update those areas. This is the basis of a dirty-rectangle method, which is useful when only portions of the screen need to be updated (not for full-screen animation such as in 1337, where dumping the whole play area – or nearly – is needed).

However we would not probably afford the memory such a method needs. At least this is true for many games back in the 80’s. There is another approach that can be used, and which is used in Skool Daze.

We will only need space for one tile, as only one tile will be updated at a time. That is 8 bytes, not bad. The method is quite simple: we will need an additional bitfield map with as many bits as the visible tiles on screen. In this case 40x21 bits, or 8x21 bytes, that is 168 bytes. We will call this the Screen Refresh Buffer, or SRB, to follow the information on this link:

http://pyskool.ca/disassemblies/skool_daze/

which is, by the way, a great source of information about the game internals I am using.

After any operation that produces a change in the play area (such a character moving) we will mark with 1’s those tiles that need refreshing. As an example here is the routine to update the SRB for a single tile. A slightly different version is used when updating the tiles for a given sprite, which is more optimized. The tile coordinates are passed in registers X and Y and we have to take the scrolling of the play area into account.

Code: Select all

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Updates the SRB, marking the tile at coords
; X,Y as invalid
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
update_SRB
.(
	; Is the tile visible?
	txa
	sec
	sbc first_col
	bmi endme
	cmp #FIRST_VIS_COL
	bcc endme
	cmp #LAST_VIS_COL+1
	bmi doit
endme
	rts
doit
	; It is and the corrected coordinate is in reg A now
	; save it for a moment
	pha

	; Get pointer to the correct row of SRB
	tya
	sta tmp
	asl
	asl
	adc tmp
	adc #<SRB
	sta tmp4
	lda #>SRB	
	adc #0
	sta tmp4+1

	; Get the correct byte of the SRB row
	pla
	pha
	lsr 
	lsr 
	lsr
	tay

	; Reg Y holds the offset of inside the SRB
	; Now get the bitmask
	pla
	and #%00000111
	tax
	lda tab_bit8,x
	ora (tmp4),y
	sta (tmp4),y
	rts
.)
The above code checks if the tile is visible and then calculates the pointer to the correct SRB row in the zero page variable tmp4 (there are 5 bytes in each row, thus the SRB+(A*4+A) code), then gets the correct byte by dividing by 8 (the three lsr) and then the bit inside the byte, which calculates mod 8 with the and #%111 and uses a table (tab_bit8). The bit is finally set with the ora/sta pair.

So after a complete frame, we will know which tiles in the screen need to be updated. We will examine the SRB bits and update tile by tile.

In this case we also want to make sure that we don’t overwrite the area used by any speech bubble. This might require more explanation, but it was a bug in the original game. The text in the speech bubble is a scroller, and uses other methods for updating (which are much quicker). We don’t want any passing character or catapult pellet to be drawn over it (or its graphic will also scroll and corrupt the text).

Therefore, in the main loop, there is some additional code to keep those bits as zeros.

The render_screen routine in engine.s does this job. As we will have to calculate x div 5 and x mod 5 for this purpose, we will use two tables to speed up things.

The next chapter will explain how the tile map and graphic data are stored in memory to speed up access.

Comments and ideas are welcome, as usual :)
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Let's continue with more internal details then...

Chapter 3: Crazy memory map

If we want our game to be playable we need to be sure that our core routines (the graphic ones in this case) are as fast as possible. As you have imagined from the previous post, we will be accessing the tile map for the school graphic and the tiles used by sprites depending on their animation state quite often. We want to do this as fast as possible.

Certainly the map could be stored as an array char map(NUM_TILES_X,NUM_TILESY), but to access a tile at (x,y) that will require us to perform y*NUM_TILESX+x, a 16-bit multiplication (even if it will be easier in this particular case, as NUM_TILESX is 128) and an additional 16-bit addition with the map base address. Then we can access the tile code with lda pointer,x.

Too much, in my opinion. We can do better. Our map is composed of 21 rows of 128 bytes (columns). If we can page-align each of those 21 rows the code would be much easier. Imagine the first row is at address skool_r00 (the low byte of this address is 00). Accessing a row stored in tile_row (zero page) is simply adding it to the high byte of this address. If the column is in the zero page variable tile_col, it all becomes:

Code: Select all

	lda #>skool_r00
	clc
	adc tile_row
	sta smc_p+2		
	ldy tile_col
smc_p
	ldx $1200,y
And there you are. We have the tile code in register X (quite handy). We may now want to special-case if the tilecode is zero (empty) so it uses an efficient code to blank out the backbuffer with a simple beq.

Now that I am writing this I notice that there is room for a further optimization. We are considering the row number in the range 0-20, but in fact nobody prevents us from using 10-30 or any other range, as long as our code is consistent with it (something in row 10 will mean the top row, as if we were saying row 0). A simple #define FIRST_ROW 10 and using FIRST_ROW+0 or FIRST_ROW+3 to refer to specific rows could be enough. So what if we declare

Code: Select all

#define FIRST_ROW >skool_r00
Mmmm that is interesting, using the high byte of the address of the first row will let us remove the clc/adc code and put simply

Code: Select all

	lda tile_row
	sta smc_p+2		
	ldy tile_col
smc_p
	ldx $1200,y
Does that make sense? This saves another 4 cycles. And if tile_row (which is a zero page address) is preceded by a byte set at 00 it could even be simpler, with

Code: Select all

.zero
	tile_col	.byt 00
		.byt 00
	tile_row 	.byt 00

...
	ldy tile_col 
	lda (tile_row-1),y
	tax
And we save another 4 cycles. Maybe it is a worthy optimization for a future version.

Ok, we now need a pointer to the graphic data. As the first graphical data corresponds to tile 1 (zero means empty) we want to calculate base_graphics+(tilecode-1)*8 (8 bytes is the size of a tile, remember?). I will use a table here to speed it up even more. We can save the dex if we move the data 8 bytes so we could use code*8 directly. This is done with the sprites, but did not implement it with the background data yet.

An additional note. In Skool Daze graphics are quite rich, so we won’t have enough different tiles if we are using just 255, so we are splitting the whole school graphic in three areas, each one with different tile codes and graphics (although some will repeat, of course). For each section (udg_skool, udg_skool2 and udg_skool3) tile graphics are page aligned, therefore to get a pointer to the graphical data, we need to calculate the (tile_code-1)*8, a 16-bit number (using a table) and adding the high byte of the result to the high byte of the correspondent section of tile graphics, leaving the low byte as it is. The resulting pointer is stored on smc_udgp+1,+2 on the lda instruction using self-modifying code again. Remember the tile code is in register X, and the tile column is still on register Y, so:

Code: Select all

	dex
	lda tab_mul8,x
	sta smc_udgp+1

	cpy #33
	bcs sec2
	lda #>udg_skool
	bne gotit
sec2
	cpy #74
	bcs sec3
	lda #>udg_skool2
	bne gotit
sec3
	lda #>udg_skool3
gotit
	clc
	adc tab_mul8hi,x
	sta smc_udgp+2

	; here it is... now copy it to the backbuffer

	ldx #7
loop
smc_udgp
	lda $1234,x
	sta backbuffer,x
	dex
	bpl loop
	rts
Easy, isn’t it? But of course we pay a price for that smart memory map: memory fragmentation. We will have holes of 128 bytes in 21 pages! We will need to use those holes to store useful data, which is not easy. Therefor the complicated memory map you can find at data.s

And still there are holes of unused memory around, but I hope I will be able to fill them as the development progresses.

And what about the sprites? We don’t have a map for those!
JamesD
Flight Lieutenant
Posts: 358
Joined: Tue Nov 07, 2006 7:38 am

Post by JamesD »

If you have the memory space for it, you could have a table of pointers to your tiles. I guess multiple tables if different screens use different tiles.
As long as only 128 tiles are used / screen anyway.

Then you just need to multiply the tile number you are looking up by 2 (bit shift) and put it in the LSB of the pointer to the pointer table you are using for that screen. Then you just load and store two bytes from that table and store them in page zero to point to your tile which could be anywhere in memory as long as they don't cross a page boundary.

It would take memory for the pointer tables but lets you place graphics almost anywhere without leaving holes in memory.
JamesD
Flight Lieutenant
Posts: 358
Joined: Tue Nov 07, 2006 7:38 am

Post by JamesD »

Come to think of it, if a screen only had 128 tiles and you don't have too many screens to fit in memory, you could just represent the screen as a table of pointers direct to your tiles.
<edit>
Actually, you wouldn't be limited to 128 tiles this way but you'd have to deal with crossing a page boundary if you used over 128.
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Thanks for the idea JamesD, it is indeed very interesting. However, in this case I have more than 128 tiles per screen.

The school graphic is divided into three sections because of the number of different tiles it uses (which is way bigger than 256), so each section use its own set of tiles. That is why the cpy's are done:

Code: Select all

   cpy #33 
   bcs sec2 
   lda #>udg_skool 
   bne gotit 
...
Basically it changes the set of tile graphics depending on the column.

In fact, as the whole background scrolls and not simply switch between screens, you need to be able to access the graphic of any of those sets in each render.

Anyway the holes in memory are not due to the graphics, really. Only the start address of each of the three sets needs to be page-aligned. The big trouble comes from the map, where tile codes are stored.

But your ideas are very interesting and I will note them down for future reference for sure :)

Cheers
JamesD
Flight Lieutenant
Posts: 358
Joined: Tue Nov 07, 2006 7:38 am

Post by JamesD »

Since you are already using multiple sections, you could do a table per section, however many would bring the number of tiles down to 128 for a section. But then it wouldn't be very practical memory wise.
And since it does scrolling you'd be scrolling the pointers through the screen tables as well as keeping tables for the entire screen... which is starting to sound pretty ugly.
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Hi guys!

A quick note to state that life put a bit of pressure on me lately, so I had no time to work on this. But that does not mean I have quit the development. I will continue as soon as I can... maybe even I'll do something during my vacations on August.

Also soon I will post the next chapter about internal details, now concentrating on sprite drawing and soon moving into AI.

Cheers
User avatar
Chema
Game master
Posts: 3013
Joined: Tue Jan 17, 2006 10:55 am
Location: Gijón, SPAIN
Contact:

Post by Chema »

Hello everyone.

I am back from my holidays and I wanted to make a small post, just to say that I did work on this baby quite a lot. The AI is nearly finished and I am experimenting with other details.

And I must say that the more I add into the game, the more I admire the work made by the original programmers. This one is a little marvel indeed.

I will post more on this soon. Stay tuned :)
Post Reply