2
\$\begingroup\$

I am coding an infinite 2D block world chunk system for fun. I would like to improve performance, but I'm not that worried.

Game.py

import pygame

import world


class Game:
    def __init__(self, title, window_size, fps):
        self.title = title
        self.window_size = window_size
        self.FPS = fps

        pygame.init()
        self.screen = pygame.display.set_mode(self.window_size)
        self.window_rect = pygame.Rect((0, 0), self.window_size)
        self.clock = pygame.time.Clock()
        pygame.display.set_caption(self.title)

        self.running = True

        self.world = world.World(8)
        self.cam_x = 0
        self.cam_y = 0
        self.draw_area = (
            int(self.window_size[0] // world.CHUNK_TOTAL_SIZE) + 2,
            int(self.window_size[1] // world.CHUNK_TOTAL_SIZE) + 2,
        )
        print(self.draw_area)

    def run(self):
        while self.running:
            self.do_frame()

        self.quit_pygame()

    def do_frame(self):
        self.handle_events()
        self.game_logic()

        self.draw()
        pygame.display.update(self.window_rect)

        self.clock.tick(self.FPS)
        # print(self.clock.get_fps())

    def quit_pygame(self):
        pygame.quit()

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            if event.type == pygame.KEYDOWN:
                print(self.clock.get_fps())

        keys = pygame.key.get_pressed()

        if keys[pygame.K_LEFT]:
            self.cam_x += 128
        if keys[pygame.K_RIGHT]:
            self.cam_x -= 128
        if keys[pygame.K_UP]:
            self.cam_y += 128
        if keys[pygame.K_DOWN]:
            self.cam_y -= 128

        self.world.load_pos = (
            int(-self.cam_x // world.CHUNK_TOTAL_SIZE),
            int(-self.cam_y // world.CHUNK_TOTAL_SIZE),
        )

    def draw(self):
        draw_offset_x = self.cam_x + self.window_rect.centerx
        draw_offset_y = self.cam_y + self.window_rect.centery

        start_chunk_x = int((-draw_offset_x) // world.CHUNK_TOTAL_SIZE)
        start_chunk_y = int((-draw_offset_y) // world.CHUNK_TOTAL_SIZE)

        chunk_poses = [
            (x, y)
            for x in range(start_chunk_x, start_chunk_x + self.draw_area[0])
            for y in range(start_chunk_y, start_chunk_y + self.draw_area[1])
        ]

        chunk_textures = self.world.get_chunk_textures(chunk_poses)

        self.screen.fill((255, 255, 255))
        for i, chunk_texture in enumerate(chunk_textures):
            chunk_pos = chunk_poses[i]
            self.screen.blit(
                chunk_texture,
                (
                    (chunk_pos[0] * world.CHUNK_TOTAL_SIZE) + draw_offset_x,
                    (chunk_pos[1] * world.CHUNK_TOTAL_SIZE) + draw_offset_y,
                ),
            )

    def game_logic(self):
        self.world.load_chunks()


new_game = Game("2D Block World", (1000, 600), 60)
new_game.run()

World.py

import random
import pygame

BLOCK_TEXTURE_NAMES = [
    "textures/void.png",
    "textures/air.png",
    "textures/grass_block.png",
    "textures/dirt.png",
    "textures/stone.png",
    "textures/sandstone.png",
    "textures/sand.png",
    "textures/bedrock.png",
    "textures/oak_log.png",
    "textures/oak_leaves.png",
    "textures/cobblestone.png",
    "textures/oak_leaves_over_log.png",
]
BACKGROUND_COLOR = (0, 200, 255)

BLOCK_SIZE = 8
CHUNK_SIZE = 32
CHUNK_TOTAL_SIZE = BLOCK_SIZE * CHUNK_SIZE
CHUNK_RANGE = range(CHUNK_SIZE)

EMPTY_CHUNK_DATA = [0] * (CHUNK_SIZE * CHUNK_SIZE)


def load_texture(texture, size):
    return pygame.transform.scale(pygame.image.load(texture).convert_alpha(), size)


def _2d_to_1d(pos):
    return (pos[0] * CHUNK_SIZE) + pos[1]


def block_to_chunk(block_pos):
    chunk_x, block_x = divmod(block_pos[0], CHUNK_SIZE)
    chunk_y, block_y = divmod(block_pos[1], CHUNK_SIZE)
    return (chunk_x, chunk_y), (block_x, block_y)


class Textures:
    def __init__(self):
        self.block_textures = [
            load_texture(texture_name, (BLOCK_SIZE, BLOCK_SIZE))
            for texture_name in BLOCK_TEXTURE_NAMES
        ]
        self.empty_chunk_surface = pygame.Surface(
            (CHUNK_TOTAL_SIZE, CHUNK_TOTAL_SIZE)
        ).convert_alpha()
        self.background_block_surface = pygame.Surface(
            (BLOCK_SIZE, BLOCK_SIZE)
        ).convert_alpha()

        self.empty_chunk_surface.fill(BACKGROUND_COLOR)
        self.background_block_surface.fill(BACKGROUND_COLOR)


class World:
    def __init__(self, load_distance):
        self.textures = Textures()
        self.chunks = {}
        self.chunk_textures = {}
        self.load_pos = (0, 0)
        self.load_distance = load_distance

    def load_chunks(self):
        chunk_range_x = range(
            self.load_pos[0] - self.load_distance,
            self.load_pos[0] + self.load_distance + 1,
        )
        chunk_range_y = range(
            self.load_pos[1] - self.load_distance,
            self.load_pos[1] + self.load_distance + 1,
        )
        chunks_in_range = [(x, y) for x in chunk_range_x for y in chunk_range_y]

        chunks_to_load = []
        chunks_to_unload = []

        for chunk_pos in chunks_in_range:
            if chunk_pos not in self.chunks.keys():
                chunks_to_load.append(chunk_pos)

        for chunk_pos in self.chunks.keys():
            if chunk_pos not in chunks_in_range:
                chunks_to_unload.append(chunk_pos)

        # for chunk_pos in chunks_to_load:
        #     self.load_chunk(chunk_pos)
        distances_from_load_pos = [
            abs(self.load_pos[0] - chunk_pos[0]) + abs(self.load_pos[1] - chunk_pos[1])
            for chunk_pos in chunks_to_load
        ]
        if distances_from_load_pos:
            best_distance = min(distances_from_load_pos)
            self.load_chunk(
                chunks_to_load[distances_from_load_pos.index(best_distance)]
            )

        for chunk_pos in chunks_to_unload:
            self.chunks.pop(chunk_pos)
            self.chunk_textures.pop(chunk_pos)

    def load_chunk(self, chunk_pos):
        self.chunks[chunk_pos] = self.generate_data(chunk_pos)
        self.chunk_textures[chunk_pos] = self.generate_chunk_texture(chunk_pos)

    def get_blocks(self, block_poses):
        blocks = []
        for block_pos in block_poses:
            chunk_pos, local_block_pos = block_to_chunk(block_pos)
            if chunk_pos in self.chunks.keys():
                blocks.append(self.chunks[chunk_pos][_2d_to_1d(local_block_pos)])
            else:
                blocks.append(0)

        return blocks

    def get_chunk_textures(self, chunk_poses):
        chunk_textures = []
        for chunk_pos in chunk_poses:
            if chunk_pos in self.chunk_textures.keys():
                chunk_textures.append(self.chunk_textures[chunk_pos])
            else:
                chunk_textures.append(self.textures.empty_chunk_surface)

        return chunk_textures

    def generate_data(self, pos):
        data = EMPTY_CHUNK_DATA[:]
        # chunk_block_x = self.pos[0] * CHUNK_SIZE
        chunk_block_y = pos[1] * CHUNK_SIZE
        for x in CHUNK_RANGE:
            for y in CHUNK_RANGE:
                # block_x = x+chunk_block_x
                block_y = y + chunk_block_y
                data_pos = _2d_to_1d((x, y))
                if block_y < 2:
                    data[data_pos] = 1
                elif block_y == 2:
                    data[data_pos] = 2
                else:
                    data[data_pos] = 3
        return data

    def generate_chunk_texture(self, pos):
        chunk_texture = self.textures.empty_chunk_surface.copy()
        chunk_data = self.chunks[pos]

        for x in CHUNK_RANGE:
            for y in CHUNK_RANGE:
                data = chunk_data[_2d_to_1d((x, y))]
                blit_pos = (x * BLOCK_SIZE, y * BLOCK_SIZE)
                chunk_texture.blit(self.textures.block_textures[data], blit_pos)

        return chunk_texture

I am using python 3.8

Images:

[ air bedrock cobblestone dirt grass_block oak_leaves oak_leaves_over_log oak_log sand sandstone stone void ]

\$\endgroup\$
1
  • 1
    \$\begingroup\$ nit: PEP 8 asks that you spell it self.fps (downcased). \$\endgroup\$ Commented Oct 31, 2024 at 2:45

1 Answer 1

2
\$\begingroup\$

Efficiency

These separate if statements:

        if event.type == pygame.QUIT:
            self.running = False
        if event.type == pygame.KEYDOWN:

should be combined into a single if/elif statement:

        if event.type == pygame.QUIT:
            self.running = False
        elif event.type == pygame.KEYDOWN:

The event.type variable can only have one value, so the checks are mutually exclusive. This makes the code more efficient since you don't have to perform the 2nd check if the first is true.

I think the same is true for the following 4 if conditions:

    if keys[pygame.K_LEFT]:
        self.cam_x += 128
    if keys[pygame.K_RIGHT]:
        self.cam_x -= 128
    if keys[pygame.K_UP]:
        self.cam_y += 128
    if keys[pygame.K_DOWN]:

If they are mutually exclusive, then there is no reason to check the last 3 if the first is true.

It would be nice to replace the repeated 128 with a meaningful variable name.

DRY

You can factor out all those repeated textures/ directory paths from:

BLOCK_TEXTURE_NAMES = [
    "textures/void.png",
    "textures/air.png",
    "textures/grass_block.png",

I wish I could play the game, but these files are needed to run the code.

Comments

You should remove all the commented-out code to remove clutter. For example:

    # print(self.clock.get_fps())

Documentation

Add a docstring to the top of each file to summarize the description of the code. Describe what a Game is and what a World is, for example.

\$\endgroup\$
0

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.