如何在类似 Minecraft 的 2D 游戏中生成树或其他结构

How to generate trees or other structures over chunks in a 2D Minecraft-like game

我正在尝试创建类似 Minecraft 的 2D 游戏,但 运行 遇到了问题。我正在尝试在地形之上生成树木。地形是用单纯形噪声生成的,它被分成 8x8 块,如果有必要,游戏会在玩家每次移动时生成新的块。我尝试通过 运行domly 在地形顶部选择一个位置来生成树,并将其上方的块设置为我想要的块,然后我 运行 进入另一个问题。树木可能会进入相邻的块。我试图通过将树中其他块中的部分存储在字典中,并在生成另一个块时从字典中生成它们来解决这个问题,但是还有另一个问题。有时与包含大部分树的块相邻的块已经生成,所以当生成具有树的块时我无法覆盖它......我对如何让它工作有点困惑。

这是生成新块的代码,其中参数x和y是要生成块的位置:

def generate_chunk(x, y):
    chunk_data = {}
    for y_pos in range(CHUNK_SIZE):
        for x_pos in range(CHUNK_SIZE):
            block = (x * CHUNK_SIZE + x_pos, y * CHUNK_SIZE + y_pos)
            block_name = ""
            height = int(noise.noise2d(block[0]*0.1, 0)*5)
            if block[1] == 5-height:
                block_name = "grass_block"
            elif 5-height < block[1] < 10-height:
                block_name = "dirt"
            elif block[1] >= 10-height:
                block_name = "stone"
            if block_name != "":
                chunk_data[block] = block_name
    return chunk_data

这是生成玩家附近的区块并在玩家离开时临时删除和保存的主循环:

running = True
while running:
    dt = clock.tick(FPS) / 16
    pygame.display.set_caption(f"2D Minecraft | FPS: {int(clock.get_fps())}")

    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    rendered_chunks = []
    for y in range(int(HEIGHT/(CHUNK_SIZE*BLOCK_SIZE)+2)):
        for x in range(int(WIDTH/(CHUNK_SIZE*BLOCK_SIZE)+2)):
            chunk = (
                x - 1 + int(round(camera.pos.x / (CHUNK_SIZE * BLOCK_SIZE))), 
                y - 1 + int(round(camera.pos.y / (CHUNK_SIZE * BLOCK_SIZE)))
            )
            rendered_chunks.append(chunk)
            if chunk not in chunks:
                chunks[chunk] = Chunk(chunk)
    unrendered_chunks = []
    for y in range(int(HEIGHT/(CHUNK_SIZE*BLOCK_SIZE)+4)):
        for x in range(int(WIDTH/(CHUNK_SIZE*BLOCK_SIZE)+4)):
            chunk = (
                x - 2 + int(round(camera.pos.x / (CHUNK_SIZE * BLOCK_SIZE))), 
                y - 2 + int(round(camera.pos.y / (CHUNK_SIZE * BLOCK_SIZE)))
            )
            try: chunks[chunk]
            except: pass
            else:
                if chunk not in rendered_chunks:
                    unrendered_chunks.append(chunk)
    for chunk in unrendered_chunks:
        for block in chunks[chunk].block_data:
            if block in blocks:
                blocks[block].kill()
                del blocks[block]

    camera.update()
    player.update()
    screen.fill((135, 206, 250))
    for chunk in rendered_chunks:
        chunks[chunk].render()
    player.draw(screen)
    pygame.display.flip()

这是块 class 和块 class:

class Block(pygame.sprite.Sprite):
    def __init__(self, chunk, pos, name):
        pygame.sprite.Sprite.__init__(self)
        blocks[tuple(pos)] = self
        self.name = name
        self.chunk = chunk
        self.coords = vec(pos)
        self.pos = self.coords * BLOCK_SIZE
        self.image = block_textures[self.name]
        self.rect = self.image.get_rect()

    def update(self):
        self.rect.topleft = self.pos - camera.pos

    def draw(self, screen):
        screen.blit(self.image, self.rect.topleft)

class Chunk(object):
    def __init__(self, pos):
        self.pos = pos
        self.block_data = generate_chunk(pos[0], pos[1])
        for block in self.block_data:
            blocks[block] = Block(self, block, self.block_data[block])

    def render(self):
        if self.pos in rendered_chunks:
            for block in self.block_data:
                try: blocks[block]
                except:
                    blocks[block] = Block(self, block, self.block_data[block])
                blocks[block].update()
                blocks[block].draw(screen)
            pygame.draw.rect(screen, (255, 255, 0), (self.pos[0]*CHUNK_SIZE*BLOCK_SIZE-camera.pos[0], self.pos[1]*CHUNK_SIZE*BLOCK_SIZE-camera.pos[1], CHUNK_SIZE*BLOCK_SIZE, CHUNK_SIZE*BLOCK_SIZE), width=1)

最小的可重现代码,我认为所有需要的信息都在上面,但以防万一您需要其余的:

import pygame
from pygame.locals import *
from random import *
from math import *
import json
import os
import opensimplex

FPS = 60
WIDTH, HEIGHT = 1200, 600
SCR_DIM = (WIDTH, HEIGHT)
GRAVITY = 0.5
SLIDE = 0.3
TERMINAL_VEL = 24
BLOCK_SIZE = 64
CHUNK_SIZE = 8
SEED = randint(-2147483648, 2147483647)

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT), HWSURFACE | DOUBLEBUF)
pygame.display.set_caption("2D Minecraft")
clock = pygame.time.Clock()
mixer = pygame.mixer.init()
vec = pygame.math.Vector2
noise = opensimplex.OpenSimplex(seed=SEED)
seed(SEED)

block_textures = {}
for img in os.listdir("res/textures/blocks/"):
    block_textures[img[:-4]] = pygame.image.load("res/textures/blocks/"+img).convert_alpha()
for image in block_textures:
    block_textures[image] = pygame.transform.scale(block_textures[image], (BLOCK_SIZE, BLOCK_SIZE))

def intv(vector):
    return vec(int(vector.x), int(vector.y))

def inttup(tup):
    return (int(tup[0]), int(tup[1]))

def block_collide(ax, ay, width, height, b):
    a_rect = pygame.Rect(ax-camera.pos.x, ay-camera.pos.y, width, height)
    b_rect = pygame.Rect(b.pos.x-camera.pos.x, b.pos.y-camera.pos.y, BLOCK_SIZE, BLOCK_SIZE)
    if a_rect.colliderect(b_rect):
        return True
    return False

class Camera(pygame.sprite.Sprite):
    def __init__(self, master):
        self.master = master
        self.pos = self.master.size / 2
        self.pos = self.master.pos - self.pos - vec(SCR_DIM) / 2 + self.master.size / 2

    def update(self):
        tick_offset = self.master.pos - self.pos - vec(SCR_DIM) / 2 + self.master.size / 2
        if -1 < tick_offset.x < 1:
            tick_offset.x = 0
        if -1 < tick_offset.y < 1:
            tick_offset.y = 0
        self.pos += tick_offset / 10

class Player(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.size = vec(0.225*BLOCK_SIZE, 1.8*BLOCK_SIZE)
        self.width, self.height = self.size.x, self.size.y
        self.start_pos = vec(0, 3) * BLOCK_SIZE
        self.pos = vec(self.start_pos)
        self.coords = self.pos // BLOCK_SIZE
        self.vel = vec(0, 0)
        self.max_speed = 5.306
        self.jumping_max_speed = 6.6
        self.rect = pygame.Rect((0, 0, 0.225*BLOCK_SIZE, 1.8*BLOCK_SIZE))
        self.bottom_bar = pygame.Rect((self.rect.x+1, self.rect.bottom), (self.width-2, 1))
        self.on_ground = False

    def update(self):
        keys = pygame.key.get_pressed()
        if keys[K_a]:
            if self.vel.x > -self.max_speed:
                self.vel.x -= SLIDE
        elif self.vel.x < 0:
            self.vel.x += SLIDE
        if keys[K_d]:
            if self.vel.x < self.max_speed:
                self.vel.x += SLIDE
        elif self.vel.x > 0:
            self.vel.x -= SLIDE
        if keys[K_w] and self.on_ground:
            self.vel.y = -9.2
            self.vel.x *= 1.1
            if self.vel.x > self.jumping_max_speed:
                self.vel.x = self.jumping_max_speed
            elif self.vel.x < -self.jumping_max_speed:
                self.vel.x = -self.jumping_max_speed
        if -SLIDE < self.vel.x < SLIDE:
            self.vel.x = 0

        self.vel.y += GRAVITY
        if self.vel.y > TERMINAL_VEL:
            self.vel.y = TERMINAL_VEL
        self.move()
        self.bottom_bar = pygame.Rect((self.rect.left+1, self.rect.bottom), (self.width-2, 1))
        for block in blocks:
            if self.bottom_bar.colliderect(blocks[block].rect):
                self.on_ground = True
                break
        else:
            self.on_ground = False
        if self.on_ground:
            self.vel.x *= 0.99
        self.coords = self.pos // BLOCK_SIZE
        self.chunk = self.coords // CHUNK_SIZE
        self.rect.topleft = self.pos - camera.pos

    def draw(self, screen):
        pygame.draw.rect(screen, (0, 0, 0), self.rect)

    def move(self):
        for y in range(4):
            for x in range(3):
                try:
                    block = blocks[(int(self.coords.x-1+x), int(self.coords.y-1+y))]
                except:
                    pass
                else:
                    if self.vel.y < 0:
                        if block_collide(floor(self.pos.x), floor(self.pos.y+self.vel.y), self.width, self.height, block):
                            self.pos.y = floor(block.pos.y + BLOCK_SIZE)
                            self.vel.y = 0
                    elif self.vel.y >= 0:
                        if self.vel.x <= 0:
                            if block_collide(floor(self.pos.x), ceil(self.pos.y+self.vel.y), self.width, self.height, block):
                                self.pos.y = ceil(block.pos.y - self.height)
                                self.vel.y = 0
                        elif self.vel.x > 0:
                            if block_collide(ceil(self.pos.x), ceil(self.pos.y+self.vel.y), self.width, self.height, block):
                                self.pos.y = ceil(block.pos.y - self.height)
                                self.vel.y = 0
                    if self.vel.x < 0:
                        if block_collide(floor(self.pos.x+self.vel.x), floor(self.pos.y), self.width, self.height, block):
                            self.pos.x = floor(block.pos.x + BLOCK_SIZE)
                            self.vel.x = 0
                    elif self.vel.x >= 0:
                        if block_collide(ceil(self.pos.x+self.vel.x), ceil(self.pos.y), self.width, self.height, block):
                            self.pos.x = ceil(block.pos.x - self.width)
                            self.vel.x = 0
        self.pos += self.vel

class Block(pygame.sprite.Sprite):
    def __init__(self, chunk, pos, name):
        pygame.sprite.Sprite.__init__(self)
        blocks[tuple(pos)] = self
        self.name = name
        self.chunk = chunk
        self.coords = vec(pos)
        self.pos = self.coords * BLOCK_SIZE
        self.image = block_textures[self.name]
        self.rect = self.image.get_rect()

    def update(self):
        self.rect.topleft = self.pos - camera.pos

    def draw(self, screen):
        screen.blit(self.image, self.rect.topleft)

class Chunk(object):
    def __init__(self, pos):
        self.pos = pos
        self.block_data = generate_chunk(pos[0], pos[1])
        for block in self.block_data:
            blocks[block] = Block(self, block, self.block_data[block])

    def render(self):
        if self.pos in rendered_chunks:
            for block in self.block_data:
                try: blocks[block]
                except:
                    blocks[block] = Block(self, block, self.block_data[block])
                blocks[block].update()
                blocks[block].draw(screen)
            pygame.draw.rect(screen, (255, 255, 0), (self.pos[0]*CHUNK_SIZE*BLOCK_SIZE-camera.pos[0], self.pos[1]*CHUNK_SIZE*BLOCK_SIZE-camera.pos[1], CHUNK_SIZE*BLOCK_SIZE, CHUNK_SIZE*BLOCK_SIZE), width=1)

def generate_chunk(x, y):
    chunk_data = {}
    for y_pos in range(CHUNK_SIZE):
        for x_pos in range(CHUNK_SIZE):
            block = (x * CHUNK_SIZE + x_pos, y * CHUNK_SIZE + y_pos)
            block_name = ""
            height = int(noise.noise2d(block[0]*0.1, 0)*5)
            if block[1] == 5-height:
                block_name = "grass_block"
            elif 5-height < block[1] < 10-height:
                block_name = "dirt"
            elif block[1] >= 10-height:
                block_name = "stone"
            if block_name != "":
                chunk_data[block] = block_name
    return chunk_data

blocks = {}
chunks = {}
player = Player()
camera = Camera(player)

running = True
while running:
    dt = clock.tick(FPS) / 16
    pygame.display.set_caption(f"2D Minecraft | FPS: {int(clock.get_fps())}")

    for event in pygame.event.get():
        if event.type == QUIT:
            running = False

    rendered_chunks = []
    for y in range(int(HEIGHT/(CHUNK_SIZE*BLOCK_SIZE)+2)):
        for x in range(int(WIDTH/(CHUNK_SIZE*BLOCK_SIZE)+2)):
            chunk = (
                x - 1 + int(round(camera.pos.x / (CHUNK_SIZE * BLOCK_SIZE))), 
                y - 1 + int(round(camera.pos.y / (CHUNK_SIZE * BLOCK_SIZE)))
            )
            rendered_chunks.append(chunk)
            if chunk not in chunks:
                chunks[chunk] = Chunk(chunk)
    unrendered_chunks = []
    for y in range(int(HEIGHT/(CHUNK_SIZE*BLOCK_SIZE)+4)):
        for x in range(int(WIDTH/(CHUNK_SIZE*BLOCK_SIZE)+4)):
            chunk = (
                x - 2 + int(round(camera.pos.x / (CHUNK_SIZE * BLOCK_SIZE))), 
                y - 2 + int(round(camera.pos.y / (CHUNK_SIZE * BLOCK_SIZE)))
            )
            try: chunks[chunk]
            except: pass
            else:
                if chunk not in rendered_chunks:
                    unrendered_chunks.append(chunk)
    for chunk in unrendered_chunks:
        for block in chunks[chunk].block_data:
            if block in blocks:
                blocks[block].kill()
                del blocks[block]

    camera.update()
    player.update()
    screen.fill((135, 206, 250))
    for chunk in rendered_chunks:
        chunks[chunk].render()
    player.draw(screen)
    pygame.display.flip()

pygame.quit()
quit()

(顺便说一句,黄线是区块边界)

总体思路是了解一个结构(例如一棵树)可以有多大,计算它可以跨越多少块,然后在生成块时检查 (x, y) 它周围的所有块。这可以通过这样的方式完成:


TREE_SHAPE = {
    (0, 0): "oak_log",
    (0, -1): "oak_log",
    (0, -2): "oak_log",
    (0, -3): "oak_log",
    (0, -4): "oak_leaves",

    (1, -4): "oak_leaves",
    (2, -4): "oak_leaves",
    (3, -4): "oak_leaves",

    (-1, -4): "oak_leaves",
    (-2, -4): "oak_leaves",
    (-3, -4): "oak_leaves",
}
MAX_TREE_SIZE = (max(x for x, y in TREE_SHAPE) - min(x for x, y in TREE_SHAPE) + 1,
                 max(y for x, y in TREE_SHAPE) - min(y for x, y in TREE_SHAPE) + 1)

CHUNKS_TO_CHECK = int(ceil(MAX_TREE_SIZE[0] / CHUNK_SIZE)), int(ceil(MAX_TREE_SIZE[1] / CHUNK_SIZE))


def generate_tree(base):
    return {(base[0] + offset[0], base[1] + offset[1]): block for offset, block in TREE_SHAPE.items()}

# Replace everything above with however you want to generate Trees.
# It might be worth factoring that out into a StructureGenerator class.


def get_trees(x, y):
    out = []
    seed(SEED + x * CHUNK_SIZE + y)  # Make sure this function always produces the same output
    for _ in range(CHUNK_SIZE // 8):  # At most one Tree attempt per 4 blocks
        block_x = x * CHUNK_SIZE + randrange(0, CHUNK_SIZE)
        grass_y = int(5 - noise.noise2d(block_x * 0.1, 0) * 5)  # Same as in generate_chunk
        if not 0 <= grass_y - y * CHUNK_SIZE < CHUNK_SIZE:  # Tree spot not in this chunk
            continue
        out.append(generate_tree((block_x, grass_y)))
    return out


def generate_chunk(x, y):
    chunk_data = {}
    # ... Your old code
    for ox in range(-CHUNKS_TO_CHECK[0], CHUNKS_TO_CHECK[0] + 1):
        for oy in range(-CHUNKS_TO_CHECK[1], CHUNKS_TO_CHECK[1] + 1):
            # For each Chunk around us (and ourself), check which trees there are.
            trees = get_trees(x + ox, y + oy)
            for tree in trees:
                for block, block_name in tree.items():
                    if 0<=block[0]-x*CHUNK_SIZE<CHUNK_SIZE and 0<=block[0]-x*CHUNK_SIZE<CHUNK_SIZE:
                        # This block is in this chunk
                        chunk_data[block] = block_name
    return chunk_data