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:
The game looks like this:
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