Open Spritekin opened 4 years ago
Hi there, you said there is another Dungeon generation library you will be using, can you share a link for that one so I can check it and tell you if that one is better or the one I have?. Also, I think instead of a FPS camera you should use a top-down camera, its as simple as adding one line of code in Ursina, two if you want to set the FOV to something different from the default value.
camera.orthographic = True
camera.fov = 20
@ZeroCool940711 Don't worry, I went and tested a few, I ended with the one you suggested. I have already built a small map and saved it into a file.
import dungeonGenerator
# define map size (x,y) max tiles to use in direction
levelSize = [64,64]
# create a generator
print('Generating map...')
dgen = dungeonGenerator.dungeonGenerator(levelSize[0], levelSize[1])
dgen.placeRandomRooms(7, 9, 2, 1, 1000) # Generate rooms
dgen.generateCorridors('m') # Build corridors
dgen.connectAllRooms(10) # connect rooma to paths via door
dgen.mergeUnconnectedAreas() # connect unconnected areas
dgen.pruneDeadends(10) # Remove corridors to nowhere
dgen.placeWalls() # Make the borders into walls
print('Map generated...')
# Define some characters we want to use to represent the map
TEMPTY = ' '
TFLOOR = '.'
TCORRIDOR = '='
TDOOR = '&'
TDEADEND = '^'
TWALL = '#'
TOBSTACLE = '%'
TCAVE = '@'
TWATER = '~'
TROAD = '*'
print('Saving the map...')
f = open('dungeon.map', 'w')
for y in range(dgen.height):
for x in range(dgen.width):
if dgen.grid[x][y] == dungeonGenerator.EMPTY: print(TEMPTY, end = ''); f.write(TEMPTY)
if dgen.grid[x][y] == dungeonGenerator.FLOOR: print(TFLOOR, end = ''); f.write(TFLOOR)
if dgen.grid[x][y] == dungeonGenerator.CORRIDOR: print(TCORRIDOR, end = ''); f.write(TCORRIDOR)
if dgen.grid[x][y] == dungeonGenerator.DOOR: print(TDOOR, end = ''); f.write(TDOOR)
if dgen.grid[x][y] == dungeonGenerator.DEADEND: print(TDEADEND, end = ''); f.write(TDEADEND)
if dgen.grid[x][y] == dungeonGenerator.WALL: print(TWALL, end = ''); f.write(TWALL)
if dgen.grid[x][y] == dungeonGenerator.OBSTACLE: print(TOBSTACLE, end = ''); f.write(TOBSTACLE)
if dgen.grid[x][y] == dungeonGenerator.CAVE: print(TCAVE, end = ''); f.write(TCAVE)
if dgen.grid[x][y] == dungeonGenerator.WATER: print(TWATER, end = ''); f.write(TWATER)
if dgen.grid[x][y] == dungeonGenerator.ROAD: print(TROAD, end = ''); f.write(ROAD)
print(''); f.write('\n')
f.close()
print("Map written to file")
@Spritekin I see, that one I suggested is pretty good, the best way to split the generation is in 2 parts, first the Structure generation which is the rooms, corridors, walls, floor,etc and the second part is the Overlays which are the stuff that you put over the structure like chests, monsters, entrances, exits, etc. As you can probably see if you checked the Demo.py
and the dungeonGenerator.py
you can add custom tiles and custom overlays to make almost anything, you can also use some of the functions inside the dungeonGenerator to find close tiles to the want where you want to place things so, in theory you could create Pack Based
overlays, that means you can create a pack of monsters and place it inside a room or you can make sure there is only one chest in the room like for example if its a boss chest, its pretty customizable, in the repo you can find a lot of tilesets too if you want to test.
Rewarding the example you wrote above on the room generation part I recommend using decreasing the number of attempts to 500 instead of 1000, the more attempts the slower it's going to be and even with 500 that's more than enough to ensure there are a lot of rooms. also increase the margin between rooms to at least 2 or 3 so there is always enough space to place corridors or something, what that does is that it create empty tiles between rooms, that should avoid weird room generation, or you could also reduce it to put more rooms together if you want to create continuous rooms, something like this should be ok.
d.placeRandomRooms(5, 9, 1, 3, 500)
Here is the documentation of what thats doing in case you want to read it but its also inside the code as a docstring.
randomly places quads in the grid takes a brute force approach: randomly a generate quad in a random place -> check if fits -> reject if not Populates self.rooms
Args:
minRoomSize: integer, smallest size of the quad
maxRoomSize: integer, largest the quad can be
roomStep: integer, the amount the room size can grow by, so to get rooms of odd or even numbered sizes set roomSize to 2 and the minSize to odd/even number accordingly
margin: integer, space in grid cells the room needs to be away from other tiles
attempts: the amount of tries to place rooms, larger values will give denser room placements, but slower generation times
Returns:
none
Here are two images showing a result created with the Demo.py
so there is some example of what it can be done with this Dungeon Generator, they show two dungeons with rooms and caves where caves are open areas, this could be used to create complex maps where there are indoor and outdoors areas or just open areas like a huge cave. There are also 5 types of overlays used, OVERLAY_ARTIFACT, OVERLAY_TREASURE, OVERLAY_TRAP, OVERLAY_FOE, this should simulate some content a normal dungeon in most games has. The first image was a dungeon with a size of 100x100 it took 0.3 seconds to generate, the second one is a huge dungeon that probably is not common to use, it can be used to create a contry size map or you could use it to procedurally generate a map like Minecraft, if you control where the generation starts and ends you can create a continuous grid, the size for the second one was 5000x5000 and took 20 seconds to generate, at least in my machine.
When checking the images make sure you zoom in, normally you would only see the basic structure from far but if you zoom in you can see the overlays and also more details on the structure.
@ZeroCool940711 I guess the idea is to demo how to build the maps only. If we try to build a big map we will have to go into advanced rendering technniques like BSP trees or level chunking... lets keep this simple for now.
Same for overlays, I understand what you mean but let me finish this first part, then I can make it more advanced.
Regards
@Spritekin I understand, sorry, I just wanted to let you know what can be done with it, of course in order to build big maps its necessary to use something like chunking and also use LOD and occlusion to optimize the map or otherwise will be hard to render such a huge map.
Another thing, when i tried using Ursina to render the dungeon created by the dungeon generator I noticed that when creating even a single Entity it made everything run really slow, I think the problem is probably related to the way Ursina load models and textures, seems like instead of having to specify a path to the model or texture it instead loads everything in memory or at least walks every directory inside the folder where the script is and when you have some tilesets there like the folder in the repository for the dungeon generator it takes forever to run the script, I found myself having to wait up to 10 minutes before the script even finished loading, if im right then I think something needs to be implemented in Ursina to handle better those cases, otherwise using Ursina for real game development will be impossible as a real game will have a lot of textures and models that will be loaded when the game starts.
Well I'm building in a single folder with a few textures and all goes fine. I'm using cubes to build the map and I'm rendering the 64x64 map and it still runs at 60FPS. I'm using a MacbookPro 15 inch so I don't think its the video card giving me an edge at all.
If it truly is a problem with the resource loader then yes it should be fixed.
Other things to fix are light management, UI standardisation, file formats, shaders... but hey it is version 0.1.
Even if it is used for learning, it is good.
Im not saying its not good, im just giving some feedback on what can be improved and a potential problem for those who might want to use Ursina for real stuff, there are not many Python game engine, at least not good ones so if Ursina becomes one of the good ones it would be nice. I have another Python game engine im using for serious stuff and its awesome but im stuck with something so if I can switch to use ursina until I can figure out how to do what I need with that other engine I will do it, if you are interested or you know someone who want to give me a hand with the game engine and the game im making just let me know, im currently looking for people with knowledge in Python but also need people with C++ knowledge as the game engine is mainly written in C++ and Python is just used for scripting, it has some nice features but its old so, not everyone might like it xD.
Entities are not really meant for big levels like that, but for a handful of dynamic intractable things. For something like this you should take look at the Mesh class.
# read from a texture and place tiles based on the color
quad = load_model('quad')
dungeon = Entity(model=Mesh(), texture='brick')
model = dungeon.model
texture = load_texture('heightmap_1')
for y in range(texture.height):
for x in range(texture.width):
col = texture.get_pixel(x,y)
# print(col)
if col.v > .5: #
model.vertices.extend([Vec3(x,y,0)+v for v in quad.vertices]) # add quad vertices, but offset.
model.colors.extend((color.random_color(),) * len(quad.vertices)) # add vertex colors.
'''
the uvs are the same for all the squares in this case, but you could also have
them change based on the tile type so you can use a tilemap
'''
model.uvs = (quad.uvs) * (texture.width * texture.height)
model.generate() # call to create the mesh
This way I get ~400 fps even with 131 072 squares
Generating a vary big level can still take a few seconds though, so you might want chunk it into parts and make them as needed.
@pokepetter Nice trick with the mesh... maybe in a more advanced version as an optimisation.
@Spritekin I can see you made it work but why are your rooms so close to each other and the corridors so small? Is that intentional or you cant create long corridors? Also, I have a question, how can you make it so things like walls and doors block the player or have collision but not the floors and other tiles?
Also I think you could rotate the wall tiles that are vertically so they look better.
@ZeroCool940711 If you are building a classic roguelike game, I don't think you need to worry about that, the logic that controls colissions is done on the map level not the scene level.
In other words if you are moving in one direction and your map says there is a wall, you can't move there. You don;t test on the quad that is on the scene.
If you are doing 3D (which I plan to do next) then collision is managed by the floor and the walls which are built on top of the floor, in that case yes you need collisions but mostly for some effects.
@ZeroCool940711 I just generated a dungeon with some parameters, adjust it anyway you prefer. Here is the code that builds the map that was generated with the generator.py I sent before.
from ursina import * # this will import everything we need from ursina with just one line.
# Define some characters we want to use to represent the map
TEMPTY = ' '
TFLOOR = '.'
TCORRIDOR = '='
TDOOR = '&'
TDEADEND = '^'
TWALL = '#'
TOBSTACLE = '%'
TCAVE = '@'
TWATER = '~'
TROAD = '*'
CAMERA_SPEED = 10
def update():
if held_keys['w']: # If q is pressed
camera.position += (0, time.dt * CAMERA_SPEED, 0) # move camera up
if held_keys['s']: # If a is pressed
camera.position -= (0, time.dt * CAMERA_SPEED, 0) # move camera down
if held_keys['d']: # If d is pressed
camera.position += (time.dt * CAMERA_SPEED, 0, 0) # move camera right
if held_keys['a']: # If a is pressed
camera.position -= (time.dt * CAMERA_SPEED, 0, 0) # move camera left
if held_keys['q']: # If d is pressed
camera.position += (0, 0, time.dt * CAMERA_SPEED) # move camera right
if held_keys['z']: # If a is pressed
camera.position -= (0, 0, time.dt * CAMERA_SPEED) # move camera left
app = Ursina()
window.title = 'Ursina Dungeons' # The window title
window.borderless = False # Show a border
window.fullscreen = False # Go Fullscreen
window.exit_button.visible = False # Show the in-game red X that loses the window
window.fps_counter.enabled = True # Show the FPS (Frames per second) counter
# Open your dungeon map for read
map=[]
with open('dungeon.map') as f:
for line in f: # For each line
map.append(list(line[:-1])) # append the line to the map but break it in individual characters first
for py, line in enumerate(map):
for px, tile in enumerate(line):
if tile == TEMPTY:
continue
if tile == TFLOOR:
Entity(model='quad', color=color.white, texture="floor", collider="box", position=(px, py, 0))
if tile == TCORRIDOR:
Entity(model='quad', color=color.white, texture="corridor", collider="box", position=(px, py, 0))
if tile == TDOOR:
Entity(model='quad', color=color.white, texture="door", collider="box", position=(px, py, 0))
if tile == TDEADEND:
Entity(model='quad', color=color.white, texture="deadend", collider="box", position=(px, py, 0))
if tile == TWALL:
Entity(model='quad', color=color.white, texture="wall", collider="box", position=(px, py, 0))
if tile == TOBSTACLE:
Entity(model='quad', color=color.white, texture="obstacle", collider="box", position=(px, py, 0))
if tile == TCAVE:
Entity(model='quad', color=color.white, texture="cave", collider="box", position=(px, py, 0))
if tile == TWATER:
Entity(model='quad', color=color.white, texture="water", collider="box", position=(px, py, 0))
if tile == TROAD:
Entity(model='quad', color=color.white, texture="road", collider="box", position=(px, py, 0))
app.run() # opens a window and starts the game.
Ok guys... got the final program and the tutorial on how to get there from scratch.
from ursina import * # this will import everything we need from ursina with just one line.
# Define some characters we want to use to represent the map
TEMPTY = ' '
TFLOOR = '.'
TCORRIDOR = '='
TDOOR = '&'
TDEADEND = '^'
TWALL = '#'
TOBSTACLE = '%'
TCAVE = '@'
TWATER = '~'
TROAD = '*'
TENTRY = 'E'
CAMERA_SPEED = 10
SCALE = 2
texoffset = 0.0 # define a variable that will keep the texture offset
def update():
global texoffset # Inform we are going to use the variable defined outside
texoffset += time.dt * 0.05 # Add a small number to this variable
for tile in water:
setattr(tile, "texture_offset", (texoffset, texoffset)) # Assign as a texture offset
# Move the camera to a new position but check if it is valid before moving
def moveto(newposition):
mapposition = newposition / SCALE # get the map coords from that position
if tilemap[int(mapposition[2])][int(mapposition[0])] != TWALL: # if the new position doesn't have a wall
camera.position = newposition
else:
print("There is a wall in that direction")
def input(key):
if key == 'i': moveto(camera.position + camera.forward * SCALE) # where will I move
if key == 'k': moveto(camera.position + camera.back * SCALE)
if key == 'j': moveto(camera.position + camera.left * SCALE)
if key == 'l': moveto(camera.position + camera.right * SCALE)
if key == 'u': camera.rotation_y -= 90 # rotate camera left
if key == 'o': camera.rotation_y += 90 # rotate camera righa
app = Ursina()
window.title = 'Ursina Dungeons' # The window title
window.borderless = False # Show a border
window.fullscreen = False # Go Fullscreen
window.exit_button.visible = False # Show the in-game red X that loses the window
window.fps_counter.enabled = True # Show the FPS (Frames per second) counter
# Open your dungeon map for read
tilemap=[]
with open('dungeon.map') as f:
for line in f: # For each line
tilemap.append(list(line[:-1])) # append the line to the map but break it in individual characters first
water = []
for py, line in enumerate(tilemap):
for px, tile in enumerate(line):
if tile == TEMPTY:
continue
if tile == TFLOOR or tile == TENTRY:
if tilemap[px+1][py] == TWATER or tilemap[px-1][py] == TWATER or tilemap[px][py+1] == TWATER or tilemap[px][py-1] == TWATER:
Entity(model='cube', color=color.white, texture="floor", collider="box", position=(px * SCALE, -0.5 * SCALE, py * SCALE), scale=(SCALE,SCALE,SCALE))
else:
Entity(model='plane', color=color.white, texture="floor", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TENTRY: camera.position = (px*SCALE, 1, py*SCALE)
if tile == TCORRIDOR:
Entity(model='plane', color=color.white, texture="corridor", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TDOOR:
Entity(model='plane', color=color.white, texture="floor", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
Entity(model='cube', color=color.white, texture="door", collider="box", position=(px * SCALE, 0.5 * SCALE, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TDEADEND:
Entity(model='plane', color=color.white, texture="deadend", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TWALL:
Entity(model='plane', color=color.white, texture="floor", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
Entity(model='cube', color=color.white, texture="wall", collider="box", position=(px * SCALE, 0.5 * SCALE, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TOBSTACLE:
Entity(model='plane', color=color.white, texture="obstacle", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TCAVE:
Entity(model='plane', color=color.white, texture="cave", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
if tile == TWATER:
Entity(model='plane', color=color.white, texture="floor", collider="box", position=(px * SCALE, -1 * SCALE, py * SCALE), scale=(SCALE,SCALE,SCALE))
water.append(Entity(model='plane', color=color.rgba(256,256,256,128), texture="water", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE)))
if tile == TROAD:
Entity(model='plane', color=color.white, texture="road", collider="box", position=(px * SCALE, 0, py * SCALE), scale=(SCALE,SCALE,SCALE))
camera.fov = 110
camera.lens.setNearFar(0.6, 1000)
app.run() # opens a window and starts the game.
[Ursina Dungeons for Dummies.txt](https://github.com/pokepetter/ursina/files/4496622/Ursina.Dungeons.for.Dummies.txt)
Notice the tutorial takes the reader rendering on 2D first, then on 3D. This program is the final part only. Also there are no textures for walls, doors, water, etc because I want the user to search his textures.
Final view with camera properly setup and transparent water with simple animation.
Any way to get the source for this? It looks pretty cool.
I think I found what I need at https://github.com/pokepetter/ursina/files/4496622/Ursina.Dungeons.for.Dummies.txt
This thread is to discuss the dungeon generator and see its progress. When finished the player should be able to walk around a dungeon as a FPS. We can later improve the same example to provide a bit of flavour like in the old Eye Of The Beholder or Dungeon Master rogue like games, or fly a top camera in Diablo style.