-
20. Putting your code in the right place: a brief introduction to prg banking
21. Giving your main character a sword
22. Adding more features to the pause menu
23. Adding a second map
24. Saving the Game
25. Adding objects that attract or repel the player
26. Adding an enemy that mimics player behavior
27. Adding a new sprite size
-
40. Understanding and tweaking the build tools
41. Dealing with limited ROM space
42. Resizing your rom
43. ROM Data Map
44. Expanding available graphics using CHR banking
45. Getting finer control over graphics with chr ram
46. Writing Functions in Assembly
47. Automatic builds with GH Actions
48. Switching to unrom 512 for advanced features
Attracting or Repelling the player
Sometimes regular movement isn’t quite enough - you want something to pull the player in or push them away. It’s a mechanic you see in many games, and it isn’t that hard to implement either. We will do this by adding a new magnet sprite that pulls the player in - we’ll make it strong enough for the player to notice, but also give the player the ability to break free of the pull.
Follow along with this example in the git branch named section3_attract_repel.
If you want to try it yourself, download the ROM
Getting started
First thing’s first, let’s add a new sprite! We’ve done this a few times before, but we will need to add a new line
to source/c/sprites/sprite_definitions.c
for our sprite. We also use a new type: SPRITE_TYPE_MAGNET
, which we define
in the header file. There is a magnet sprite at 0xe0 that we will use for this.
Here it is, at the bottom of the list:
const unsigned char spriteDefinitions[] = {
// Other sprite definitions...
SPRITE_TYPE_MAGNET, 0xe0, SPRITE_SIZE_16PX_16PX | SPRITE_PALETTE_2, SPRITE_ANIMATION_NONE, SPRITE_MOVEMENT_NONE, 0x00, 0x00, 0x00
Of note, we are actually not animating this sprite in the definition, despite having two animation frames for it. The reason for this is that we want the sprite to not animate until it is active. The plan is to have this sprite “turn on” every few seconds.
If you rebuild the rom, then open the map with Tiled
, you should see the new sprite available to be placed on the
map. Make sure to do this before moving on. If you run the game afterwards you will see your sprite.
Next, we need to actually make it do something!
Toggling the sprite animation and activity
We need to do a little special logic with magnets to make them work the way we want. Right now we don’t have a great
place that we loop through all sprite types, but we do loop through all sprites for a few things in
source/c/sprites/map_sprites.c
, so we’ll piggyback off of that to update the player’s acceleration. We also put
this into a section that only runs every other frame to save ourselves some time. This code lives in the
update_map_sprites()` method. Here’s the updated code:
// We only want to do movement once every other frame, to save some cpu time.
// So, split this to update even sprites on even frames, odd sprites on odd frames
if ((i & 0x01) == everyOtherCycle) {
switch (currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_MOVEMENT_TYPE]) {
// Sprite movement code is here; left out for readability
case SPRITE_MOVEMENT_NONE:
default:
break;
}
// Special case for magnet to do movement and more.
if (currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_TYPE] == SPRITE_TYPE_MAGNET) {
// We want the magnet to turn on/off every 128 frames
if (frameCount & 0x80) {
// It's on for half the time; set the sprite to be animated
currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_ANIMATION_TYPE] = SPRITE_ANIMATION_SWAP_FAST;
} else {
// It's off; set the sprite not to be animated anymore.
currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_ANIMATION_TYPE] = SPRITE_ANIMATION_NONE;
}
}
}
If you add this code in and rebuild the game, you will see the magnet switch on and off on a regular interval. If you
felt so inclined, you could also do this based off the player’s distnace from the sprite, or have it move around by
using a different SPRITE_MOVEMENT_TYPE
in source/c/sprites/sprite_definitions.c
.
Next, let’s actually move the play around a bit.
Affecting player movement
This is the main bit of this feature; we need to affect player movement whenever any magnets are active. For this we
need a couple new variables. we’ll add two new variables in source/c/sprites/player.c
: playerMagnetXAccel
and
playerMagnetYAccel
- we will update these with each magnet on the map, then apply it to the player’s speed in
player.c
. Here are the variable definitions:
int playerMagnetXAccel;
int playerMagnetYAccel;
We’ll need to also expose these variables in source/c/sprites/player.h
, and also define some constants around how much
magnets affect us. We can do this by adding the following:
extern int playerMagnetXAccel;
extern int playerMagnetYAccel;
#define MAGNET_ACCELERATION_CHANGE 4
Next, we need to get some data into them. We’ll try to calculate how much to move the player. You will be able to tweak
the speed as needed. We’ll update the code we used before to update these new variables, then apply them to the
player’s movement. First, let’s go back to source/c/sprites/map_sprites.c
and update our logic:
// Special case for magnet to do movement and more.
if (currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_TYPE] == SPRITE_TYPE_MAGNET) {
// We want the magnet to turn on/off every 128 frames
if (frameCount & 0x80) {
// It's on for half the time; set the sprite to be animated
currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_ANIMATION_TYPE] = SPRITE_ANIMATION_SWAP_FAST;
// Set the acceleration based on the player's location compared to the magnet.
// But only if we are not very close to the sprite already.
if (ABS(sprX - playerXPosition) > 4) {
if (playerXPosition < sprX) {
playerMagnetXAccel += MAGNET_ACCELERATION_CHANGE;
} else {
playerMagnetXAccel -= MAGNET_ACCELERATION_CHANGE;
}
}
if (ABS(sprY - playerYPosition) > 4) {
if (playerYPosition < sprY) {
playerMagnetYAccel += MAGNET_ACCELERATION_CHANGE;
} else {
playerMagnetYAccel -= MAGNET_ACCELERATION_CHANGE;
}
}
} else {
// It's off; set the sprite not to be animated anymore.
currentMapSpriteData[currentMapSpriteIndex + MAP_SPRITE_DATA_POS_ANIMATION_TYPE] = SPRITE_ANIMATION_NONE;
// Also reset the acceleration for the magnet, so we don't touch the player anymore.
playerMagnetXAccel = 0;
playerMagnetYAccel = 0;
}
}
The new piece we added does a couple things: First, it makes sure that the distance between the two sprites is
greater than 4 units, and if so it continues to act on the sprite. Past this point, it checks which side we are on,
and adds or subtracts the acceleration constant MAGNET_ACCELERATION_CHANGE
as a result.
We also should be sure to reset this value before we loop over sprites each frame; otherwise we will keep increasing
acceleration indefinitely. Add this to the very beginning of update_map_sprites()
in the same file:
void update_map_sprites(void) {
lastPlayerSpriteCollisionId = NO_SPRITE_HIT;
// Reset the player's magnet acceleration, so we are only affected by magnets active this frame.
playerMagnetXAccel = 0;
playerMagnetYAccel = 0;
// Rest of the method...
With that done, all we need to do is make the player move based off of this. The logic to do this isn’t too complex.
Pop open source/c/sprites/player.c
and let’s add a little more logic to the prepare_player_movement()
method. This is
actually only two lines, which we add after we have finished updating movement based on the player’s input. It can be added right after we tick down invulnerability.
// While we're at it, tick down the invulnerability timer if needed
if (playerInvulnerabilityTime) {
playerInvulnerabilityTime--;
}
playerXVelocity += playerMagnetXAccel;
playerYVelocity += playerMagnetYAccel;
nextPlayerXPosition = playerXPosition + playerXVelocity;
nextPlayerYPosition = playerYPosition + playerYVelocity;
After adding this in, if you go to the tile you added the magnet to, you should see it start to pull you when the magnet is on. Success!
Taking it further
This is a very simple implementation, but you can definitely take it a bit further if you want. There are a number of ways you could improve this.
More magnets!
There’s no reason for you not to include multiple magnets on a screen, with no code changes needed. Since we set the acceleration to 0 each frame, then add/subtract from the player’s speed, multiple magnets should work fine.
Tweak the speed
The acceleration applied via the magnet is handled by the MAGNET_ACCELERATION_CHANGE
variable added earlier on - the
value of this dictates how hard the magnet pulls the player. Higher values mean faster pull.
Make the magnet repel the player instead
The magnet looks at the player’s location, and pulls the player closer based on the difference between their two
positions. If we simply reverse the if statements in source/c/sprites/map_sprites.c
to check if
playerXPosition > sprX
instead of playerXPosition < sprX
(and the same with sprY
) we can reverse the direction.
Only pull the player when close to the magnet
Another option is to only pull the player when they are within a certain distance of the magnet. We do a test now on the absolute value of the distance between player and sprites, and don’t move the player if they are within 4 distance units of the magnet’s position. We can use this same logic to test how far away the player is. Here is a rough example of the code for that:
if (ABS(sprX - playerXPosition) < 180 && ABS(sprY - playerYPosition) < 180) {
// Existing code
if (ABS(sprX - playerXPosition) > 4 ) {
if (playerXPosition < sprX) {
playerMagnetXAccel += MAGNET_ACCELERATION_CHANGE;
} else {
// The rest of this logic stays the same...
Move the magnet itself
The magnet is a regular sprite; there is no reason not to make it move like some of our other sprites. You can change
SPRITE_MOVEMENT_NONE
to SPRITE_MOVEMENT_LEFT_RIGHT
or even SPRITE_MOVEMENT_RANDOM_WANDER
in the sprite’s
definition. (source/c/sprites/sprite_definitions.c
)
Make the magnet into an enemy
With a few changes, you could also make the magnet into an enemy. You would need to find out where the
SPRITE_TYPE_MONSTER
type triggers damage, then alter this check to also look at SPRITE_TYPE_MAGNET
.
Add a sound effect when the magnet is on
This is covered in the Changing Sound Effects Chapter, but adding a little sound when the magnet kicks off should give the player an extra notification that something is happening.