Tutorial game: Let’s render level (part 6)

Note: The source code for this tutorial is available in the tutorials/TutorialGame_Introduction/part6.ras (in the TRSE  framework), together with all resources mentioned in these texts.

We now assume that you have a character set, a nicely designed level and some rough knowledge on how Turbo Rascal & the c64 works. Now it is time to combine all these things together, and create a level renderer.

In order to to things properly, let’s set up a Raster IRQ loop. If you don’t know how this works, be sure to read the general-structure-tutorial and check out tutorial 13.

First, create a new ras file (like “part6.ras”). Let’s start with defining the variables that we will use.

program part6_RenderLevel;
var  

@define levelPosition $5000
@define charsetPosition $2000
@define useKernal 0

   	charset1: incbin("charsets/charset1.bin", @charsetPosition);
	colors, levelpointer, colormemory : pointer;

	i,j:byte = 0;
	levels: incbin("levels/level1.flf", @levelPosition);
	curLevel_x, curLevel_y : byte=0;

Note that we’ve used some global defines for the charset and level memory positions, as these will be reference later in the code. curLevel_x and curLevel_y references the current screen level to render.

We’ll be using three pointers here: one to reference the color location of the charset (colors, at $2800), a pointer that will point to the level data (levelpointer) and a temporary pointer that will be pointing to color ram ($D800, colormemory).

Let’s continue by combining all intialization stuff to a single procedure:

procedure Initialize();
begin
	clearscreen(LIGHT_BLUE, $D800);
	clearscreen($20, $0400);
	SCREEN_BG_COL:=BLACK;
	SCREEN_FG_COL:=BLACK;

	colors:=$2800;

	MULTICOLOR_CHAR_COL[1]:=GREY;
	MULTICOLOR_CHAR_COL[2]:=BROWN;

	setmulticolormode();

	setcharsetlocation(@charsetPosition);
end;

The initialization process should be identical the one in part 4. A quick reminder: the two multicolors + the background colors will be defined by the level editor, while the last (free) color is the one you used to draw the char in the char editor. This color was exported with the binary charset, and will reside at $2800. These are the values that ultimately will be written to $D800.

Now let’s initialize an empty raster function that is called whenever the raster hits line 0:

interrupt RasterRenderLevels();
begin
	StartIRQ(@useKernal);

	CloseIRQ();
end;

begin
	Initialize();
	DisableCIAInterrupts();
	SetMemoryConfig(1,@useKernal,0);
	RasterIRQ(RasterRenderLevels(),$00,@useKernal);
	EnableRasterIRQ();
	enableirq();
	Loop(); 
end.

Here, we start by calling our intialization method before temporarily turning off all interrups. While all interrupts are disabled, we set up the raster IRQ to point to RasterRenderLevels() when reaching line 0 with kernal turned off. When this is done we safely re-engange the raster to trigger interrupts. The RasterRenderLevels() will then be executed the next time the raster hits line 0. This should be a completely typical initialization for any TRSE program

Rendering the level: The easy part

If you’re not looking to understand how the simple level renderer works in detail, we’ll first cover the easy version.

First, before the “Initialize” method, include the raslib library “levels.ras” and set the color pointer:

@define colorpointer colormemory
@include "../RasLib/levels.ras"

Then read the level header at the end of the initialization routine (“Initialize()“) as such:

...
	levelpointer:=@levelPosition;
	ReadHeader();
end;

“ReadHeader” will read out and filll all the necessary variables for rendering the levels. Now finally, in the main method right after “Initialize()” is called, render the level at the current position (0,0) to screen memory $0400:

begin
  Initialize();

  levelpointer:=@levelPosition;
  RenderCharsetColorLevel(curLevel_x,curLevel_y,$04);

  DisableCIAInterrupts();
  ...

and you should end up with a nice view of your level screen rendered at (0,2) :

Let’s add the option of moving around the levels with the joystick. This is done in the Raster IRQ routine:

interrupt RasterRenderLevels();
begin
	StartIRQ(@useKernal);
	if (i=0) then begin
		joystick();
		if (joystickdown=1) then begin inc(curLevel_y); i:=10; end;
		if (joystickup=1) then begin dec(curLevel_y); i:=10; end;
		if (joystickleft=1) then begin dec(curLevel_x); i:=10; end;
		if (joystickright=1) then begin inc(curLevel_x); i:=10; end;
	end;

	if (i=10) then begin
		levelpointer:=@levelPosition;
		screenoff();
		RenderCharsetColorLevel(curLevel_x,curLevel_y,$04);
		screenon();
	end;
	if i<>0 then dec(i); // i acts as a counter

	CloseIRQ();
end;

You should now be able to move around your levels with the joystick!

Rendering the level: The details

The level renderer into two parts: a header loader and a level renderer. The header loader is responsible for loading the parameters of the .flf level file, while the level renderer “positions” the level renderer poiner to the correct data area and performs the rendering. Let’s start with the header:

procedure ReadLevelheader();
var
   m_rl_width, m_rl_height : byte;
   m_rl_sizex, m_rl_sizey : byte;
   m_rl_startx, m_rl_starty : byte;
   m_rl_dataSize, m_rl_pos : byte;
   m_rl_chunksize, m_rl_chunks : byte;

begin
	levelpointer:=@levelPosition;
	inczp(levelpointer, 13); // Fluff header

	// Read level header info

	m_rl_sizex := levelpointer[0];
	m_rl_sizey := levelpointer[1];

	m_rl_width := levelpointer[2];
	m_rl_height := levelpointer[3];

	m_rl_startx := levelpointer[4];
	m_rl_starty := levelpointer[5];

	m_rl_chunks := levelpointer[6];
	m_rl_chunksize := levelpointer[7];

	m_rl_dataSize := levelpointer[8];

end;

This procedure contains a bunch of variable declarations that reflect the same level parameters defined in part 5 (level width, data chunk size etc). These are here just read once. The header has a total size of 13 + 32 bytes.

Next, let’s go ahead with the full-blown level renderer:

procedure RenderLevel(m_li, m_lj, m_screen: byte);
var
   m_rl_i, m_rl_j, m_rl_val, m_rl_idx: byte;

begin
	levelpointer:=@levelPosition;
	// Fluff files have 13+32 byte headers
	inczp(levelpointer, 32+13);

	m_rl_pos := m_rl_sizex*m_lj + m_li;
	m_rl_val := m_rl_width*m_rl_height;

	// Skip levels
	if m_rl_pos<>0 then
	for m_rl_j:=0 to m_rl_pos do begin
			inczp(levelpointer,m_rl_val);
			inczp(levelpointer,m_rl_dataSize);
	end;


	moveto(m_rl_startx, m_rl_starty, $D8);
	colormemory:=screenmemory;
	moveto(m_rl_startx, m_rl_starty, m_screen);
	
	// Fill screen with chars
	//if current_game_mode=@gameModeMap then
	//call(SIDFILE_1_PLAY);

	for m_rl_j:=0 to m_rl_height do begin
		m_rl_idx:=0;
	
		for m_rl_i:=0 to m_rl_width do begin
			m_rl_val := levelpointer[m_rl_i];
			screenmemory[m_rl_idx+40]:=m_rl_val+40;
			screenmemory[m_rl_idx]:=m_rl_val;
			screenmemory[m_rl_idx+41]:=m_rl_val+41;
			screenmemory[m_rl_idx+1]:=m_rl_val+1;

			colormemory[m_rl_idx] := colors[m_rl_val];
			colormemory[m_rl_idx+1] := colors[m_rl_val+1];
			colormemory[m_rl_idx+41] := colors[m_rl_val+41];
			colormemory[m_rl_idx+40] := colors[m_rl_val+40];

			m_rl_idx:=m_rl_idx+2;
		 end;
		inczp(screenmemory, 80);
		inczp(colormemory, 80);
		inczp(levelpointer,m_rl_width);
	end;

	poke(MULTICOLOR_CHAR_COL, 0, levelpointer[0]);
	poke(MULTICOLOR_CHAR_COL, 2, levelpointer[1]); 
	poke(MULTICOLOR_CHAR_COL, 1, levelpointer[2]);
	inczp(levelpointer,3);

end;

To do : write more details about the level renderer.

Next part.