MapRenderer - Kermalis/PokemonGameEngine GitHub Wiki

Overview

This page explains both the concepts and the implementation of how you end up with the maps on your screen! This information is only accurate since ORDER 66 (https://github.com/Kermalis/PokemonGameEngine/pull/66), since the old software renderer just looped the tiles at each screen position. The MapEditor also does not use these implementations, since it uses the software rendering method of looping tiles and placing them at the image position.

Maps vs MapLayouts

The easiest way to explain the difference is with an example. Each Pokémon Center in the game looks identical on the inside, but has different NPCs. The actual visual of the Map itself is the MapLayout. MapLayouts are the unique visual of what you see and identify with a place, so they can be reused throughout many Maps. So, each Pokémon Center location would be its own Map (which defines NPCs, events, encounters, etc), but all of those Maps would use the same MapLayout you design one time in the MapEditor.

Concept - Visible Maps & Current Map

The camera is always following an Obj, whether it's the PlayerObj or not. An invisible Obj called CameraObj can be created with the script command CreateCameraObj. An example of how to use it is in the test script @Test_GroudonBattle. The attached Obj is stored in OverworldGUI.Instance.CamAttachedTo and is never null. The PlayerObj and the camera's Obj can be in separate blocks, even while crossing maps, but warping to another Map is only possible if PlayerObj is attached. Other Objs cannot warp.

Simply put, the "current map" is whatever Map the camera's Obj is on. This affects:

  • Which Maps are considered visible
  • Whether the DayTint is active
  • What Map's music to play and when

However, there are exceptions that use the PlayerObj's Map instead because it makes more sense:

  • Met locations for caught & hatched Pokémon
  • Determining your party's Burmy & Giratina forms
  • Whether the player can fly/teleport/dig etc
  • Battle weather (will change in the future when Weather is a globally changeable thing)

Obj movement and interaction (such as encountering Pokémon and talking to signs) happens on the individual Objs' Map/MapLayout and is not relevant to this topic. An Obj attempting to move into an unloaded Map is considered undefined behavior.

Visible Maps are any Map that is... visible on the screen. These are dynamically determined every frame; more on that later.

How things are loaded

The Maps themselves only store map details, encounters, connections, events, and their layout. First, the current Map is used as a base to determine what to load, since we assume it is always visible due to the camera being there.

All visible Maps (initially just the current Map) load their connected Maps. If those Maps are visible, then they do the same, and so on. This means all of the connected Maps are loaded in preparation for becoming visible themselves and causes a chain reaction. Once a Map becomes visible, in addition to its connections, its MapLayout and Events are loaded.

Once a Map becomes the current Map, or is being warped to, the map details and encounters are loaded. If it is no longer the current Map, they are unloaded.

When a MapLayout is loaded, it loads all of its used Blocksets, which in turn load all of its used Tilesets. Once they're unloaded, they unload the things they are using if they were the only ones using them.

The Blockset class itself contains a "3D texture", which is just a stack of 2D textures where each layer is a loaded block's texture. Actually, it contains many of these 3D textures, but each one just represents each elevation layer, since each of those has to be separated, but they all work the same way. When Blockset blocks are loaded, they create their textures initially, and mark whether they're animated. Whenever tile animations happen, only the affected animated blocks' textures are updated. Once the MapRenderer decides what blocks it needs to render and where, it just takes those textures and renders them at the correct spot.

The MapRenderer

Every frame, the MapRenderer.cs has a job to do:

  • Figure out which blocks are currently visible
  • Mark Maps as no longer visible
  • Mark Maps as newly visible
  • Render every visible MapLayout
  • Render border blocks where the gaps are
  • Render every visible VisualObj
  • Combine all of these rendered elevation layers into the final product

The MapRenderer itself contains frame buffers for each layer: one for the blocks and one for the Objs.

Each visible block is marked by a bit that indicates if it is a border block or not, and the border blocks are taken from the current Map. This means you have to design all of your border blocks between Maps to be somewhat the same or at least make it coherent so you don't see the border blocks change completely when crossing Maps.

Each block on the screen -- including border blocks -- is rendered as a simple instanced rectangle that tells it where to be on the screen and what texture to use.

Each visible VisualObj is rendered as a simple rectangle, but they are not instanced. The shadows are rendered all together first, then the VisualObjs are rendered top to bottom.

Finally, each elevation layer is pasted to the screen, with the block frame buffer first, then the obj frame buffer. This is what allows VisualObjs to appear below the above elevations but above their current elevation's blocks.

The debug MapRenderer just renders text and rectangles to a separate frame buffer and then adds that to the screen after.

Limitations

  • You cannot use more than GLTextureUtils.MAX_ACTIVE_TEXTURES Tilesets per Blockset, but that's more than you'll ever use at once anyway
  • An infinite hallway of the same map connecting to itself does not work