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
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?
2 Answers 2
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
-
\$\begingroup\$ amazing tips ill use them to improve my code thank you \$\endgroup\$Hossein TwoK– Hossein TwoK2025年11月03日 13:33:32 +00:00Commented yesterday
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.
-
\$\begingroup\$ i used
os.pathfor gettingtmap_pathandtsheet_pathand 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\$Hossein TwoK– Hossein TwoK2025年11月03日 13:32:55 +00:00Commented yesterday -
\$\begingroup\$ @HosseinTwoK: You're welcome. Another way to say "thanks" is to upvote answers which were helpful to you. \$\endgroup\$toolic– toolic2025年11月04日 11:35:44 +00:00Commented 14 hours ago