October 21, 2024
Chicago 12, Melborne City, USA
python

How do I make caves randomly generate in pygame?


My game is supposed to be like Minecraft using pygame, but 2D. I do not know how to generate caves, and other questions I looked at didn’t really work, like this: Perlin worms for 2D cave generation. That did not explain how to, and I am trying to find out.
I heard of perlin noise, but I don’t know how to implement.
My code generates line by line, and I don’t know how to implement this for caves.

I’m not really sure how to make a picture of what I want, so I manually dug a cave:

Manually dug cave.

The game looks like this:

The game I have

Here is the line generation:

    def generate_line(self, x, max):
        line = []
        if max:
            y = self.current_max_y + (randint(-1, 1) * GRID_SIZE)
            self.current_max_y = y
        else:
            y = self.current_min_y + (randint(-1, 1) * GRID_SIZE)
            self.current_min_y = y
        tree_generates = self.get_random(TREE_CHANCE)

        if tree_generates:
            tree_y = y - GRID_SIZE
            tree_height = randint(TREE_HEIGHT - HEIGHT_VARIABILITY, TREE_HEIGHT + HEIGHT_VARIABILITY)

            for i in range(tree_height):
                log_block = self.make_block(x, tree_y, self.log_block, LOG, False, 10)
                tree_y -= GRID_SIZE
                left_leaf = self.make_block(x - GRID_SIZE, tree_y, self.leaf_block, LEAVES, False, 1)
                middle_leaf = self.make_block(x, tree_y, self.leaf_block, LEAVES, False, 1)
                right_leaf = self.make_block(x + GRID_SIZE, tree_y, self.leaf_block, LEAVES, False, 1)
                self.blocks.append(middle_leaf)
                self.blocks.append(log_block)
                self.blocks.append(left_leaf)
                self.blocks.append(right_leaf)
            top_leaf = self.make_block(x, tree_y - GRID_SIZE, self.leaf_block, LEAVES, False, 1)
            self.blocks.append(top_leaf)

        grass_y = y

        grass_block = self.make_block(x, y, self.grass_block, GRASS, durability = 5)
        line.append(grass_block)
        y += GRID_SIZE

        while y <= DIRT_UNTIL + grass_y:
            dirt_block = self.make_block(x, y, self.dirt_block, DIRT, durability = 5)
            line.append(dirt_block)
            y += GRID_SIZE

        while y <= STONE_UNTIL:
            coal_generates = self.get_random(COAL_CHANCE)
            iron_generates = self.get_random(IRON_CHANCE)
            gold_generates = self.get_random(GOLD_CHANCE)

            block = self.make_block(x, y, self.stone_block, STONE, durability = 10)

            if coal_generates and y > (coal_generates * GRID_SIZE):
                block = self.make_block(x, y, self.coal_block, COAL, durability = 8)
            elif iron_generates and y > (iron_generates * GRID_SIZE):
                block = self.make_block(x, y, self.iron_block, IRON, durability = 12)
            elif gold_generates and y > (gold_generates * GRID_SIZE):
                block = self.make_block(x, y, self.gold_block, GOLD, durability = 15)

            line.append(block)
            y += GRID_SIZE

        infurium_block = self.make_block(x, y, self.infurium_block, INFURIUM, durability = 2 ** 64)
        line.append(infurium_block)

        for block in line:
            self.blocks.append(block)

Here is the constants:

WIDTH, HEIGHT = 720, 405
FPS = 60

WHITE = (255, 255, 255)
BLACK = (  0,   0,   0)
SKY_BLUE = (60, 140, 255)
GRAY = (128, 128, 128)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
ORANGE = (255, 128, 0)

GRID_SIZE = 64

# Files
GRASS_BLOCK = "grass_block.png"
DIRT_BLOCK = "dirt_block.png"
STONE_BLOCK = "stone_block.png"
IRON_ORE = "iron_ore.png"
GOLD_ORE = "gold_ore.png"
COAL_ORE = "coal_ore.png"
RED_FLOWER = "red_flower.png"
ORANGE_FLOWER = "orange_flower.png"
YELLOW_FLOWER = "yellow_flower.png"
LOG_BLOCK = "log.png"
LEAF_BLOCK = "leaves.png"
INFURIUM_BLOCK = "infurium_block.png"
FONT = "Monoserrat.ttf"

GRASS = "grass"
DIRT = "dirt"
STONE = "stone"
IRON = "iron"
GOLD = "gold"
COAL = "coal"
INFURIUM = "infurium"
RED_FLOW = "red flower"
ORANGE_FLOW = "orange flower"
YELLOW_FLOW = "yellow flower"
LOG = "log"
LEAVES = "leaves"

SAVE_DATA = "save_data.txt"

BORDER_COLOR = (  0,  0,  0)

USER = "user.png"

GRASS_SPAWN = 300
DIRT_UNTIL = 300 + (GRID_SIZE * 5)
STONE_UNTIL = 300 + (GRID_SIZE * 5) + (GRID_SIZE * 60)

COAL_SPAWNS = 20
IRON_SPAWNS = 40
GOLD_SPAWNS = 50

COAL_CHANCE = 15
IRON_CHANCE = 10
GOLD_CHANCE = 5

TREE_CHANCE = 20
TREE_HEIGHT = 4
HEIGHT_VARIABILITY = 2

FLOWER_CHANCE = 10

# Speed at which lines render, 0 means every frame.
RENDER_SPEED = 0
PLAYER_SPEED = 250
# Size of images in inventory
INVENTORY_SIZE = 32

# Cracks
CRACK_1 = "crack_1.png"
CRACK_2 = "crack_2.png"
CRACK_3 = "crack_3.png"
CRACK_4 = "crack_4.png"
CRACK_5 = "crack_5.png"

DURABILITY_DRAIN = 5
GRAVITY = 400
COLLISION_MARGIN = 20

# If a block is more than `MAX_DIST` pixels away, you can't mine it.
MAX_DIST = 100

MOD = "cave_game.CGMOD"

Here is the classes:

import pygame
from constants import BORDER_COLOR, INFURIUM

class Block:
    def __init__(self, x, y, block, type, collisible = True, durability = 10):
        self.x, self.y = x, y
        self.block = block

        self.durability = durability
        self.max_durability = self.durability

        self.broken = False
        self.type = type

        # Collisible = Collision + Able = able to collide.
        self.collisible = collisible

        self.rect = pygame.Rect(self.x, self.y, self.block.get_width(), self.block.get_height())
    def draw(self, display, pos, marks):
        self.rect = pygame.Rect(self.x - pos[0], self.y - pos[1], self.block.get_width(), self.block.get_height())
        display.blit(self.block, [self.x - pos[0], self.y - pos[1]])

        durability_left = self.durability / self.max_durability
        mark = None

        if self.durability <= 0:
            self.broken = True

        if durability_left < 0.1:
            mark = marks[4]
        elif durability_left < 0.3:
            mark = marks[3]
        elif durability_left < 0.5:
            mark = marks[2]
        elif durability_left < 0.7:
            mark = marks[1]
        elif durability_left < 0.9:
            mark = marks[0]

        if mark:
            display.blit(mark, self.rect)

    def draw_border(self, display):
        pygame.draw.rect(display, BORDER_COLOR, self.rect, 3)
    def draw_on(self, display, to_draw):
        display.blit(to_draw, self.rect)

Here is the whole code(over 300 lines):

# Import external modules: I have not been able to fully comment the code.
import pygame
from random import randint

# The block class, and all the constants.
import classes
from constants import *

from ast import literal_eval

# The main game class.
class ProceduralGenerationGame:
    def __init__(self):
        # Initialize pygame
        pygame.init()

        # Get the display named and pass it pygame.SCALED|pygame.FULLSCREEN
        self.display = pygame.display.set_mode((WIDTH, HEIGHT), pygame.SCALED|pygame.FULLSCREEN)
        pygame.display.set_caption("2D Cave Game")

        # The variables with the time and the boolean if the game is over.
        self.running = True
        self.clock = pygame.time.Clock()

        # All the images.
        self.grass_block = self.process_image(GRASS_BLOCK)
        self.dirt_block = self.process_image(DIRT_BLOCK)
        self.stone_block = self.process_image(STONE_BLOCK)

        # Ores
        self.coal_block = self.process_image(COAL_ORE)
        self.iron_block = self.process_image(IRON_ORE)
        self.gold_block = self.process_image(GOLD_ORE)

        self.leaf_block = self.process_image(LEAF_BLOCK)
        self.log_block = self.process_image(LOG_BLOCK)

        self.infurium_block = self.process_image(INFURIUM_BLOCK)

        self.user_image = self.process_image(USER)

        self.crack_1 = self.process_image(CRACK_1)
        self.crack_2 = self.process_image(CRACK_2)
        self.crack_3 = self.process_image(CRACK_3)
        self.crack_4 = self.process_image(CRACK_4)
        self.crack_5 = self.process_image(CRACK_5)

        self.cracks = (self.crack_1, self.crack_2, self.crack_3, self.crack_4, self.crack_5)

        self.red_flower = self.process_image(RED_FLOWER)
        self.orange_flower = self.process_image(ORANGE_FLOWER)
        self.yellow_flower = self.process_image(YELLOW_FLOWER)

        self.flowers = (self.red_flower, self.orange_flower, self.yellow_flower)
        self.image_types = {GRASS: self.grass_block, DIRT: self.dirt_block, STONE: self.stone_block, LOG: self.log_block, LEAVES: self.leaf_block, INFURIUM: self.infurium_block, COAL: self.coal_block, IRON: self.iron_block, GOLD: self.gold_block, RED_FLOW: self.red_flower, ORANGE_FLOW: self.orange_flower, YELLOW_FLOW: self.yellow_flower}
        # End of images

        self.tiny_font = pygame.font.Font(FONT, 14)

        # The most important list, the blocks list.
        self.blocks = []
        self.inventory = {}

        self.health = 100

        # Used for rendering at which height, so heights don't go crazy.
        self.current_min_y = GRASS_SPAWN
        self.current_max_y = GRASS_SPAWN
    def run(self):
        self.game_program()
    def game_program(self):
        """TODO: OPTIMIZE RENDERING"""
        render_x_min, render_x_max = WIDTH / 2, WIDTH / 2 + GRID_SIZE
        spawn_wait = 0

        camera_x, camera_y = 0, -100
        player_y_vel = 0

        valid_blocks = []

        mousing_block = None
        pressing_e = False

        with open(MOD, "r") as cave_mod:
            self.mod_file = literal_eval(cave_mod.read())

        while self.running:
            delta_time = self.clock.tick(FPS) / 1000
            mouse_x, mouse_y = pygame.mouse.get_pos()

            game_keys = pygame.key.get_pressed()
            player_moving = False

            if game_keys[pygame.K_a]:
                camera_x -= PLAYER_SPEED * delta_time
                player_moving = True
            if game_keys[pygame.K_d]:
                player_moving = True
                camera_x += PLAYER_SPEED * delta_time

            if spawn_wait > RENDER_SPEED:
                spawn_wait = 0

                # Added (GRID_SIZE * 3) to render further than the user can see, so the user cannot see trees spawning.
                if camera_x - (GRID_SIZE * 3) < (render_x_min + GRID_SIZE):
                    self.generate_line(render_x_min, False)
                    render_x_min -= GRID_SIZE
                if camera_x + WIDTH + (GRID_SIZE * 3) > (render_x_max - GRID_SIZE):
                    self.generate_line(render_x_max, True)
                    render_x_max += GRID_SIZE
            else:
                spawn_wait += delta_time

            player_rect = self.user_image.get_rect()
            player_rect.centerx, player_rect.y = WIDTH / 2, GRASS_SPAWN - GRID_SIZE

            rel_x, rel_y = player_rect.x, player_rect.y

            for block in valid_blocks:
                if not block.rect.colliderect(player_rect) or not block.collisible:
                    continue

                player_bottom = player_rect.bottom
                player_right = player_rect.right
                player_left = player_rect.left
                player_top = player_rect.y
                rect_top = block.rect.y
                rect_left = block.rect.x
                rect_right = block.rect.right
                rect_bottom = block.rect.bottom

                # Ensure player is aligned horizontally with block to avoid falling off
                on_block_horizontally = (player_rect.centerx + (GRID_SIZE / 4) >= rect_left and player_rect.centerx - (GRID_SIZE / 4) <= rect_right) or player_moving

                top_collision = True if (player_bottom - COLLISION_MARGIN < rect_top) and on_block_horizontally else False
                right_collision = True if (player_right - COLLISION_MARGIN < rect_left) else False
                left_collision = True if (player_left + COLLISION_MARGIN > rect_right) else False
                bottom_collision = True if (player_top + COLLISION_MARGIN > rect_bottom) else False

                if top_collision:
                    camera_y = block.y - GRID_SIZE - rel_y
                    if player_y_vel > 0:
                        if player_y_vel > GRAVITY * 1.2:
                            self.health -= player_y_vel / 20
                        player_y_vel = 0
                elif bottom_collision:
                    camera_y = (block.y + GRID_SIZE) - rel_y
                    if player_y_vel < 0:
                        player_y_vel = 0
                else:
                    if right_collision:
                        camera_x = block.x - GRID_SIZE - rel_x
                    if left_collision:
                        camera_x = block.x + GRID_SIZE - rel_x

                #if not top_collision and not bottom_collision and not right_collision and not left_collision:
                    #player_y_vel = 0
                    #camera_y = rect_bottom + GRID_SIZE - rel_y

            player_y_vel += GRAVITY * delta_time

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.running = False
                    if event.key == pygame.K_SPACE:
                        player_y_vel = -GRAVITY / 1.5
                    if event.key == pygame.K_e:
                        if mousing_block:
                            self.blocks[mousing_block].durability -= 1
                        pressing_e = True
                if event.type == pygame.KEYUP:
                    if event.key == pygame.K_e:
                        pressing_e = False
            camera_y += player_y_vel * delta_time
            valid_blocks = []

            self.display.fill(SKY_BLUE)

            broken_block = None

            for index, block in enumerate(self.blocks):
                spawn_block = True if (block.x > -GRID_SIZE + camera_x) and (block.x < WIDTH + camera_x) else False
                if spawn_block:
                    valid_blocks.append(block)
                    block.draw(self.display, (camera_x, camera_y), self.cracks)
                if block.rect.collidepoint(mouse_x, mouse_y):
                    mousing_block = index
                if block.broken:
                    broken_block = index
                    mousing_block = None
            if broken_block:
                if not self.blocks[broken_block].type in self.inventory:
                    self.inventory[self.blocks[broken_block].type] = 1
                else:
                    self.inventory[self.blocks[broken_block].type] += 1
                del self.blocks[broken_block]
            if mousing_block:
                self.blocks[mousing_block].draw_border(self.display)
            self.display.blit(self.user_image, player_rect)

            self.draw_health_bar(5, 5)

            object_x = 64
            inventory_length = len(self.inventory) * INVENTORY_SIZE
            pygame.draw.rect(self.display, ORANGE, (object_x - 4, HEIGHT - 36, inventory_length + 8, INVENTORY_SIZE + 4))
            for inventory_object in self.inventory.keys():
                amount_had = self.tiny_font.render(str(self.inventory[inventory_object]), False, WHITE)
                self.display.blit(self.resize_surface(self.image_types[inventory_object], (INVENTORY_SIZE, INVENTORY_SIZE)), (object_x, HEIGHT - 32))
                self.display.blit(amount_had, (object_x + 2, HEIGHT - 30))
                object_x += INVENTORY_SIZE

            pygame.display.flip()
        pygame.quit()
    def menu_program(self):
        while self.running:
            delta_time = self.clock.tick(FPS) / 1000

            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.running = False
            self.display.fill(WHITE)

            pygame.display.flip()
        pygame.quit()
    def resize_surface(self, surface, size, bg = WHITE):
        resized_surface = pygame.transform.scale(surface, size)
        resized_surface.set_colorkey(WHITE)

        return resized_surface
    def draw_health_bar(self, x, y):
        if self.health < 0:
            self.health = 0
        color = 255 - self.health * 2.55, self.health * 2.55, 0
        pygame.draw.rect(self.display, GRAY, (x, y, 110, 30))

        pygame.draw.rect(self.display, color, (x + 5, y + 5, self.health, 20))
    @staticmethod
    def process_image(location, color_key = WHITE):
        try:
            image = pygame.image.load(location).convert_alpha()
            image.set_colorkey(color_key)
        except Exception as error:
            image = pygame.Surface((GRID_SIZE, GRID_SIZE))
            print(f"Error in loading image: {error}")
        return image
    def generate_line(self, x, max):
        line = []
        if max:
            y = self.current_max_y + (randint(-1, 1) * GRID_SIZE)
            self.current_max_y = y
        else:
            y = self.current_min_y + (randint(-1, 1) * GRID_SIZE)
            self.current_min_y = y
        tree_generates = self.get_random(TREE_CHANCE)

        if tree_generates:
            tree_y = y - GRID_SIZE
            tree_height = randint(TREE_HEIGHT - HEIGHT_VARIABILITY, TREE_HEIGHT + HEIGHT_VARIABILITY)

            for i in range(tree_height):
                log_block = self.make_block(x, tree_y, self.log_block, LOG, False, 10)
                tree_y -= GRID_SIZE
                left_leaf = self.make_block(x - GRID_SIZE, tree_y, self.leaf_block, LEAVES, False, 1)
                middle_leaf = self.make_block(x, tree_y, self.leaf_block, LEAVES, False, 1)
                right_leaf = self.make_block(x + GRID_SIZE, tree_y, self.leaf_block, LEAVES, False, 1)
                self.blocks.append(middle_leaf)
                self.blocks.append(log_block)
                self.blocks.append(left_leaf)
                self.blocks.append(right_leaf)
            top_leaf = self.make_block(x, tree_y - GRID_SIZE, self.leaf_block, LEAVES, False, 1)
            self.blocks.append(top_leaf)

        grass_y = y

        grass_block = self.make_block(x, y, self.grass_block, GRASS, durability = 5)
        line.append(grass_block)
        y += GRID_SIZE

        while y <= DIRT_UNTIL + grass_y:
            dirt_block = self.make_block(x, y, self.dirt_block, DIRT, durability = 5)
            line.append(dirt_block)
            y += GRID_SIZE

        while y <= STONE_UNTIL:
            coal_generates = self.get_random(COAL_CHANCE)
            iron_generates = self.get_random(IRON_CHANCE)
            gold_generates = self.get_random(GOLD_CHANCE)

            block = self.make_block(x, y, self.stone_block, STONE, durability = 10)

            if coal_generates and y > (coal_generates * GRID_SIZE):
                block = self.make_block(x, y, self.coal_block, COAL, durability = 8)
            elif iron_generates and y > (iron_generates * GRID_SIZE):
                block = self.make_block(x, y, self.iron_block, IRON, durability = 12)
            elif gold_generates and y > (gold_generates * GRID_SIZE):
                block = self.make_block(x, y, self.gold_block, GOLD, durability = 15)

            line.append(block)
            y += GRID_SIZE

        infurium_block = self.make_block(x, y, self.infurium_block, INFURIUM, durability = 2 ** 64)
        line.append(infurium_block)

        for block in line:
            self.blocks.append(block)
    @staticmethod
    def make_block(x, y, image, type, collisible = True, durability = 10):
        return classes.Block(x, y, image, type, collisible, durability)
    @staticmethod
    def get_random(chance, out_of = 100):
        if randint(1, out_of) <= chance:
            return True
        return False
    @staticmethod
    def round_to_nearest(number):
        return round(number // GRID_SIZE) * GRID_SIZE

if __name__ == "__main__":
    game = ProceduralGenerationGame()
    game.run()



You need to sign in to view this answers

Leave feedback about this

  • Quality
  • Price
  • Service

PROS

+
Add Field

CONS

+
Add Field
Choose Image
Choose Video