Tutorial game: Let’s 楽しい (part 9)

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

In this final tutorial (for now), we’ll use the level designer to place a monster/another (entity) sprite into our game, and enabling collisions with this entity.

We now have to think about the data structure that was defined in the level designer, and decide on what the data chunks should contain.

When creating the level file, we defined that each screen should contain 8 chunks with 8 bytes of data each (you can resize these numbers by clicking the “Resize data” button). Let’s decide on the first byte for each chunk to be the sprite location ID, the next X coordinate, Y coordinate and sprite color. In order to remember this ordering, you can type in header names in the input field below the “resize data” button, separated by commas.

Let’s fill in some data (as shown in the image above): sprite = 1, x=35, y = 14 and color = 4. You can obtain the x/y positions in the lower part of the screen. Let’s save the file and start coding!

Data structure definitions

We need to discern between the original data from the level file, and have a set of data that represents the current values of the entity. The entity might move, change colors, lose health etc, so here are some new variable declarations:

	spriteLocations : array[1] of byte = (0, $C9);

	@define numEntities 6
	@define crashDistance 8

	entity_id : array[@numEntities] of byte;
	entity_pos_x : array[@numEntities] of byte;
	entity_pos_y : array[@numEntities] of byte;
	entity_color : array[@numEntities] of byte;

Some explanation: Let’s assume the maximum number of entities on the screen to be 6, so we define @numEntities will be this number. We then set up x/y and color arrays for all active entities on the screen. Also, note that we are creating a table that will transform the internal ID used in the level editor (like 1=monster 1, 6 = health upgrades etc) to actual sprite positions. You really don’t want to hard-code sprite positions into the level editor, because you’ll probably have to relocate sprites later during the development process. And as always, this will turn into a pain in the butt. The preprocessor @crashdistance will be dealt with later, but is basically the closest distance an entity can be to the player sprite before activation the crash routine.

Next, let’s create an entity loader. After rendering a new level, “levelpointer” points directly to where the data starts. Add a new procedure:

procedure LoadEntities();
begin
	// Make sure all ID's are set to zer0
	fill(entity_id,0, @numEntities);
	for i:=0 to @numEntities do begin
		if levelpointer[0]=0 then return();
		entity_id[i] := levelpointer[0];
		entity_pos_x[i] := levelpointer[1]*4 + 10;
		entity_pos_y[i] := levelpointer[2]*8 + 60;
		entity_color[i] := levelpointer[3];
		inczp(levelpointer, m_rl_chunksize);

	end;
end;

This method must be called every time you render a new level. First, you fill all current entity id’s to zero. Zero means that there are no more entities to intialize. We then go through the entire list (6) of possible entities in this level, and assign the x,y and color values. Note how we calculate the sprite position of the x and y positions from character positions. This is basically the inverse operation used in the procedure for calculating background collisions.

A note: In this tutorial, we operate with entity_pos_x in a range from 0-160, and choose to double it only when displaying the sprite. The reason for this is simple – we do not work on the “real” integer values (0-320) of the sprite because well.. uhm.. TRSE doesn’t yet support integer arrays. Instead, we let the entities x position range between 0-160 and then double the value only when calculating the actual sprite position.

Now we’ve populated the entity arrays – but haven’t yet initialized any sprites. This is done in the next procedure:

procedure InitEntities();
var
	ex: integer;
begin
	SPRITE_BITMASK:=%00000001; // Only enable player sprite
	for i:=0 to @numEntities do begin
		k:=i+1;
		if entity_id[i]=0 then return();
		j:=spriteLocations[entity_id[i]];
		setspriteloc(k,j,0);
		togglebit(SPRITE_BITMASK,k,1);
		SPRITE_COLOR[k]:=entity_color[i];
		ex:=entity_pos_x[i]*2;
		spritepos(ex,entity_pos_y[i], k);		
	end;
end;

In this procedure, we go through all the newly loaded entities and check whether the ID is zero. if it is, we end the procedure. If not, we set the sprite data location from the spriteLocations array. Also note that since the player sprite is always 0, we assume entity sprites to start from 1, hence k:=i+1. SetSpriteLoc points the VIC chip to the correct sprite, while togglebit turns on the sprite rendering of sprite k. Next, we assign sprite colors and finally set the sprite location (multiplying the byte variable in sprite_pos_x with 2).

When compiling and running the tutorial, you should be able to marvel at something like this:

(where sprite 1 = sprite position $C9 = the heart). Naturally, you can now add more entities through the level designer.

Collisions

In this final part of the tutorial series, we’ll define a method that checks whether your player sprite has collided with any of the entities on the screen. First, we need a software collision method that calculates whether the player sprite has collided with an entity:

procedure EntityCollider();
var 
	maxDistance:byte;
	nearestEntity:byte;
	px:byte;
begin
	maxDistance := 255;
	px:=player_x/2;
	nearestEntity := 255; 
	
	for i:=0 to @numEntities  do begin
		if (entity_id[i]=0) then return();

		k := abs(entity_pos_x[i] - px);
		l := abs(entity_pos_y[i] - player_y);
			
		k:=k/2 + l/2;
			
		if k<maxDistance then begin
			maxDistance:=k;
			nearestEntity:=i;
		end;
	end;
	
end;

This method probably needs a bit of a comment. First, it will set up some variables: maxDistance, which is the current largest distance from any entity (255). Then, that obnoxious player_x is divided by two in order to correspond to entity_pos_x locations. Finally, the value which will be calculated in this method – nearestEntity – is set to a random large value (larger than @numEntities at least).

We then proceed by looping through all the current entities (breaking if any ID is zero), doing the following:

  • We really should calculate a distance = sqrt( (player_x-entity_x)^2 + (player_y-entity_y)^2), but since we’re working on a C64 and don’t have a sqrt / sqr procedure we just calculate the absolute difference between the x and y coordinates as our distance method.
  • If the newly calculated (somewhat fake) distance is lower than the current calculated distance, we save it and mark the current entity as the closest

Finally, we just end the method. Now, the  nearestEntity should contain the index of the closest entity, while maxDistance contains the approximated (L1 norm) distance to this object.

After this, we define a new procedure that actually performs some action:

procedure CrashWithEntities();
begin
	EntityCollider();
	if (maxDistance<@crashDistance) then begin
		entity_id[nearestEntity]:=0;
		togglebit(SPRITE_BITMASK, nearestEntity+1,0);
		PlaySound(SID_CHANNEL1,
			13,  // Volume
			20,  // Hi byte frequency
			0*16+0,  // Attack voice 1
			3*16 + 12,   // Sustain = 16*15 + release=6
			1 +SID_SAW,  // Waveform
			SID_SAW);  // waveform*/
	end;

end;

Here’s where the @crashdistance preprocessor comes into use. We first call EntityCollider(), and then test if the current closest object is closer than @crashdistance=6. If so, we just remove the entity from the list (by setting its ID to zero), remove the sprite and play a simple sound.

Combining stuff

Finally, you need to combine these new procedures into the existing file.

  • Whenever you render a new level, you will also have to perform a call to LoadEntities and InitEntities

 

...
	if redraw=1 then begin
		levelpointer:=@levelPosition;
		RenderCharsetColorLevel(curLevel_x,curLevel_y,$04);
		LoadEntities();
		InitEntities();
	end;
...
and also in the main block:

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

Also, the raster interrupt also needs to call the CrashWithEntities procedure:

interrupt RasterRenderLevels();
begin
	StartIRQ(@useKernal);
	MovePlayerSprite();	
	TraverseLevels();
	CrashWithEntities();
	CloseIRQ();
end;

And that’s it! You should now have a tutorial where you have a moving, colliding sprite that can traverse levels at the same time as you can crash into other entities that are all defined in the level editor.

Future stuff

In the current version of TRSE (0.04), this unfortunately marks the end of the tutorial game. However, in the next version of TRSE there might be some additional tutorials. You really should think about stuff like

  • How to make the entity sprites move
  • How various entities can trigger various effects – health, death, upgrades etc
  • How to create an intro, and ending
  • How to render status text below/above the levels
  • Entities will always be reinitialized when entering a screen.

Hopefully, these items will be addresses in a future tutorial, if the author doesn’t expire from non-coding commitment issues.