-
Notifications
You must be signed in to change notification settings - Fork 825
Improve the outdoor sprite system
In the tutorial to add a new map, we covered the concept of outdoor sprite sets. Outdoor maps—those with a TOWN
or ROUTE
environment—can only use sprites from their map group's set of usable sprites.
The outdoor sprite sets are defined in data/maps/outdoor_sprites.asm. For example, here's the one for Olivine City's map group:
OlivineGroupSprites:
db SPRITE_SUICUNE
db SPRITE_SILVER_TROPHY
db SPRITE_FAMICOM
db SPRITE_POKEDEX
db SPRITE_WILL
db SPRITE_KAREN
db SPRITE_NURSE
db SPRITE_OLD_LINK_RECEPTIONIST
db SPRITE_STANDING_YOUNGSTER
db SPRITE_BIG_ONIX
db SPRITE_SUDOWOODO
db SPRITE_BIG_SNORLAX
db SPRITE_OLIVINE_RIVAL
db SPRITE_POKEFAN_M
db SPRITE_LASS
db SPRITE_BUENA
db SPRITE_SWIMMER_GIRL
db SPRITE_SAILOR
db SPRITE_POKEFAN_F
db SPRITE_SUPER_NERD
db SPRITE_TAUROS
db SPRITE_FRUIT_TREE
db SPRITE_ROCK
We can see how those get loaded with BGB's VRAM viewer:
VRAM is divided into six areas, each 128 tiles large. The top two areas are for sprites' standing frames. The middle-right area is for the walking frames of the sprites in the top-right. But the middle-left area is for font tiles, so the top-left sprites can't walk. (If they do, they'll appear as text tiles.)
As they're currently implemented, these sprite sets are hard to edit. Every set has 23 sprites, and it's hard to tell which ones are needed for which map. It's also not clear which sprites get placed in VRAM bank 1 (the one on the right in the VRAM viewer). There's only enough room for nine sprites to have walking frames, but those nine are not in any particular order, nor does the order of the list correspond to the order in VRAM.
This tutorial will improve the outdoor sprite sets by making them variable-length lists ending with 0, and with the first nine sprites being the ones to get walking frames. The existing sprite sets for pokecrystal's maps will be optimized to work with this new format.
- Make outdoor sprite sets variable-length, ending with 0
- Don't automatically sort outdoor sprite sets
- Update the outdoor sprite sets
- Remove the now-redundant non-walking sprite versions
- Remove the now-redundant variable sprites
Edit constants/map_data_constants.asm:
-MAX_OUTDOOR_SPRITES EQU 23 ; see engine/overworld/overworld.asm
We won't need MAX_OUTDOOR_SPRITES
any more.
Next, edit engine/overworld/overworld.asm:
AddOutdoorSprites:
ld a, [wMapGroup]
dec a
ld c, a
ld b, 0
ld hl, OutdoorSprites
add hl, bc
add hl, bc
ld a, [hli]
ld h, [hl]
ld l, a
- ld c, MAX_OUTDOOR_SPRITES
.loop
- push bc
ld a, [hli]
+ and a
+ ret z
call AddSpriteGFX
- pop bc
- dec c
- jr nz, .loop
- ret
+ jr .loop
Instead of counting c
down from MAX_OUTDOOR_SPRITES
to 0, now AddOutdoorSprites
will continue until it finds a 0 list entry. (So don't forget to add them! We'll do so later.)
Now that outdoor sprite lists can be arbitrarily long, it's more important to enforce the limit of how much RAM is even available. However, there happens to be a bug with the LoadSpriteGFX
routine that ignores the SPRITE_GFX_LIST_CAPACITY
limit. So be sure to fix that.
As we saw in earlier, the current sprite lists are not in any particular order. It turns out that the LoadAndSortSprites
routine sorts the lists before loading their graphics, in order of how many tiles each one has, from most to least. Most NPC sprites have 12 tiles (four each for the front, back, and side views), so they get sorted first, and then come the still sprites like SPRITE_POKE_BALL
, SPRITE_FRUIT_TREE
, etc.
(Each outdoor sprite list gets padded to 23 entries with a bunch of still sprites like SPRITE_SILVER_TROPHY
or SPRITE_OLD_LINK_RECEPTIONIST
. They're not used in any outdoor map, but they have to be there so the walking sprites get sorted first.)
Anyway, edit engine/overworld/overworld.asm again:
LoadAndSortSprites:
call LoadSpriteGFX
- call SortUsedSprites
call ArrangeUsedSprites
ret
...
-SortUsedSprites:
-; Bubble-sort sprites by type.
-
- ...
-
-.quit
- ret
Now sprites will be loaded into VRAM in whatever order the list specifies. So if we want a certain nine sprites to have walking frames available, we'll have to put them first.
If you're replacing all of Crystal's maps with your own, you won't need these exact sets; but they're a good reference anyway for how the new sets work.
Edit data/maps/outdoor_sprites.asm:
-PalletGroupSprites:
- ...
-
-...
-
-CableClubGroupSprites:
- ...
+; Route1 and ViridianCity are connected
+; Route2 and PewterCity are connected
+; PalletTown and Route21 are connected
+PalletGroupSprites:
+; Route1, PalletTown
+ViridianGroupSprites:
+; Route2, Route22, ViridianCity
+PewterGroupSprites:
+; Route3, PewterCity
+CinnabarGroupSprites:
+; Route19, Route20, Route21, CinnabarIsland
+ db SPRITE_TEACHER
+ db SPRITE_FISHER
+ db SPRITE_YOUNGSTER
+ db SPRITE_BLUE
+ db SPRITE_GRAMPS
+ db SPRITE_BUG_CATCHER
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_SWIMMER_GIRL
+ db SPRITE_SWIMMER_GUY
+ ; limit of 9 walking sprites
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db 0 ; end
+
+; CeruleanCity and Route5 are connected
+CeruleanGroupSprites:
+; Route4, Route9, Route10North, Route24, Route25, CeruleanCity
+SaffronGroupSprites:
+; Route5, SaffronCity
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_SUPER_NERD
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_FISHER
+ db SPRITE_YOUNGSTER
+ db SPRITE_LASS
+ db SPRITE_POKEFAN_M
+ db SPRITE_ROCKET
+ db SPRITE_MISTY
+ ; limit of 9 walking sprites
+ db SPRITE_POKE_BALL
+ db SPRITE_SLOWPOKE
+ db 0 ; end
+
+VermilionGroupSprites:
+; Route6, Route11, VermilionCity
+ db SPRITE_POKEFAN_M
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_TEACHER
+ db SPRITE_SUPER_NERD
+ ; 5 of max 9 walking sprites
+ db SPRITE_BIG_SNORLAX
+ db SPRITE_MACHOP
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db 0 ; end
+
+CeladonGroupSprites:
+; Route7, Route16, Route17, CeladonCity
+ db SPRITE_FISHER
+ db SPRITE_TEACHER
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_LASS
+ db SPRITE_BIKER
+ ; 6 of max 9 walking sprites
+ db SPRITE_POLIWAG
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db 0 ; end
+
+; Route12 and Route13 are connected
+LavenderGroupSprites:
+; Route8, Route12, Route10South, LavenderTown
+FuchsiaGroupSprites:
+; Route13, Route14, Route15, Route18, FuchsiaCity
+ db SPRITE_POKEFAN_M
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_FISHER
+ db SPRITE_TEACHER
+ db SPRITE_SUPER_NERD
+ db SPRITE_BIKER
+ ; 7 of max 9 walking sprites
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db 0 ; end
+
+IndigoGroupSprites:
+; Route23
+ ; 0 of max 9 walking sprites
+ db 0 ; end
+
+; Route29 and CherrygroveCity are connected
+NewBarkGroupSprites:
+; Route26, Route27, Route29, NewBarkTown
+CherrygroveGroupSprites:
+; Route30, Route31, CherrygroveCity
+ db SPRITE_SILVER
+ db SPRITE_TEACHER
+ db SPRITE_FISHER
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_YOUNGSTER
+ db SPRITE_MONSTER
+ db SPRITE_GRAMPS
+ db SPRITE_BUG_CATCHER
+ db SPRITE_COOLTRAINER_F
+ ; limit of 9 walking sprites
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db 0 ; end
+
+; Route37 and EcruteakCity are connected
+VioletGroupSprites:
+; Route32, Route35, Route36, Route37, VioletCity
+EcruteakGroupSprites:
+; EcruteakCity
+ db SPRITE_FISHER
+ db SPRITE_LASS
+ db SPRITE_OFFICER
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_BUG_CATCHER
+ db SPRITE_SUPER_NERD
+ ; 8 of max 9 walking sprites
+ db SPRITE_WEIRD_TREE ; variable sprite: becomes SPRITE_SUDOWOODO and SPRITE_TWIN
+ db SPRITE_POKE_BALL
+ db SPRITE_FRUIT_TREE
+ db SPRITE_SUICUNE
+ db 0 ; end
+
+AzaleaGroupSprites:
+; Route33, AzaleaTown
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_POKEFAN_M
+ db SPRITE_TEACHER
+ db SPRITE_AZALEA_ROCKET ; variable sprite: becomes SPRITE_ROCKET and SPRITE_SILVER
+ db SPRITE_LASS
+ ; 6 of max 9 walking sprites
+ db SPRITE_FRUIT_TREE
+ db SPRITE_SLOWPOKE
+ db SPRITE_KURT_OUTSIDE ; non-walking version of SPRITE_KURT
+ db 0 ; end
+
+GoldenrodGroupSprites:
+; Route34, GoldenrodCity
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_OFFICER
+ db SPRITE_POKEFAN_M
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_ROCKET
+ db SPRITE_LASS
+ ; 7 of max 9 walking sprites
+ db SPRITE_DAY_CARE_MON_1
+ db SPRITE_DAY_CARE_MON_2
+ db SPRITE_POKE_BALL
+ db 0 ; end
+
+; OlivineCity and Route40 are connected
+OlivineGroupSprites:
+; Route38, Route39, OlivineCity
+CianwoodGroupSprites:
+; Route40, Route41, CianwoodCity, BattleTowerOutside
+ db SPRITE_OLIVINE_RIVAL ; variable sprite: becomes SPRITE_SILVER and SPRITE_SWIMMER_GUY
+ db SPRITE_POKEFAN_M
+ db SPRITE_LASS
+ db SPRITE_BUENA
+ db SPRITE_SWIMMER_GIRL
+ db SPRITE_SAILOR
+ db SPRITE_POKEFAN_F
+ db SPRITE_SUPER_NERD
+ ; 8 of max 9 walking sprites
+ db SPRITE_TAUROS
+ db SPRITE_FRUIT_TREE
+ db SPRITE_ROCK
+ db SPRITE_STANDING_YOUNGSTER ; non-walking version of SPRITE_YOUNGSTER
+ db SPRITE_SUICUNE
+ db 0 ; end
+
+MahoganyGroupSprites:
+; Route42, Route44, MahoganyTown
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_LASS
+ db SPRITE_SUPER_NERD
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_POKEFAN_M
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_FISHER
+ ; 8 of max 9 walking sprites
+ db SPRITE_FRUIT_TREE
+ db SPRITE_POKE_BALL
+ db SPRITE_SUICUNE
+ db 0 ; end
+
+LakeOfRageGroupSprites:
+; Route43, LakeOfRage
+ db SPRITE_LANCE
+ db SPRITE_GRAMPS
+ db SPRITE_SUPER_NERD
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_FISHER
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_LASS
+ db SPRITE_YOUNGSTER
+ ; 8 of max 9 walking sprites
+ db SPRITE_GYARADOS
+ db SPRITE_FRUIT_TREE
+ db SPRITE_POKE_BALL
+ db 0 ; end
+
+BlackthornGroupSprites:
+; Route45, Route46, BlackthornCity
+ db SPRITE_GRAMPS
+ db SPRITE_YOUNGSTER
+ db SPRITE_LASS
+ db SPRITE_SUPER_NERD
+ db SPRITE_COOLTRAINER_M
+ db SPRITE_POKEFAN_M
+ db SPRITE_BLACK_BELT
+ db SPRITE_COOLTRAINER_F
+ ; 8 of max 9 walking sprites
+ db SPRITE_FRUIT_TREE
+ db SPRITE_POKE_BALL
+ db 0 ; end
+
+SilverGroupSprites:
+; Route28, SilverCaveOutside
+ ; 0 of max 9 walking sprites
+ db 0 ; end
+
+DungeonsGroupSprites:
+; NationalPark, NationalParkBugContest, RuinsOfAlphOutside
+ db SPRITE_LASS
+ db SPRITE_POKEFAN_F
+ db SPRITE_TEACHER
+ db SPRITE_YOUNGSTER
+ db SPRITE_POKEFAN_M
+ db SPRITE_ROCKER
+ db SPRITE_FISHER
+ db SPRITE_SCIENTIST
+ ; 8 of max 9 walking sprites
+ db SPRITE_GAMEBOY_KID
+ db SPRITE_GROWLITHE
+ db SPRITE_POKE_BALL
+ db 0 ; end
+
+FastShipGroupSprites:
+; OlivinePort, VermilionPort, MountMoonSquare, TinTowerRoof
+ db SPRITE_SAILOR
+ db SPRITE_FISHING_GURU
+ db SPRITE_SUPER_NERD
+ db SPRITE_COOLTRAINER_F
+ db SPRITE_YOUNGSTER
+ ; 5 of max 9 walking sprites
+ db SPRITE_HO_OH
+ db SPRITE_FAIRY
+ db SPRITE_ROCK
+ db 0 ; end
+
+CableClubGroupSprites:
+; (no outdoor maps)
+ ; 0 of max 9 walking sprites
+ db 0 ; end
Now we're done! These new sets are easier to define and debug than before. For example, here's the one for Olivine City's map group:
; OlivineCity and Route40 are connected
OlivineGroupSprites:
; Route38, Route39, OlivineCity
CianwoodGroupSprites:
; Route40, Route41, CianwoodCity, BattleTowerOutside
db SPRITE_OLIVINE_RIVAL; variable sprite: becomes SPRITE_SILVER and SPRITE_SWIMMER_GUY
db SPRITE_POKEFAN_M
db SPRITE_LASS
db SPRITE_BUENA
db SPRITE_SWIMMER_GIRL
db SPRITE_SAILOR
db SPRITE_POKEFAN_F
db SPRITE_SUPER_NERD
; 8 of max 9 walking sprites
db SPRITE_TAUROS
db SPRITE_FRUIT_TREE
db SPRITE_ROCK
db SPRITE_STANDING_YOUNGSTER ; non-walking version of SPRITE_YOUNGSTER
db SPRITE_SUICUNE
db 0 ; end
And here's how they get loaded into VRAM:
The comments make it clear which maps the set applies to; and the order matches their order in VRAM, from top to bottom, right and then left.
Some things to note about the new system:
- If you can walk across a map connection from one map group to another, those groups now share an outdoor sprite set. Sprites and tilesets are only reloaded when you warp to a different map, not when you cross a connection, so this is necessary. Previously, connected sets like
OlivineGroupSprites
andCianwoodGroupSprites
used different lists which had to be kept in sync. - Removing the
LoadAndSortSprites
also affects indoor maps. If a map'sobject_event
s use many different sprites, make sure to put the walking ones first.
Two sprites are just copies of other sprites, but with the walking frames removed:
-
SPRITE_STANDING_YOUNGSTER
is a non-walking version ofSPRITE_YOUNGSTER
used byOlivineGroupSprites
andCianwoodGroupSprites
-
SPRITE_KURT_OUTSIDE
is a non-walking version ofSPRITE_KURT
used byAzaleaGroupSprites
This used to be necessary because if there were ten or more walking sprites, you couldn't control which nine would be loaded first and have their walking frames available; it was up to LoadAndSortSprites
.
Now, though, you can completely delete SPRITE_STANDING_YOUNGSTER
and SPRITE_KURT_OUTSIDE
, and use SPRITE_YOUNGSTER
and SPRITE_KURT
instead. Just be sure to put them after all the walking sprites in their respective outdoor sprite sets.
For example, if you replace SPRITE_STANDING_YOUNGSTER
with SPRITE_YOUNGSTER
in OlivineGroupSprites
, its standing frames will get loaded in VRAM bank 0, right where SPRITE_STANDING_YOUNGSTER
in the previous screenshot; and its walking frames won't interfere with the font graphics.
Three sprites are actually variable sprites:
-
SPRITE_WEIRD_TREE
is used byVioletGroupSprites
andEcruteakGroupSprites
. It starts out looking likeSPRITE_SUDOWOODO
, and becomesSPRITE_TWIN
after you battle Sudowoodo. This works because the only Twins in those map groups are encountered after Sudowoodo disappears. -
SPRITE_AZALEA_ROCKET
is used byAzaleaGroupSprites
. It starts out looking likeSPRITE_ROCKET
, and becomesSPRITE_SILVER
after you save the Slowpoke. This works because the rival encounter occurs after all the Rockets disappear. -
SPRITE_OLIVINE_RIVAL
is used byOlivineGroupSprites
andCianwoodGroupSprites
. It starts out looking likeSPRITE_SILVER
, and becomesSPRITE_SWIMMER_GUY
after you run into your rival. This works because the only male Swimmers in those map groups are encountered after your rival disappears.
At first glance, it makes sense why these variable sprites exist. They're a neat way to have ten or more walking sprites: if you know that only nine will be needed at a time, then a variable sprite can look like one now and another later.
...Except, all three of those are used in outdoor sprite sets with enough room for more walking sprites! VioletGroupSprites
and EcruteakGroupSprites
only use 8 of max 9; AzaleaGroupSprites
uses 6 of max 9; and OlivineGroupSprites
and CianwoodGroupSprites
use 8 of max 9.
It turns out that Crystal doesn't need these variable sprites, but Gold and Silver did. Crystal is exclusive to the GameBoy Color, so it has twice as much VRAM, and it's able to load walking frames for every sprite in bank 1 while still having room for standing-still sprites in bank 0. But Gold and Silver supported the Super GameBoy, which only had one VRAM bank; so still sprites like SPRITE_POKE_BALL
and SPRITE_SLOWPOKE
used up the same space budget as walking sprites.
Anyway, the point is that we don't need them any more. So you can completely delete those three variable sprites, and directly use the sprites that they turned into. Be sure to edit InitializeEventsScript
in engine/events/std_scripts.asm, which is where those variable sprites get initialized.