3
\$\begingroup\$

This code requires:

  • tile map exported as .csv file
  • tile set as .png
  • knowing tile size
  • knowing the rows and cols of the tile set .png

For example, this tileset has 2 rows and 6 cols

enter image description here

import pygame
from pygame.locals import *
import csv
import os

class TileMapReader():
    """get tilemap and tileset list need to import csv first"""
    def __init__(self,tilemap_path, tilesheet_path, tile_size, tileset_rows, tileset_cols):
        self.tilemap_path = tilemap_path
        self.tilesheet_path = tilesheet_path
        self.tile_size = tile_size
        self.tileset_rows = tileset_rows
        self.tileset_cols = tileset_cols
        
    def get_tilemap(self):
        """returns the tile map data (nested list)\n
        -1 represents empty spaces"""
        
        tilemap_data = []

        with open(self.tilemap_path, "r") as csvfile:
            tilemap_csv = csv.reader(csvfile)

            for rows in tilemap_csv:
                temp = []
                for element in rows:
                    temp.append(int(element))
                tilemap_data.append(temp)
        
        return tilemap_data
    
    def get_tileset(self):
        """returns a list of surfaces (tiles)\n
        for tileset in tilemap editor tile ID is starting from 0 to n\n
        in this list index is the same ID of each tile"""
        
        tilesheet = pygame.image.load(self.tilesheet_path)
        tilesets = []
        
        for h in range(self.tileset_rows):
            for w in range(self.tileset_cols):
                surface = pygame.Surface((self.tile_size,self.tile_size))
                surface.blit(tilesheet, (0,0), (w*self.tile_size, h*self.tile_size, self.tile_size, self.tile_size))
                tilesets.append(surface)
                
        return tilesets
                
class TileDraw():
    def __init__(self, tileset:list, tilemap:list, tilesize:int):
        super().__init__()
        self.tilesize = tilesize
        self.tileset = tileset
        self.tilemap = tilemap
        self.tile_types = [i for i in range(len(tileset))]
            
    def fill_groups(self, mapgroup:pygame.sprite.Group, groundgroup:pygame.sprite.Group = pygame.sprite.Group(), groundtypes:list[int]=[]):   
        for h,row in enumerate(self.tilemap):
            for w,tiletype in enumerate(row):           
                if tiletype in self.tile_types:
                    tile = pygame.sprite.Sprite()
                    tile.image = self.tileset[tiletype]
                    tile.rect = tile.image.get_rect()
                    tile.rect.topleft = (w*self.tilesize, h*self.tilesize)
                    mapgroup.add(tile)
                    if tiletype in groundtypes:
                        groundgroup.add(tile)
                

# Test
if __name__ == "__main__":
    pygame.init()

    display_surface = pygame.display.set_mode((800,608))
    pygame.display.set_caption("Tile Map")

    # tilemap csv path
    tmap_path = os.path.join("assets_map","TM.csv")
    # tileset png path
    tsheet_path = os.path.join("assets_map","TM-tileset.png")
  
        
    tilemapreader = TileMapReader(tilemap_path= tmap_path, 
                                  tilesheet_path= tsheet_path, 
                                  tile_size=16, 
                                  tileset_rows=2, 
                                  tileset_cols=6)

    
    tset = tilemapreader.get_tileset()
    tmap = tilemapreader.get_tilemap()
    group_map = pygame.sprite.Group()
    
    tiledraw = TileDraw(tileset = tset, tilemap= tmap, tilesize = 16)
    tiledraw.fill_groups(mapgroup= group_map)
    
    print(group_map)
    
    clock = pygame.time.Clock()
    fps = 60


    running = True
    while running:
        for event in pygame.event.get():
            if event.type == QUIT or (event.type == KEYDOWN and event.key == K_q):
                running = not running
                
        group_map.draw(display_surface)
        pygame.display.update()
        clock.tick(fps)
        
    pygame.quit()

What do you think of the code I've written? Is there any better method to implement a tilemap in Python?

New contributor
Hossein TwoK is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

I confess to not knowing much about games in general and tilemaps in particular, so I will offer comments of a more general nature.

Docstrings and Type Hints

You have provided docstrings and either type hints or comments describing your methods' argument types and return values. This is good to see. I would suggest your adding a docstring for the entire module describing what its purpose is.

Private Attributes

Your TileMapReader.__init__ method creates several attributes that are for internal use, i.e. that a client of this class should not be accessing. Rename those attributes to use a leading "_" to indicate that they are "private".

Avoid Re-calculating Return Values

If either TileMapReader.get_tilemap or TileMapReader.get_tileset were called multiple times, you would be re-executing the method's logic only to return what had been returned on a previous call. That is, these methods always return the same value. To prevent performing needless calculations you could store these return values as attributes _tilemap and _tileset which would be initialized to None in the __init__ method. Then these methods would first check if these attributes are None. If not, just return the previously cached value. Otherwise, execute the logic to compute the return values but store those values in _tilemap and _tileset respectively before returning.

Alternatively, you can decorate these methods with the functools.cache decorator.

Consistent Naming

This is a rather insignificant suggestion, but that won't stop me form offering it:

Your class name is TileMapReader composed of combining three names, "Tile", "Map" and "Reader". Yet you consider "tilemap" to be a single word. Otherwise, you would have, for example, attribute tile_map_data instead of tilemap_data. To be consistent, a better name for the class would be TilemapReader.

Use List Comprehensions Where Applicable

Method get_tilemap can be implemented more simply and efficiently as:

    def get_tilemap(self):
        """returns the tile map data (nested list)\n
        -1 represents empty spaces"""
        
        with open(self.tilemap_path, "r", newline='') as csvfile:
            tilemap_csv = csv.reader(csvfile)
            return [
                [int(element) for element in row]
                for row in tilemap_csv
            ]

Note the newline='' argument being passed to open as require by the csv.reader method.

Do Not Use a List Comprehension Where Inappropriate

In method TileDraw.__init__ you have:

        self.tile_types = [i for i in range(len(tileset))]

This should be instead:

        self.tile_types = list(range(len(tileset))

Provide Real Automated Tests

You have some logic within your if __name__ == "__main__": block that you have commented as a test. This seems to be more of a demo than a test.

You should include automated tests that match actual results against expected results for some test cases. To that end there are any number of APIs you could use, including Python's built-in doctest and unittest modules, mock and pytest

\$\endgroup\$
1
  • \$\begingroup\$ amazing tips ill use them to improve my code thank you \$\endgroup\$ Commented 9 hours ago
0
\$\begingroup\$

Unused code

This code is not used and should be deleted:

import os

UX

When I run the code, the GUI appears for a second, then disappears. It would be better to notify the user what the code should do. You could print a message to the shell, instructing the user what is required to run the code.

Tools

You could run code development tools to automatically find some style issues with your code.

ruff shows this message:

F403 `from pygame.locals import *` used; unable to detect undefined names
 |
 | import pygame
 | from pygame.locals import *
 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F403

It is better to not do wildcard imports this way.

Documentation

It is good that you used docstrings to add documentation to the code, as recommended by the PEP 8 style guide.

In this docstring:

"""get tilemap and tileset list need to import csv first"""

It would be better to describe more about the CSV input file. You should show the expected name and location of the file and show examples of its contents.

It is unexpected to see "\n" at the end of the docstring line:

"""returns the tile map data (nested list)\n

You should either remove "\n" or explain what it means.

Naming

You should use more underscores in variable names to make them easier to read. For example, tilesheet_path would be tile_sheet_path and tileset_cols would be tile_set_cols.

The variable name temp is too generic. You could name it "elements", but that still may be too generic. Describe what kind of an element it is.

\$\endgroup\$
2
  • \$\begingroup\$ i used os.path for getting tmap_path and tsheet_path and the \n inside docstrings is to make nextline while hovering to see the information through the name of method in vs code i have to either use \n or double Enter in docstring to have newline and now i understand that \n is not proper thank you \$\endgroup\$ Commented 9 hours ago
  • \$\begingroup\$ @HosseinTwoK: You're welcome. \$\endgroup\$ Commented 9 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.