2
\$\begingroup\$

Follow up from this question. I have added a slower loading system that allows the main game to run faster but I still get under 30 fps with caves even if I allow the player to outrun the loading. I could lower the worlds loading distance but then terrain has to keep being reloaded for no reason + if you stay still for long enough the terrain can be loaded to a large distance. I have profiled the code and the bottleneck is (obviously) world.load_nearest_chunk , and listcomps, chunks_to_unload = [chunk_pos for chunk_pos in self.chunks if chunk_pos not in chunks_in_range] and chunks_in_range = [(x, y) for x in chunk_range_x for y in chunk_range_y]. I think something like not generating chunks_in_range over and over again would help.

game.py:

import pygame
import world

MOVEMENT_SPEED = 64


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_numpy.World(64, 1)
        self.cam_x = 0
        self.cam_y = 0
        self.draw_area = (
            int(self.window_size[0] // world_numpy.CHUNK_TOTAL_SIZE) + 2,
            int(self.window_size[1] // world_numpy.CHUNK_TOTAL_SIZE) + 2,
        )

        self.player = self.world.create_entity(
            0,
            pygame.Vector2(0, -512),
        )

        self.cam_on_player = True

    def run(self):
        print("pre-gen")
        for i in range(4):
            self.world.load_nearest_chunk(fast=True)
        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)

    def quit_pygame(self):
        pygame.quit()

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.KEYDOWN:
                print(self.clock.get_fps(), len(self.world.chunks))
                if event.key == pygame.K_ESCAPE:
                    self.running = False
                if (event.key == pygame.K_w) and self.player.collide_y:
                    self.player.force += pygame.Vector2(0, -10)
                if event.key == pygame.K_c:
                    self.cam_on_player = not self.cam_on_player

        keys = pygame.key.get_pressed()

        if not self.cam_on_player:
            if keys[pygame.K_LEFT]:
                self.cam_x += MOVEMENT_SPEED
            elif keys[pygame.K_RIGHT]:
                self.cam_x -= MOVEMENT_SPEED
            elif keys[pygame.K_UP]:
                self.cam_y += MOVEMENT_SPEED
            elif keys[pygame.K_DOWN]:
                self.cam_y -= MOVEMENT_SPEED

        if keys[pygame.K_a]:
            self.player.force += pygame.Vector2(-2, 0)
        elif keys[pygame.K_d]:
            self.player.force += pygame.Vector2(2, 0)

        self.world.load_pos = (
            int(-self.cam_x // world_numpy.CHUNK_TOTAL_SIZE),
            int(-self.cam_y // world_numpy.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
        # draw_offset = pygame.Vector2(draw_offset_x, draw_offset_y)

        start_chunk_x = int((-draw_offset_x) // world_numpy.CHUNK_TOTAL_SIZE)
        start_chunk_y = int((-draw_offset_y) // world_numpy.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_numpy.CHUNK_TOTAL_SIZE) + draw_offset_x,
                    (chunk_pos[1] * world_numpy.CHUNK_TOTAL_SIZE) + draw_offset_y,
                ),
            )
        for entity in self.world.entitys:
            self.screen.blit(
                entity.image,
                (
                    int(entity.pos.x) + draw_offset_x,
                    int(entity.pos.y) + draw_offset_y,
                ),
            )


    def game_logic(self):
        self.world.load_nearest_chunk()
        for entity in self.world.entitys:
            entity.update()
        if self.cam_on_player:
            self.cam_x = -int((self.player.pos.x + (self.player.size[0] / 2)))
            self.cam_y = -int((self.player.pos.y + (self.player.size[1] / 2)))



new_game = Game("2D Block world_numpy", (1280, 640), 60)
new_game.run()

world.py:

import math
import perlin_noise
import pygame
import numpy

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",
]
BLOCK_COLLISION_DATA = [0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 1, 2]
ENTITY_TEXTURE_NAMES = ["textures/player.png"]
ENTITY_SIZES = [(32, 64)]
ENTITY_HITBOX_DATA = [[(pygame.Vector2(8, 0), pygame.Vector2(16, 64))]]
# ENTITY_ARMS_TEXTURE_NAMES

GRAVITY = pygame.Vector2(0, 1)
RESISTANCE = pygame.Vector2(0.8, 0.99)

BACKGROUND_COLOR = (0, 200, 255)

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

COLUMNS_PER_FRAME = 2

EMPTY_CHUNK_DATA = numpy.zeros((CHUNK_SIZE, CHUNK_SIZE), dtype=numpy.int32)

BEDROCK = 1024
SOIL_AMOUNT = 16
FREQUENCY = 250
AMPLITUDE = 200
CAVE_FREQUENCY = 300
CAVE_SIZE = 0.25


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


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.entity_textures = [
            load_texture(texture_name, ENTITY_SIZES[i])
            for i, texture_name in enumerate(ENTITY_TEXTURE_NAMES)
        ]

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


class Entity:
    def __init__(
        self,
        world,
        entity_number,
        init_pos=pygame.Vector2(),
        init_vel=pygame.Vector2(),
        init_force=pygame.Vector2(),
    ):
        self.world = world
        self.size = pygame.Vector2(ENTITY_SIZES[entity_number])
        self.image = self.world.textures.entity_textures[entity_number]
        self.hitbox_data = ENTITY_HITBOX_DATA[entity_number]

        self.pos = init_pos
        self.vel = init_vel
        self.force = init_force

        self.collide_x = False
        self.collide_y = False
        self.collide_xy = False
        self.stuck = False

    def get_blocks_collide(self):
        blocks = []
        for start_pos, size in self.hitbox_data:
            start_pos_block = (start_pos + self.test_pos) / BLOCK_SIZE
            end_pos_block = (size + start_pos + self.test_pos) / BLOCK_SIZE

            block_range_x = range(
                math.floor(start_pos_block.x), math.floor(end_pos_block.x) + 1
            )
            block_range_y = range(
                math.floor(start_pos_block.y), math.floor(end_pos_block.y) + 1
            )

            blocks += [
                (block_x, block_y)
                for block_x in block_range_x
                for block_y in block_range_y
            ]
        return self.world.get_blocks(blocks)

    def collide(self):
        self.blocks_collide = self.get_blocks_collide()
        self.collide_data = [
            BLOCK_COLLISION_DATA[block] for block in self.blocks_collide
        ]

    def test(self):
        self.test_pos = self.pos.copy()
        self.collide()

    def test_xy(self):
        self.test_pos = self.pos.copy()
        self.test_pos += self.vel
        self.collide()

    def test_x(self):
        self.test_pos = self.pos.copy()
        self.test_pos.x += self.vel.x
        self.collide()

    def test_y(self):
        self.test_pos = self.pos.copy()
        self.test_pos.y += self.vel.y
        self.collide()

    def update(self):
        self.vel += self.force + GRAVITY
        self.vel = self.vel.elementwise() * RESISTANCE
        self.force = pygame.Vector2()

        self.collide_x = False
        self.collide_y = False
        self.collide_xy = False
        self.stuck = False

        self.test()
        if 1 in self.collide_data:
            self.vel = pygame.Vector2()
            self.stuck = True
            return

        self.test_xy()
        if 1 in self.collide_data:
            self.collide_xy = True
            self.test_x()
            if 1 in self.collide_data:
                self.vel.x = 0
                self.collide_x = True
            self.test_y()
            if 1 in self.collide_data:
                self.vel.y = 0
                self.collide_y = True

        if not (self.collide_x or self.collide_y) and self.collide_xy:
            self.vel.y = 0
            self.collide_y = True

        self.pos += self.vel


class Chunk_loader:
    def __init__(self, world, chunk_pos):
        self.world = world
        self.pos = chunk_pos
        self.data = EMPTY_CHUNK_DATA.copy()
        self.texture = self.world.textures.empty_chunk_surface.copy()

        self.chunk_block_x = self.pos[0] * CHUNK_SIZE
        self.chunk_block_y = self.pos[1] * CHUNK_SIZE
        self.chunk_block_range_x = [x + self.chunk_block_x for x in CHUNK_RANGE]
        self.chunk_block_range_y = [y + self.chunk_block_y for y in CHUNK_RANGE]

        self.generate_step = 0

    def generate_next(self):
        for x in range(self.generate_step, self.generate_step + COLUMNS_PER_FRAME):
            if self.generate_step >= CHUNK_SIZE:
                self.generate_chunk_texture()
                return False
            self.generate_data(x)
            self.generate_step += 1
        return True

    def generate_data(self, x):
        block_x = self.chunk_block_range_x[x]
        surface = int(
            self.world.surface_noise.noise(self.chunk_block_range_x[x] / FREQUENCY)
            * AMPLITUDE
        )
        for y in CHUNK_RANGE:
            block_y = self.chunk_block_range_y[y]
            cave_noise = (
                self.world.cave_noise.noise(
                    (block_x / CAVE_FREQUENCY, block_y / CAVE_FREQUENCY)
                )
                * 10
            ) + 1
            cave = (cave_noise > (0.5 - CAVE_SIZE)) and (cave_noise < (0.5 + CAVE_SIZE))
            if not block_y > BEDROCK:
                if not cave:
                    if block_y == surface:
                        block_type = 2
                    elif block_y > surface and block_y <= surface + SOIL_AMOUNT:
                        block_type = 3
                    elif block_y > surface + SOIL_AMOUNT and block_y < BEDROCK:
                        block_type = 4
                    elif block_y == BEDROCK:
                        block_type = 7
                    else:
                        block_type = 1
                else:
                    block_type = 1
            else:
                block_type = 0
            self.data[x, y] = block_type

    def generate_all_data(self):
        surface = 0
        for x in CHUNK_RANGE:
            block_x = self.chunk_block_range_x[x]
            if not self.world.flat:
                surface = int(
                    self.world.surface_noise.noise(block_x / FREQUENCY) * AMPLITUDE
                )
            for y in CHUNK_RANGE:
                block_y = self.chunk_block_range_y[y]
                cave_noise = (
                    self.world.cave_noise.noise(
                        (block_x / CAVE_FREQUENCY, block_y / CAVE_FREQUENCY)
                    )
                    * 10
                ) + 1
                cave = (cave_noise > (0.5 - CAVE_SIZE)) and (
                    cave_noise < (0.5 + CAVE_SIZE)
                )
                if not block_y > BEDROCK:
                    if not cave:
                        if block_y == surface:
                            block_type = 2
                        elif block_y > surface and block_y <= surface + SOIL_AMOUNT:
                            block_type = 3
                        elif block_y > surface + SOIL_AMOUNT and block_y < BEDROCK:
                            block_type = 4
                        elif block_y == BEDROCK:
                            block_type = 7
                        else:
                            block_type = 1
                    else:
                        block_type = 1
                else:
                    block_type = 0
                self.data[x, y] = block_type

        self.generate_chunk_texture()

    def generate_chunk_texture(self):
        for x in CHUNK_RANGE:
            for y in CHUNK_RANGE:
                data = self.data[x, y]
                self.texture.blit(
                    self.world.textures.block_textures[data],
                    (x * BLOCK_SIZE, y * BLOCK_SIZE),
                )


class World:
    def __init__(self, load_distance, seed=0):
        self.flat = True
        if seed:
            self.flat = False

        self.surface_noise = perlin_noise.PerlinNoise(octaves=1.3, seed=seed)
        self.cave_noise = perlin_noise.PerlinNoise(octaves=1.3, seed=seed + 1)

        self.textures = Textures()
        self.chunks = {}
        self.chunk_textures = {}
        self.load_pos = (0, 0)
        self.load_distance = load_distance

        self.entitys = []
        self.chunk_to_load = None

    def create_entity(
        self,
        entity_number,
        init_pos=pygame.Vector2(),
        init_vel=pygame.Vector2(),
        init_force=pygame.Vector2(),
    ):
        new_entity = Entity(
            self,
            entity_number,
            init_pos,
            init_vel,
            init_force,
        )
        self.entitys.append(new_entity)
        return new_entity

    def load_nearest_chunk(self, fast=False):
        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 = []

        for chunk_pos in chunks_in_range:
            if not self.chunk_to_load == None:
                if self.chunk_to_load.pos == chunk_pos:
                    continue
            if chunk_pos not in self.chunks:
                chunks_to_load.append(chunk_pos)

        chunks_to_unload = [chunk_pos for chunk_pos in self.chunks if chunk_pos not in chunks_in_range]

        distances_from_load_pos = [
            math.dist(self.load_pos, chunk_pos) for chunk_pos in chunks_to_load
        ]

        if distances_from_load_pos:
            best_distance = min(distances_from_load_pos)
            self.set_chunk_loader(
                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)


        if self.chunk_to_load:
            if fast:
                self.load_chunk_fast()
            else:
                self.load_chunk()

    def set_chunk_loader(self, chunk_pos):
        if self.chunk_to_load == None:
            self.chunk_to_load = Chunk_loader(self, chunk_pos)

    def load_chunk(self):
        if not self.chunk_to_load.generate_next():
            self.finish_chunk()

    def load_chunk_fast(self):
        self.chunk_to_load.generate_all_data()
        self.finish_chunk()

    def finish_chunk(self):
        self.chunks[self.chunk_to_load.pos] = self.chunk_to_load.data
        self.chunk_textures[self.chunk_to_load.pos] = self.chunk_to_load.texture
        self.chunk_to_load = None

    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:
                blocks.append(self.chunks[chunk_pos][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:
                chunk_textures.append(self.chunk_textures[chunk_pos])
            else:
                chunk_textures.append(self.textures.empty_chunk_surface)

        return chunk_textures

Profile output:

        1    0.000    0.000   12.035   12.035 game.py:1(<module>)
      169    0.002    0.000    0.002    0.000 game.py:100(<listcomp>)
      169    0.200    0.001    8.133    0.048 game.py:128(game_logic)
        1    0.006    0.006   10.088   10.088 game.py:36(run)
      169    0.007    0.000    9.104    0.054 game.py:45(do_frame)
        1    0.000    0.000    0.075    0.075 game.py:54(quit_pygame)
      169    0.006    0.000    0.152    0.001 game.py:57(handle_events)
        1    0.000    0.000    0.000    0.000 game.py:7(Game)
        1    0.000    0.000    0.290    0.290 game.py:8(__init__)
      169    0.020    0.000    0.585    0.003 game.py:92(draw)
        1    0.000    0.000    0.024    0.024 world.py:1(<module>)
      384    0.006    0.000    0.049    0.000 world.py:102(get_blocks_collide)
      384    0.002    0.000    0.002    0.000 world.py:115(<listcomp>)
      384    0.003    0.000    0.053    0.000 world.py:122(collide)
      384    0.002    0.000    0.002    0.000 world.py:124(<listcomp>)
      169    0.001    0.000    0.029    0.000 world.py:128(test)
      169    0.001    0.000    0.021    0.000 world.py:132(test_xy)
       23    0.000    0.000    0.003    0.000 world.py:137(test_x)
       23    0.000    0.000    0.003    0.000 world.py:142(test_y)
      169    0.006    0.000    0.061    0.000 world.py:147(update)
        1    0.000    0.000    0.000    0.000 world.py:182(Chunk_loader)
       14    0.000    0.000    0.006    0.000 world.py:183(__init__)
       14    0.000    0.000    0.000    0.000 world.py:191(<listcomp>)
       14    0.000    0.000    0.000    0.000 world.py:192(<listcomp>)
      169    0.003    0.000    1.876    0.011 world.py:196(generate_next)
      320    0.059    0.000    1.842    0.006 world.py:205(generate_data)
        4    0.023    0.006    0.770    0.193 world.py:238(generate_all_data)
       13    0.020    0.002    0.046    0.004 world.py:277(generate_chunk_texture)
        1    0.000    0.000    0.000    0.000 world.py:287(World)
        1    0.000    0.000    0.038    0.038 world.py:288(__init__)
        1    0.000    0.000    0.000    0.000 world.py:305(create_entity)
      173    2.308    0.013    8.776    0.051 world.py:322(load_nearest_chunk)
      173    0.492    0.003    0.492    0.003 world.py:331(<listcomp>)
      173    0.696    0.004    0.696    0.004 world.py:342(<listcomp>)
      173    1.155    0.007    2.000    0.012 world.py:344(<listcomp>)
      173    0.001    0.000    0.007    0.000 world.py:363(set_chunk_loader)
      169    0.001    0.000    1.877    0.011 world.py:367(load_chunk)
        4    0.000    0.000    0.770    0.193 world.py:371(load_chunk_fast)
       13    0.000    0.000    0.000    0.000 world.py:375(finish_chunk)
      384    0.019    0.000    0.038    0.000 world.py:380(get_blocks)
      169    0.004    0.000    0.005    0.000 world.py:390(get_chunk_textures)
       13    0.000    0.000    0.037    0.003 world.py:48(load_texture)
    10368    0.012    0.000    0.017    0.000 world.py:52(block_to_chunk)
        1    0.000    0.000    0.000    0.000 world.py:58(Textures)
        1    0.000    0.000    0.038    0.038 world.py:59(__init__)
        1    0.000    0.000    0.034    0.034 world.py:60(<listcomp>)
        1    0.000    0.000    0.002    0.002 world.py:70(<listcomp>)
        1    0.000    0.000    0.000    0.000 world.py:79(Entity)
        1    0.000    0.000    0.000    0.000 world.py:80(__init__)
     1659    0.003    0.000    0.003    0.000 {built-in method __new__ of type object at 0x00007FFC4B94B810}
       43    0.001    0.000    0.001    0.000 {built-in method _abc._abc_init}
    14470    0.013    0.000    0.013    0.000 {built-in method _abc._abc_instancecheck}
       12    0.000    0.000    0.000    0.000 {built-in method _abc._abc_register}
    30/15    0.000    0.000    0.000    0.000 {built-in method _abc._abc_subclasscheck}
        1    0.000    0.000    0.000    0.000 {built-in method _codecs.charmap_decode}
       30    0.000    0.000    0.000    0.000 {built-in method _codecs.utf_8_decode}
        2    0.003    0.002    0.003    0.002 {built-in method _ctypes.LoadLibrary}
        2    0.000    0.000    0.000    0.000 {built-in method _ctypes.POINTER}
       38    0.000    0.000    0.000    0.000 {built-in method _ctypes.sizeof}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_md5}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_sha1}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_sha224}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_sha256}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_sha384}
        1    0.000    0.000    0.000    0.000 {built-in method _hashlib.openssl_sha512}
      186    0.000    0.000    0.000    0.000 {built-in method _imp._fix_co_filename}
     1818    0.001    0.000    0.001    0.000 {built-in method _imp.acquire_lock}
       18    0.001    0.000    0.001    0.000 {built-in method _imp.create_builtin}
    52/50    0.225    0.004    0.238    0.005 {built-in method _imp.create_dynamic}
       18    0.000    0.000    0.000    0.000 {built-in method _imp.exec_builtin}
    52/47    0.005    0.000    0.058    0.001 {built-in method _imp.exec_dynamic}
       92    0.000    0.000    0.000    0.000 {built-in method _imp.is_builtin}
      247    0.000    0.000    0.000    0.000 {built-in method _imp.is_frozen}
     1818    0.001    0.000    0.001    0.000 {built-in method _imp.release_lock}
        1    0.000    0.000    0.000    0.000 {built-in method _locale._getdefaultlocale}
        1    0.000    0.000    0.000    0.000 {built-in method _socket.gethostname}
        4    0.000    0.000    0.000    0.000 {built-in method _sre.ascii_iscased}
        4    0.000    0.000    0.000    0.000 {built-in method _sre.ascii_tolower}
       93    0.000    0.000    0.000    0.000 {built-in method _sre.compile}
      609    0.000    0.000    0.000    0.000 {built-in method _sre.unicode_iscased}
      463    0.000    0.000    0.000    0.000 {built-in method _sre.unicode_tolower}
      596    0.000    0.000    0.000    0.000 {built-in method _stat.S_ISDIR}
        1    0.000    0.000    0.000    0.000 {built-in method _stat.S_ISREG}
       23    0.000    0.000    0.000    0.000 {built-in method _struct.calcsize}
        1    0.000    0.000    0.000    0.000 {built-in method _thread._set_sentinel}
      543    0.003    0.000    0.003    0.000 {built-in method _thread.allocate_lock}
     1387    0.001    0.000    0.001    0.000 {built-in method _thread.get_ident}
        1    0.000    0.000    0.000    0.000 {built-in method _thread.get_native_id}
        5    0.000    0.000    0.000    0.000 {built-in method _warnings._filters_mutated}
        2    0.000    0.000    0.000    0.000 {built-in method _win32sysloader.GetModuleFilename}
        1    0.000    0.000    0.000    0.000 {built-in method _win32sysloader.LoadModule}
        6    0.000    0.000    0.000    0.000 {built-in method _winapi.CloseHandle}
        1    0.000    0.000    0.000    0.000 {built-in method _winapi.CreatePipe}
        1    0.051    0.051    0.051    0.051 {built-in method _winapi.CreateProcess}
        3    0.000    0.000    0.000    0.000 {built-in method _winapi.DuplicateHandle}
        6    0.000    0.000    0.000    0.000 {built-in method _winapi.GetCurrentProcess}
        1    0.000    0.000    0.000    0.000 {built-in method _winapi.GetExitCodeProcess}
        1    0.000    0.000    0.000    0.000 {built-in method _winapi.GetStdHandle}
        1    0.001    0.001    0.001    0.001 {built-in method _winapi.WaitForSingleObject}
  519/515    0.017    0.000    0.089    0.000 {built-in method builtins.__build_class__}
   381/18    0.001    0.000    0.979    0.054 {built-in method builtins.__import__}
   115692    0.027    0.000    0.027    0.000 {built-in method builtins.abs}
      219    0.000    0.000    0.000    0.000 {built-in method builtins.all}
      126    0.000    0.000    0.000    0.000 {built-in method builtins.any}
       11    0.000    0.000    0.000    0.000 {built-in method builtins.callable}
      558    0.000    0.000    0.000    0.000 {built-in method builtins.chr}
        3    0.000    0.000    0.000    0.000 {built-in method builtins.dir}
    20796    0.005    0.000    0.005    0.000 {built-in method builtins.divmod}
    207/1    0.005    0.000   12.035   12.035 {built-in method builtins.exec}
     6032    0.006    0.000    0.006    0.000 {built-in method builtins.getattr}
      411    0.000    0.000    0.000    0.000 {built-in method builtins.globals}
     2895    0.002    0.000    0.002    0.000 {built-in method builtins.hasattr}
       36    0.000    0.000    0.000    0.000 {built-in method builtins.hash}
       18    0.000    0.000    0.000    0.000 {built-in method builtins.id}
    55925    0.035    0.000    0.058    0.000 {built-in method builtins.isinstance}
      645    0.000    0.000    0.000    0.000 {built-in method builtins.issubclass}
        8    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
342801/341843    0.069    0.000    0.070    0.000 {built-in method builtins.len}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.locals}
      994    0.002    0.000    0.003    0.000 {built-in method builtins.max}
     2838    0.116    0.000    0.116    0.000 {built-in method builtins.min}
       24    0.000    0.000    0.010    0.000 {built-in method builtins.next}
     1910    0.001    0.000    0.001    0.000 {built-in method builtins.ord}
       24    0.006    0.000    0.006    0.000 {built-in method builtins.print}
      107    0.000    0.000    0.000    0.000 {built-in method builtins.repr}
       12    0.000    0.000    0.000    0.000 {built-in method builtins.round}
     1928    0.001    0.000    0.001    0.000 {built-in method builtins.setattr}
       21    0.002    0.000    0.115    0.005 {built-in method builtins.sorted}
    73036    0.041    0.000    0.041    0.000 {built-in method builtins.sum}
        6    0.000    0.000    0.000    0.000 {built-in method builtins.vars}
      559    0.000    0.000    0.000    0.000 {built-in method from_bytes}
      247    0.001    0.000    0.001    0.000 {built-in method fromkeys}
      186    0.344    0.002    0.344    0.002 {built-in method io.open_code}
        9    0.010    0.001    0.010    0.001 {built-in method io.open}
        2    0.000    0.000    0.000    0.000 {built-in method maketrans}
      186    0.043    0.000    0.043    0.000 {built-in method marshal.loads}
  2877296    0.845    0.000    0.845    0.000 {built-in method math.dist}
        1    0.000    0.000    0.000    0.000 {built-in method math.exp}
    59776    0.046    0.000    0.046    0.000 {built-in method math.floor}
        2    0.000    0.000    0.000    0.000 {built-in method math.log}
   346752    0.187    0.000    0.187    0.000 {built-in method math.pow}
        1    0.000    0.000    0.000    0.000 {built-in method math.sqrt}
        1    0.000    0.000    0.000    0.000 {built-in method msvcrt.get_osfhandle}
        1    0.000    0.000    0.000    0.000 {built-in method msvcrt.open_osfhandle}
        2    0.000    0.000    0.000    0.000 {built-in method nt._add_dll_directory}
       26    0.007    0.000    0.007    0.000 {built-in method nt._getfinalpathname}
        4    0.000    0.000    0.000    0.000 {built-in method nt._getfullpathname}
      262    0.001    0.000    0.001    0.000 {built-in method nt._path_splitroot}
        1    0.000    0.000    0.000    0.000 {built-in method nt.close}
     8268    0.002    0.000    0.002    0.000 {built-in method nt.fspath}
       88    0.000    0.000    0.000    0.000 {built-in method nt.getcwd}
      245    0.059    0.000    0.059    0.000 {built-in method nt.listdir}
        1    0.000    0.000    0.000    0.000 {built-in method nt.open}
        5    0.000    0.000    0.000    0.000 {built-in method nt.putenv}
        1    0.000    0.000    0.000    0.000 {built-in method nt.readlink}
        1    0.000    0.000    0.000    0.000 {built-in method nt.scandir}
     1770    0.280    0.000    0.280    0.000 {built-in method nt.stat}
        1    0.000    0.000    0.000    0.000 {built-in method nt.urandom}
      118    0.001    0.000    0.001    0.000 {built-in method numpy.array}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath._reload_guard}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath._set_madvise_hugepage}
      338    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath.add_docstring}
        2    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core._multiarray_umath.set_typeDict}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.empty}
       18    0.000    0.000    0.000    0.000 {built-in method numpy.geterrobj}
        9    0.000    0.000    0.000    0.000 {built-in method numpy.seterrobj}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.zeros}
        3    0.000    0.000    0.000    0.000 {built-in method pygame.base.get_sdl_version}
        1    0.181    0.181    0.191    0.191 {built-in method pygame.base.init}
        1    0.075    0.075    0.075    0.075 {built-in method pygame.base.quit}
        1    0.000    0.000    0.000    0.000 {built-in method pygame.display.set_caption}
        1    0.059    0.059    0.062    0.062 {built-in method pygame.display.set_mode}
      169    0.223    0.001    0.223    0.001 {built-in method pygame.display.update}
      169    0.136    0.001    0.136    0.001 {built-in method pygame.event.get}
       13    0.036    0.003    0.036    0.003 {built-in method pygame.image.load}
      169    0.004    0.000    0.004    0.000 {built-in method pygame.key.get_pressed}
       13    0.000    0.000    0.000    0.000 {built-in method pygame.transform.scale}
       55    0.000    0.000    0.000    0.000 {built-in method sys._getframe}
        2    0.000    0.000    0.000    0.000 {built-in method sys.audit}
        1    0.000    0.000    0.000    0.000 {built-in method sys.exc_info}
        2    0.001    0.000    0.001    0.000 {built-in method sys.getwindowsversion}
      114    0.000    0.000    0.000    0.000 {built-in method sys.intern}
        1    0.000    0.000    0.000    0.000 {built-in method win32api.GetFullPathName}
        1    0.000    0.000    0.000    0.000 {built-in method win32api.GetTempPath}
        1    0.000    0.000    0.000    0.000 {built-in method win32api.RegOpenKey}
        1    0.000    0.000    0.000    0.000 {built-in method winreg.OpenKeyEx}
        1    0.000    0.000    0.000    0.000 {built-in method winreg.QueryValueEx}
       12    0.000    0.000    0.000    0.000 {function Random.getstate at 0x000002652F7A5AF0}
       13    0.000    0.000    0.000    0.000 {function Random.seed at 0x000002652F7A5C10}
       12    0.000    0.000    0.000    0.000 {function Random.setstate at 0x000002652F7A5DC0}
        1    0.000    0.000    0.000    0.000 {function SeedSequence.generate_state at 0x000002652F747EE0}
      114    0.000    0.000    0.000    0.000 {method '__contains__' of 'frozenset' objects}
        1    0.000    0.000    0.000    0.000 {method '__enter__' of '_thread.lock' objects}
        1    0.000    0.000    0.000    0.000 {method '__exit__' of '_thread.lock' objects}
       11    0.000    0.000    0.000    0.000 {method '__init_subclass__' of 'object' objects}
      471    0.001    0.000    0.001    0.000 {method '__reduce_ex__' of 'object' objects}
        2    0.000    0.000    0.000    0.000 {method 'acquire' of '_thread.lock' objects}
      842    0.000    0.000    0.000    0.000 {method 'add' of 'set' objects}
        4    0.000    0.000    0.000    0.000 {method 'append' of 'collections.deque' objects}
  2914448    0.474    0.000    0.474    0.000 {method 'append' of 'list' objects}
        6    0.000    0.000    0.000    0.000 {method 'bit_length' of 'int' objects}
    18213    0.470    0.000    0.470    0.000 {method 'blit' of 'pygame.surface.Surface' objects}
        7    0.000    0.000    0.000    0.000 {method 'cast' of 'memoryview' objects}
        6    0.000    0.000    0.000    0.000 {method 'clear' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'close' of '_io.TextIOWrapper' objects}
       15    0.000    0.000    0.000    0.000 {method 'convert_alpha' of 'pygame.surface.Surface' objects}
        3    0.000    0.000    0.000    0.000 {method 'copy' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'copy' of 'list' objects}
       86    0.001    0.000    0.001    0.000 {method 'copy' of 'numpy.ndarray' objects}
      384    0.000    0.000    0.000    0.000 {method 'copy' of 'pygame.math.Vector2' objects}
       14    0.005    0.000    0.005    0.000 {method 'copy' of 'pygame.surface.Surface' objects}
        1    0.000    0.000    0.000    0.000 {method 'count' of 'list' objects}
        8    0.000    0.000    0.000    0.000 {method 'decode' of 'bytes' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
       18    0.000    0.000    0.000    0.000 {method 'discard' of 'set' objects}
        1    0.000    0.000    0.000    0.000 {method 'dot' of 'numpy.ndarray' objects}
      169    0.000    0.000    0.000    0.000 {method 'elementwise' of 'pygame.math.Vector2' objects}
        4    0.000    0.000    0.000    0.000 {method 'encode' of 'str' objects}
       15    0.000    0.000    0.000    0.000 {method 'end' of 're.Match' objects}
    11564    0.005    0.000    0.005    0.000 {method 'endswith' of 'str' objects}
       31    0.000    0.000    0.000    0.000 {method 'expandtabs' of 'str' objects}
      308    0.000    0.000    0.001    0.000 {method 'extend' of 'list' objects}
      171    0.115    0.001    0.115    0.001 {method 'fill' of 'pygame.surface.Surface' objects}
     1141    0.001    0.000    0.001    0.000 {method 'find' of 'bytearray' objects}
        3    0.000    0.000    0.000    0.000 {method 'find' of 'str' objects}
        4    0.000    0.000    0.000    0.000 {method 'findall' of 're.Pattern' objects}
     1330    0.002    0.000    0.002    0.000 {method 'format' of 'str' objects}
    10050    0.003    0.000    0.003    0.000 {method 'get' of 'dict' objects}
      133    0.000    0.000    0.000    0.000 {method 'get' of 'mappingproxy' objects}
       21    0.000    0.000    0.000    0.000 {method 'get_fps' of 'pygame.time.Clock' objects}
     4315    0.002    0.000    0.002    0.000 {method 'group' of 're.Match' objects}
       12    0.000    0.000    0.000    0.000 {method 'groupdict' of 're.Match' objects}
        2    0.000    0.000    0.000    0.000 {method 'groups' of 're.Match' objects}
      173    0.049    0.000    0.049    0.000 {method 'index' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'index' of 'tuple' objects}
        1    0.000    0.000    0.000    0.000 {method 'indices' of 'slice' objects}
       16    0.000    0.000    0.000    0.000 {method 'insert' of 'list' objects}
      173    0.000    0.000    0.000    0.000 {method 'isidentifier' of 'str' objects}
       12    0.000    0.000    0.000    0.000 {method 'islower' of 'str' objects}
      845    0.000    0.000    0.000    0.000 {method 'isupper' of 'str' objects}
      170    0.000    0.000    0.000    0.000 {method 'items' of 'dict' objects}
       44    0.000    0.000    0.000    0.000 {method 'items' of 'mappingproxy' objects}
3629/3555    0.004    0.000    0.101    0.000 {method 'join' of 'str' objects}
        3    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
     3602    0.001    0.000    0.001    0.000 {method 'lower' of 'str' objects}
      926    0.000    0.000    0.000    0.000 {method 'lstrip' of 'str' objects}
      345    0.001    0.000    0.001    0.000 {method 'match' of 're.Pattern' objects}
       14    0.000    0.000    0.000    0.000 {method 'mro' of 'type' objects}
      486    0.000    0.000    0.000    0.000 {method 'partition' of 'str' objects}
        4    0.000    0.000    0.000    0.000 {method 'pop' of 'collections.deque' objects}
      279    0.000    0.000    0.000    0.000 {method 'pop' of 'dict' objects}
       48    0.000    0.000    0.000    0.000 {method 'pop' of 'list' objects}
       21    0.000    0.000    0.000    0.000 {method 'random' of '_random.Random' objects}
      192    0.028    0.000    0.028    0.000 {method 'read' of '_io.BufferedReader' objects}
        1    0.015    0.015    0.015    0.015 {method 'read' of '_io.TextIOWrapper' objects}
        2    0.000    0.000    0.000    0.000 {method 'readline' of '_io.BufferedReader' objects}
        1    0.003    0.003    0.003    0.003 {method 'readlines' of '_io._IOBase' objects}
        8    0.000    0.000    0.000    0.000 {method 'remove' of 'list' objects}
      316    0.001    0.000    0.001    0.000 {method 'replace' of 'code' objects}
     5152    0.002    0.000    0.002    0.000 {method 'replace' of 'str' objects}
       27    0.000    0.000    0.000    0.000 {method 'reverse' of 'list' objects}
     1956    0.002    0.000    0.002    0.000 {method 'rfind' of 'str' objects}
     1671    0.002    0.000    0.002    0.000 {method 'rpartition' of 'str' objects}
        6    0.000    0.000    0.000    0.000 {method 'rsplit' of 'str' objects}
     8098    0.003    0.000    0.003    0.000 {method 'rstrip' of 'str' objects}
     1025    0.005    0.000    0.005    0.000 {method 'search' of 're.Pattern' objects}
        1    0.000    0.000    0.000    0.000 {method 'seek' of '_io.BufferedReader' objects}
      469    0.000    0.000    0.000    0.000 {method 'setdefault' of 'dict' objects}
       22    0.000    0.000    0.000    0.000 {method 'setter' of 'property' objects}
        9    0.000    0.000    0.000    0.000 {method 'sort' of 'list' objects}
      626    0.004    0.000    0.004    0.000 {method 'split' of 're.Pattern' objects}
      932    0.001    0.000    0.001    0.000 {method 'split' of 'str' objects}
        6    0.000    0.000    0.000    0.000 {method 'splitlines' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {method 'startswith' of 'bytes' objects}
    13481    0.008    0.000    0.008    0.000 {method 'startswith' of 'str' objects}
      442    0.000    0.000    0.000    0.000 {method 'strip' of 'str' objects}
      210    0.001    0.000    0.001    0.000 {method 'sub' of 're.Pattern' objects}
      169    0.004    0.000    0.004    0.000 {method 'tick' of 'pygame.time.Clock' objects}
        2    0.000    0.000    0.000    0.000 {method 'title' of 'str' objects}
        7    0.000    0.000    0.000    0.000 {method 'tolist' of 'memoryview' objects}
        1    0.000    0.000    0.000    0.000 {method 'toordinal' of 'datetime.date' objects}
       55    0.000    0.000    0.000    0.000 {method 'translate' of 'bytearray' objects}
       99    0.000    0.000    0.000    0.000 {method 'translate' of 'str' objects}
        1    0.000    0.000    0.000    0.000 {method 'union' of 'set' objects}
      881    0.002    0.000    0.002    0.000 {method 'update' of 'dict' objects}
       53    0.000    0.000    0.000    0.000 {method 'upper' of 'str' objects}
       17    0.000    0.000    0.000    0.000 {method 'values' of 'dict' objects}
        1    0.000    0.000    0.000    0.000 {method 'view' of 'numpy.generic' objects}
      4/3    0.000    0.000    0.000    0.000 {method 'view' of 'numpy.ndarray' objects}
      228    0.000    0.000    0.000    0.000 {method 'zfill' of 'str' objects}

Images:

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

\$\endgroup\$
2
  • 3
    \$\begingroup\$ You also have a second answer that mentions JIT in your previous question. I would wager that should speed things up significantly. \$\endgroup\$ Commented Nov 17, 2024 at 17:14
  • \$\begingroup\$ I hope to not have to use jit... but i may end up doing it... \$\endgroup\$ Commented Nov 17, 2024 at 21:03

2 Answers 2

6
+25
\$\begingroup\$

A few things jump out:

  1. Don't calculate the chunks lists. Based on the player's movements, you know which chunks might need to be loaded and which ones can be dropped. For example, if the player moves right you may need to load a column of chunks on the right and drop the column on the left. All the other chunks remain. You only need to figure out one row or column that might need to be filled.

  2. Don't blit all the chunks onto the screen every cycle. Use surface.scroll() to move most of the chunks that are already on the screen to their new position. Then blit new chunks along one of the edges (only one edge should need it).

The draw function should look something like:

a. erase all the entities by bliting the saved background over them.
b. scroll the background
c. blit the new chunks along which ever edge needs it
d. put all the entities back by:
   i.  blit the background from the screen where the entity is going
       to go to save it for step (a) next time around
   ii. blit the entity to the screen 
\$\endgroup\$
1
  • \$\begingroup\$ 1 - I have a method i figured out that is even better for my purposes, 2 - A few blits (ok maybe 16) is all you would save, which is nothing compared to the amount you would save fixing the world.load_nearest_chunk method. Yes if i did these things it would fix my performance problem but it isn't what i would do, thx for answering. +1 \$\endgroup\$ Commented Nov 24, 2024 at 5:39
2
\$\begingroup\$

I have managed to fix the world.load_nearest_chunk method and now I get over 60 fps! This is how i did it:

1 - pregenerate the chunks_in_range as self.chunks_in_range around (0, 0)

2 - when using chunk_pos of self.chunks_in_range add self.load_pos to it

3 - when finding what chunks to unload get their chunk_pos and see if chunk_pos[0](x) or chunk_pos[1](y) are not in self.chunk_range_x or self.chunk_range_y

Updated: world.py

...
class World:
    def __init__(self, load_distance, seed=0):
        self.flat = True
        if seed:
            self.flat = False

        self.surface_noise = perlin_noise.PerlinNoise(octaves=1, seed=seed)
        self.cave_noise = perlin_noise.PerlinNoise(octaves=1, seed=seed + 1)

        self.textures = Textures()
        self.chunks = {}
        self.chunk_textures = {}
        self.load_pos = (0, 0)
        self.last_load_pos = self.load_pos
        self.load_distance = load_distance

        self.chunk_range_x = range(
            self.load_pos[0] - self.load_distance,
            self.load_pos[0] + self.load_distance + 1,
        )
        self.chunk_range_y = range(
            self.load_pos[1] - self.load_distance,
            self.load_pos[1] + self.load_distance + 1,
        )
        self.chunks_in_range = [(x, y) for x in self.chunk_range_x for y in self.chunk_range_y]
        self.chunks_in_range.sort(key=abs_max)

        self.entitys = []
        self.chunk_to_load = None

...

    def load_nearest_chunk(self, fast=False):
        if not self.chunk_to_load:
            for pos in self.chunks_in_range:
                chunk_pos = (pos[0] + self.load_pos[0], pos[1] + self.load_pos[1])
                if chunk_pos not in self.chunks:
                    self.set_chunk_loader(chunk_pos)
                    break
        else:
            if fast:
                self.load_chunk_fast()
            else:
                self.load_chunk()
        
        chunks_to_unload = [
            chunk_pos
            for chunk_pos in self.chunks
            if (chunk_pos[0] - self.load_pos[0]) not in self.chunk_range_x
            or (chunk_pos[1] - self.load_pos[1]) not in self.chunk_range_y
        ]

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

Much better!

\$\endgroup\$

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.