Refactor map handling/generation/rendering

master
GreenXenith 2020-04-23 14:19:46 -07:00
parent 4d52a67743
commit cb8ce89166
28 changed files with 351 additions and 247 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/loot_gold.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
assets/loot_pile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/rect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
assets/wall_cobble.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

BIN
none.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

View File

@ -3,9 +3,14 @@ import pygame
global assets
assets = {}
none = pygame.image.load("none.png")
def load(filename):
assets[filename] = pygame.image.load("assets/" + filename)
assets[filename] = pygame.image.load("assets/" + filename) #.convert()
return assets[filename]
def get(filename):
return assets[filename]
try:
return assets[filename]
except:
return none

174
src/dungeon.py Normal file
View File

@ -0,0 +1,174 @@
"""
Dungeon Generator
Based on https://www.jamesbaum.co.uk/blether/procedural-level-generation-rust/
"""
import random
import time
import math
from .tiles import Tile
from .vector import Vector
# Helper functions
def rand(*args):
random.seed(time.clock())
return random.randint(*args)
# Room class for generation handling
class Room():
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
self.left = x
self.right = x + width
self.top = y
self.bottom = y + height
self.cx = math.floor(self.x + (width / 2))
self.cy = math.floor(self.y + (height / 2))
def intersects(self, other):
return self.left <= other.right and self.right >= other.left and self.top <= other.bottom and self.bottom >= other.top
# Generator class in case its used multiple times
class Generator():
rooms = [] # Stores room objects
board = [] # Map of zeros and ones
def __init__(self, width, height = None):
self.width = width
self.height = height or width
for y in range(self.height + 1):
self.board.append([])
for _ in range(self.width + 1):
self.board[y].append(0)
self.place_rooms()
self.place_corridors()
def place_rooms(self):
for _ in range(rand(10, 30)): # Amount of rooms
width = rand(4, 10) # Room size
height = rand(4, 10)
x = rand(0, self.width - width) # Room position
y = rand(0, self.height - height)
if x + width > self.width:
x = self.width - width
if y + height > self.height:
y = self.height - height
collides = False
room = Room(x, y, width, height)
# Make sure room doesn't collide
for other_room in self.rooms:
if room.intersects(other_room):
collides = True
break
# Add room if valid place
if not collides:
for row in range(room.height):
for col in range(room.width):
self.board[room.y + row][room.x + col] = 1
self.rooms.append(room)
# Connect rooms together
def place_corridors(self):
for i in range(len(self.rooms) - 1):
room1 = self.rooms[i]
room2 = self.rooms[i + 1]
if rand(0, 2) == 0:
if room1.cx <= room2.cx:
self.horiz_corridor(room1.cx, room2.cx, room1.cy)
else:
self.horiz_corridor(room2.cx, room1.cx, room1.cy)
if room1.cy <= room2.cy:
self.vert_corridor(room1.cy, room2.cy, room2.cx)
else:
self.vert_corridor(room2.cy, room1.cy, room2.cx)
else:
if room1.cy <= room2.cy:
self.vert_corridor(room1.cy, room2.cy, room2.cx)
else:
self.vert_corridor(room2.cy, room1.cy, room2.cx)
if room1.cx <= room2.cx:
self.horiz_corridor(room1.cx, room2.cx, room1.cy)
else:
self.horiz_corridor(room2.cx, room1.cx, room1.cy)
def horiz_corridor(self, x1, x2, y):
for row in range(y - 1, y + 2):
for col in range(x1 - 1, x2 + 2):
self.board[row][col] = 1
def vert_corridor(self, y1, y2, x):
for row in range(y1, y2 + 2):
for col in range(x - 1, x + 2):
self.board[row][col] = 1
def generate(self, map):
for y in range(self.height):
for x in range(self.width):
if self.board[y][x] == 1:
# Floor
map.set_tile(x, y, 0, "map:cobble")
if self.board[y][x] == 1:
# Adjacent sides (Corresponds with rotation map below)
asides = [
[1, 0],
[0, 1],
[-1, 0],
[0, -1]
]
adj = 0 # How many adjacent
arot = 0 # Last adjacent key
for key in range(len(asides)):
off = asides[key]
if self.board[y + off[1]][x + off[0]] == 0:
adj += 1
arot = key
# Diagonal sides (Corresponds with rotation map below)
dsides = [
[-1, 1],
[-1, -1],
[1, -1],
[1, 1]
]
dia = 0 # How many diagonal
drot = 0 # Last diagonal key
for key in range(len(dsides)):
off = dsides[key]
if self.board[y + off[1]][x + off[0]] == 0:
dia += 1
# Only set key if opposite tile is floor
if self.board[y - off[1]][x - off[0]] == 1:
drot = key
tile = ""
rmap = [2, 3, 0, 1] # Rotation map
if adj == 0 and dia == 1: # Need diagonals to prevent false positives with all-floor
tile = "map:wall_cobble_corner_outer"
rot = rmap[drot]
elif adj == 1:
tile = "map:wall_cobble"
rot = rmap[arot]
elif adj == 2:
tile = "map:wall_cobble_corner_inner"
rot = rmap[drot]
if tile != "":
map.set_tile(x, y, 1, tile)
map.get_tile(x, y, 1).set_rotation(rot)

View File

@ -1,23 +1,24 @@
import pygame
import os, sys
from . import assets, controller, spritesheet
from .map import Map
from .player import Player
# Constants
SCALE = 3
METER = 32
FPS = 60
# Init
pygame.init()
pygame.font.init()
# Screen Init (might implement a display module later to handle maximizing)
winsize = [800, 600]
screen = pygame.display.set_mode(winsize, pygame.RESIZABLE)
from . import assets, controller, register, spritesheet
from .map import Map
from .player import Player
pygame.display.set_caption("Zoria")
pygame.display.set_icon(assets.load("icon.png"))
winsize = [800, 600]
screen = pygame.display.set_mode(winsize) #, pygame.RESIZABLE)
# Constants
SCALE = 2
METER = 32
FPS = 60
# Load all assets
for filename in os.listdir(os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "assets")):
@ -39,6 +40,8 @@ player.set_pos(map.generator.rooms[0].x + 2, map.generator.rooms[0].y + 2)
CENTER = [winsize[0] / 2, winsize[1] / 2]
arial = pygame.font.SysFont("Arial", 10)
# Mainloop
clock = pygame.time.Clock()
while 1:
@ -46,6 +49,11 @@ while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
elif event.type == pygame.VIDEORESIZE:
winsize = event.size
SCALE = 2 * (winsize[0] / 800)
CENTER = [winsize[0] / 2, winsize[1] / 2]
screen = pygame.display.set_mode(winsize, pygame.RESIZABLE)
screen.fill((0, 0, 0))
@ -56,19 +64,37 @@ while 1:
psize = player.sprite.rect.size
camera = [CENTER[0] - (psize[0] / 2 * SCALE), CENTER[1] - (psize[1] / 2 * SCALE)]
for layer in map.map["renderLayers"]:
for row in range(len(layer)):
for column in range(len(layer[row])):
materialIndex = layer[row][column]
if materialIndex != 0:
texture = assets.get(map.map["tiles"][materialIndex - 1])
tilesize = texture.get_rect().size[0]
screen.blit(pygame.transform.scale(texture, [SCALE * tilesize, SCALE * tilesize]), [
column * SCALE * tilesize - (player.pos.x * SCALE * METER) + camera[0],
row * SCALE * tilesize - (player.pos.y * SCALE * METER) + camera[1]
])
for z in range(len(map.map)):
for y in range(len(map.map[z])):
for x in range(len(map.map[z][y])):
tile = map.get_tile(x, y, z)
if tile:
texture = assets.get(tile.texture)
# Rotations are clockwise due to Y+ down rendering
# NOTE: This will be obsolete once game is converted to 2.5D
rotated = pygame.transform.rotate(texture, -90 * tile.rotation)
tilesize = texture.get_rect().size
tilex = (x * METER) - ((tilesize[0] / 2) - (METER / 2))
tiley = (y * METER) - (tilesize[1] - METER)
pos = [
camera[0] + round((tilex - (player.pos.x * METER)) * SCALE),
camera[1] + round((tiley - (player.pos.y * METER)) * SCALE)
]
# Only render tile if on-screen
if pos[0] + tilex >= 0 and pos[0] <= winsize[0] and \
pos[1] + tiley >= 0 and pos[1] <= winsize[1]:
screen.blit(pygame.transform.scale(rotated, [round(tilesize[0] * SCALE), round(tilesize[1] * SCALE)]), pos)
# text = arial.render(str(int(x)) + ", " + str(int(y)), False, (255, 255, 255))
# screen.blit(text, [x * 64 - (player.pos.x * round(SCALE * METER)) + camera[0], y * 64 - (player.pos.y * round(SCALE * METER)) + camera[1]])
# Draw player based on camera position
screen.blit(pygame.transform.scale(player.sprite.texture.frame, [SCALE * player.sprite.texture.width, SCALE * player.sprite.texture.height]), camera)
screen.blit(pygame.transform.scale(player.sprite.texture.frame, [round(SCALE * player.sprite.texture.width), round(SCALE * player.sprite.texture.height)]), camera)
pygame.display.flip()
pygame.display.update()
# pygame.display.flip()

View File

@ -1,206 +0,0 @@
import random
import time
import math
MSIZE = 80
def rand(*args):
random.seed(time.clock())
return random.randint(*args)
class Room():
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
self.left = x
self.right = x + width
self.top = y
self.bottom = y + height
self.cx = math.floor(self.x + (width / 2))
self.cy = math.floor(self.y + (height / 2))
def intersects(self, other):
return self.left <= other.right and self.right >= other.left and self.top <= other.bottom and self.bottom >= other.top
class Generator():
rooms = []
board = []
def __init__(self, width, height = None):
self.width = width
self.height = height or width
for y in range(self.height):
self.board.append([])
for _ in range(self.width):
self.board[y].append(0)
self.place_rooms()
self.place_corridors()
def place_rooms(self):
for _ in range(rand(10, 30)):
width = rand(4, 10)
height = rand(4, 10)
x = rand(0, 60)
y = rand(0, 60)
if x + width > self.width:
x = self.width - width
if y + height > self.height:
y = self.height - height
collides = False
room = Room(x, y, width, height)
for other_room in self.rooms:
if room.intersects(other_room):
collides = True
break
if not collides:
self.place_room(room)
def place_room(self, room):
for row in range(room.height):
for col in range(room.width):
y = room.y + row
x = room.x + col
self.board[y][x] = 1
self.rooms.append(room)
def place_corridors(self):
for i in range(0, len(self.rooms) - 1):
room1 = self.rooms[i]
room2 = self.rooms[i + 1]
if rand(0, 2) == 0:
if room1.cx <= room2.cx:
self.horiz_corridor(room1.cx, room2.cx, room1.cy)
else:
self.horiz_corridor(room2.cx, room1.cx, room1.cy)
if room1.cy <= room2.cy:
self.vert_corridor(room1.cy, room2.cy, room2.cx)
else:
self.vert_corridor(room2.cy, room1.cy, room2.cx)
else:
if room1.cy <= room2.cy:
self.vert_corridor(room1.cy, room2.cy, room2.cx)
else:
self.vert_corridor(room2.cy, room1.cy, room2.cx)
if room1.cx <= room2.cx:
self.horiz_corridor(room1.cx, room2.cx, room1.cy)
else:
self.horiz_corridor(room2.cx, room1.cx, room1.cy)
def horiz_corridor(self, x1, x2, y):
for row in range(y - 1, y + 2):
for col in range(x1 - 1, x2 + 2):
self.board[row][col] = 1
def vert_corridor(self, y1, y2, x):
for row in range(y1, y2 + 2):
for col in range(x - 1, x + 2):
self.board[row][col] = 1
def get_map(self):
# Map object
map = {
"tiles": [
"floor_cobble.png",
"wall_cobble_down.png",
"wall_cobble_right.png",
"wall_cobble_up.png",
"wall_cobble_left.png",
"wall_cobble_corner_nw_inner.png",
"wall_cobble_corner_ne_inner.png",
"wall_cobble_corner_se_inner.png",
"wall_cobble_corner_sw_inner.png",
"wall_cobble_corner_nw_outer.png",
"wall_cobble_corner_ne_outer.png",
"wall_cobble_corner_se_outer.png",
"wall_cobble_corner_sw_outer.png"
],
"renderLayers": []
}
# Generate walls
bounds = []
for y in range(self.height):
bounds.append([])
for x in range(self.width):
wall = 0
if self.board[y][x] > 0:
for x2 in range(x - 1, x + 2):
for y2 in range(y - 1, y + 2):
if self.board[y2][x2] == 0:
wall += 1
bounds[y].append(wall)
map["boundaries"] = bounds
# Floor layer
layer1 = self.board.copy()
map["renderLayers"].append(layer1)
# Wall layer
layer2 = []
for y in range(self.height):
layer2.append([])
for _ in range(self.width):
layer2[y].append(0)
for y in range(1, self.height - 1):
for x in range(1, self.width - 1):
if self.board[y][x] == 1:
tile = 0
if self.board[y][x - 1] == 1 and self.board[y][x + 1] == 1:
if self.board[y - 1][x] == 0:
tile = 2
elif self.board[y + 1][x] == 0:
tile = 4
if self.board[y - 1][x] == 1 and self.board[y + 1][x] == 1:
if self.board[y][x - 1] == 0:
tile = 3
elif self.board[y][x + 1] == 0:
tile = 5
layer2[y][x] = tile
for y in range(1, self.height - 1):
for x in range(1, self.width - 1):
if bounds[y][x] > 0 and layer2[y][x] == 0:
tile = 0
if bounds[y + 1][x] > 0 and bounds[y][x + 1] > 0:
if bounds[y][x] > 1:
tile = 6
else:
tile = 10
elif bounds[y + 1][x] > 0 and bounds[y][x - 1] > 0:
if bounds[y][x] > 1:
tile = 7
else:
tile = 11
elif bounds[y - 1][x] > 0 and bounds[y][x - 1] > 0:
if bounds[y][x] > 1:
tile = 8
else:
tile = 12
elif bounds[y - 1][x] > 0 and bounds[y][x + 1] > 0:
if bounds[y][x] > 1:
tile = 9
else:
tile = 13
layer2[y][x] = tile
map["renderLayers"].append(layer2)
return map

View File

@ -1,7 +1,7 @@
import pygame
import json, math
from . import assets
from .generator import Generator
from . import assets, dungeon, loot
from .tiles import Tile
from .vector import Vector
class Map:
@ -10,21 +10,44 @@ class Map:
def __init__(self, meter):
self.METER = meter # Pixels per 1 meter
def load(self, filename):
with open(filename) as file:
self.map = json.load(file)
# def load(self, filename):
# with open(filename) as file:
# self.map = json.load(file)
def generate(self):
self.generator = Generator(80)
self.map = self.generator.get_map()
self.generator = dungeon.Generator(80)
self.generator.generate(self)
# self.placer = loot.Placer()
# self.map = self.placer.populate(self.map)
def collides(self, pos, rect):
for y in range(int(math.floor(pos.y)) - 1, int(math.floor(pos.y)) + 2):
for x in range(int(math.floor(pos.x)) - 1, int(math.floor(pos.x)) + 2):
if not (x == pos.x and y == pos.y):
if self.map["boundaries"][y][x] > 0:
px = int(math.floor(pos.x))
py = int(math.floor(pos.y))
for y in range(py - 1, py + 2):
for x in range(px - 1, px + 2):
if y >= 0 and y < len(self.map[1]) and x >=0 and x < len(self.map[1][y]):
tile = self.map[1][y][x]
if tile and tile.is_solid():
if pos.x + (rect.width / self.METER) >= x and pos.x <= (x + 1) and \
pos.y + (rect.height / self.METER) >= y and pos.y <= (y + 1):
return True
return False
def set_tile(self, x, y, z, name):
for _ in range(len(self.map), z + 1):
self.map.append([])
for _ in range(len(self.map[z]), y + 1):
self.map[z].append([])
for _ in range(len(self.map[z][y]), x + 1):
self.map[z][y].append(None)
if name != "":
self.map[z][y][x] = Tile(name)
def get_tile(self, x, y, z):
try:
return self.map[z][y][x]
except:
return None

25
src/register.py Normal file
View File

@ -0,0 +1,25 @@
from . import tiles
tiles.register_tile("map:cobble", {
"texture": "floor_cobble.png",
})
tiles.register_tile("map:wall_cobble", {
"texture": "wall_cobble.png",
})
tiles.register_tile("map:wall_cobble_corner_inner", {
"texture": "wall_cobble_corner_inner.png",
})
tiles.register_tile("map:wall_cobble_corner_outer", {
"texture": "wall_cobble_corner_outer.png",
})
tiles.register_tile("loot:coins", {
"texture": "loot_gold.png",
})
tiles.register_tile("loot:pile", {
"texture": "loot_pile.png",
})

49
src/tiles.py Normal file
View File

@ -0,0 +1,49 @@
import pygame
global registered_tiles
registered_tiles = {}
content_ids = []
content_id_map = {}
def register_tile(name, definition):
registered_tiles[name] = definition
content_id_map[name] = len(content_ids)
content_ids.append(name)
def get_content_id(name):
try:
return content_id_map[name]
except:
return None
def get_tile_from_content_id(id):
try:
return registered_tiles[content_ids[id]]
except:
return None
class Tile:
texture = "none.png"
solid = True
rotation = 0
def __init__(self, name):
self.name = name
for key in registered_tiles[name]:
self.__dict__[key] = registered_tiles[name][key]
def get(self, key):
try:
return getattr(self, key)
except:
return None
def set_rotation(self, rot):
self.rotation = rot
def get_rotation(self, rot):
return self.rotation
def is_solid(self):
return self.get("solid") == True

View File

@ -14,8 +14,13 @@ class Vector:
def __repr__(self):
return "Vector {{x: {0}, y: {1}}}".format(self.x, self.y)
def apply(self, func):
return Vector(func(self.x), func(self.y))
def __eq__(self, b):
vec = vec_or_num(b)
return self.x == vec.x and self.y == vec.y
def __ne__(self, b):
vec = vec_or_num(b)
return self.x != vec.x or self.y != vec.y
def __add__(self, b):
vec = vec_or_num(b)
@ -41,3 +46,6 @@ class Vector:
def __ceil__(self):
return Vector(ceil(self.x), ceil(self.y))
def apply(self, func):
return Vector(func(self.x), func(self.y))