1
\$\begingroup\$

I am developing a 2D Metroidvania which consists of a series of interconnected levels. When my player gets near a portal which can take him to a new map, I want to pre-load the next map so that there is a seamless transition when he actually enters the portal.

Here is some background about the structure of my code.

Each level is represented by a Tiled TMX file in which I have specified where various objects of different sprite classes spawn (for example, enemies, pickups, levers, etc.). When starting a new game, loading a saved game, or changing a level, my game loop runs through the appropriate TMX file and generates all the objects in that level.

For example, here is a simplified version of my new_game() method.

level = "level_1.tmx"

def new_game(self):
    Globals.scene = "game"

    # create groups
    self.all_sprites = pg.sprite.Group()
    self.walls = pg.sprite.Group()

    # load map and spawn objects
    self.map = TiledMap(path.join(self.map_folder, level))                    
    self.map_name = level
    self.map_img = self.map.make_map()                                        
    self.map_rect = self.map_img.get_rect()      

    # spawn objects
    for tile_object in self.map.objects:                     
        if tile_object.name == 'player':                                
           Player(self, tile_object.x, tile_object.y)
        if tile_object.name == 'wall':                                
            Obstacle(self, tile_object.x, tile_object.y, tile_object.width, tile_object.height)

I handle level changes like this: If the Player object intersects a Portal object, a change_map() method is called which loads a new map (the one associated with the intersected portal) and positions the player at the appropriate position on the new map. The change_map() method looks very similar to the new_game() method above, except that it empties all the sprite groups and hangs on to the player object for repositioning on the new map.

I want to restructure my code so that when the player gets near a portal object (say, within 640 pixels), the map associated with the portal begins to load. This way, when the player enters the portal, the map will load instantly.

I was thinking that I could check to see if the player is near a portal in my game's update() method. If he is, I could call a preload_map() method. But I am unsure how this function should look and how to transition to the new map when the player enters the portal. In order to avoid a stutter when the new map loads, do I need to learn about multiple threads? Maybe I should simply load all the new map images and sprites into their own variables (suffix them with "temp" or something) and then, when my player enters the portal, I can effectuate transition with lines like self.map_img = self.map_img_temp and self.all_sprites = self.all_sprites_temp.

So my question is: Given the structure of my code, how can I use pre-loading to achieve seamless map transitions?


Here is a similar question where the poster does not specify enough information about the structure of their code.

\$\endgroup\$
1
\$\begingroup\$

What I would do (let's assume you have a game instance called GameStatus) is something similar to this::

  1. Have a GameStatus.cached_map that can be either None or contain a TileMap object (created by the TileMap() call)

  2. Have a function, let's say 'load_map', that loads the map and assign it to the .cached_map property

  3. When in your update cycle you detect the player is in range of the portal, you'll call load_map (this will set your cached_map from None to a valid map object)

  4. In your Portal, you check if there's an available with if cached_map:, if it's available you assign it to GameStatus.map and finish setting it up, if not you call your usual map loading code.

This will work as a prototype: you'll test how splitting the code works before starting to fight with threading: verify you have all your variables available and handy or, if not, refactor the code maybe by creating your own Map management class, containing all the information and functions you need to manage it effectively. I do expect that doing this will block the game action (if map loading was instantaneous already, you wouldn't bother with preloading :-) ) so you'll need a background loading mechanism. Now that you know that loading work and you put all your pieces together, it'll be easier to add threading.

To do that, I'd use the threading module (unless your game engine/framework provides something similar) to start the map loading function in your update cycle (point 3 above) by launching a thread running load_map(), then in your Portal switch code, you'll check if there's a cached_map available: if not, you can check if the thread is alive and if it is, wait for it to finish.

You can play around a bit in an interactive shell with the following code (that I used to check that I still remembered things correctly :D )::

import threading
import time

def load_map(map_level, map_path):
    print("Here I'd load map {} from {}".format(map_level, map_path))
    # GameStatus.cached_map = TileMap(map_path, map_level)
    time.sleep(10)  
    print("map loaded!")
    return None

t1 = threading.Thread(target=load_map, args=("Lost Forest of Darkness", "/path/to/map.tmx"))
t1.run()
Here I'd load map Lost Forest of Darkness from /path/to/map.tmx
t1.is_alive()
True
<10 seconds pass>
map loaded!
t1.is_alive()
False

In your update cycle you will do something like this::

self.cached_map = None   # this might be better moved to load_map, if it has access to GameStatus)
t1 = threading.Thread(target=load_map, args=("Lost Forest of Darkness", "/path/to/map.tmx"))
t1.start()

statement, to create the thread then run it in background immediately.

When finally the player enters the portal, the portal loading function needs to check if the map is available (GameStatus.cached_map is not None), if not it can wait on the thread calling t1.join() (join() will wait for the thread to end before returning, t1.is_alive() tells you if it's running, etc. You can check https://docs.python.org/3/library/threading.html for more). You can then copy the cached_map to the 'real' map and do the rest to activate it (or encapsulate all this in a dedicated map object, as said above some refactoring might be useful to keep things readable and simple (i.e it might be a good idea to save the thread(s) as property of your GameStatus class, and/or implement map_load as a method of it).

P.S.

Which Python Game Engine are you using?

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the advice. This is a very nice explanation. I am using Pygame, though I am flirting with the idea of porting everything over to Pyglet. \$\endgroup\$ – GoldenGremlin Jan 19 '18 at 13:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.