BenPyton / ProceduralDungeon

This is an Unreal Engine 4/5 plugin to generate procedural dungeon.
MIT License
508 stars 61 forks source link

[BUG/Question] Choose Door called after IsValidDungeon #57

Open VGO08 opened 3 weeks ago

VGO08 commented 3 weeks ago

Hello and thanks for all the project first, I really appreciate your work and it helped me a lot for my project, but i have on quick question : Why is choose door called after IsValidDungeon? I want to know how many unused door are spawned to know if its even or odd. (Im trying to create a lethal company style procedural generation and also if you dont mind giving me some tips on how to reduce the dead ends because on some maps a lot of time i get really a lot of dead ends, what i aim for is more like 2-3 big zones but thats isnt my real question)

Technical informations

Expected behavior I expected that Choose Door will run before IsValidDungeon

I really appreciate your help and i hope i will find a solution :)

BenPyton commented 3 weeks ago

Hi @VGO08,

There are 2 main reasons why it is called after the dungeon has finished its dungeon creation:

However, I'm thinking of reworking the doors in a future (major?) version. I think of adding a door data type to hold instance related data of the doors (for example the lock state, and the actor class) so we could set them during the Initialize Dungeon event, and the Choose Door call could be moved before the Is Valid Dungeon, so we can validate those data in the Is Valid Dungeon too.

I will not work soon on this feature, it could take months (or even a year if for the next major version) before I'm working on it. Nevertheless, you are not without solution. What do you mean exactly by "unused" doors? If you mean unconnected doors (room only on one side), then there is a solution. During the Is Valid Dungeon (I recommend creating a function to encapsulate this logic, though), you can do that:

Here a screenshot:

image

VGO08 commented 3 weeks ago

Thank you so much i really appreciate the help, just after writing i had almost the same code but with slight difference that bugged it so really thank you ! But like now i tryied different things ect with the generator and i really think it goes a bit everywhere and with a lots of dead ends and some times maps are actually incredible but most of the time its a bit more one or two paths and some rooms right and left, i try both gen methods and both are good but do you have any tips on how i can maybe enhance that a make it more connected between them, I dont want to take too much of your time or annoy you, Have a nice day :)

BenPyton commented 3 weeks ago

Well, it really depends on how you want your dungeon layout to be.

Before writing code in the Choose Next Room function, I would strongly recommend to design your dungeon layout on paper.

Someone linked me this blog post explaining how dungeons are generated in Enter the Gungeon. My plugin does not work the same way, so you can't implement the same logic as they did. However, I would suggest you to create a graph of your dungeon layout like you can see in the blog post (keep in mind that my algorithm generates one room after another, unlike in Enter the Gungeon).

After that, you should define your Room Unit appropriately: the smaller the Room Unit is, the less likely your doors will be able to connect (outside the doors that connect previous and next rooms).

So, keep something rather large to increase the chances of creating loops. For example, in The Binding of Isaac, or in th old 2D Zelda games, one room unit is the size of the screen, so most rooms are of the same size, making it easier to arrange rooms without gaps between them and connect doors to make loops.

Another tips is to use breadth first generation (BFS) because the generator call Choose Next Room on all the doors of a room before going to the next room, resulting in a more large dungeon than long, and increasing the chances of creating loops. While the Depth First generation (DFS) will call the function directly on the next room returned, resulting in a more linear dungeon and creating lots of dead-ends.

Finally, you should keep in mind that you don't need to fill the entire room volume. Only the doors locations are important. You should choose the Room Unit depending on how fine grained you want to place your doors: the more precision in door placement (small Room Unit) the less likely the doors will be aligned properly to create loops.

VGO08 commented 3 weeks ago

Ive tryied all of these just didnt really understand room unit Here are the generations from my game and what i aim a bit for : HRG - Unreal Editor 17_08_2024 12_57_01 HRG - Unreal Editor 17_08_2024 12_57_28 HRG - Unreal Editor 17_08_2024 12_57_59 HRG - Unreal Editor 17_08_2024 12_58_15 And thats what i want to approximately achieve: Lethal Company 17_08_2024 12_50_28 Lethal Company 17_08_2024 12_50_43 Lethal Company 17_08_2024 12_50_53 Lethal Company 17_08_2024 12_51_10

Maybe with what ive actually done is ok but if you look closely with the debut lots of parts arent connected even i sometimes it make pretty things, maybe its just me stressing a bit about all and i need to mess with the weight probability of the rooms but i think its better to get an expert advise, thank you for helping me and have a great day !

BenPyton commented 3 weeks ago

Okay, it's better now with screenshots to know where you are.

So, if I understand properly your issue, your biggest concern is about those final corridors (pointed in orange) and you want more specialized dead-end rooms (pointed in green)?

image

If so, then I think your generation rules are too simple (from what I can see in your variables of your dungeon generator on your screenshots).

You ave many ways to get better results, I'll describe below some of them (I strongly recommend to mix them).

Split your room array into categories

You should make multiple arrays of room, for example a gameplay room array (storing the important rooms in your dungeon) and a corridor room array. You will have better control on what is generated when. For example, you can alternate, one corridor and one gameplay room. You can also choose randomly the array before choosing a room inside it, adding weights to their probabilities (e.g. 70% proba for a gameplay room and 20% proba for a corridor).

Create multiple array of corridors

You can also create "sub-categories" arrays of rooms that you can combine before selecting randomly a room from them. For example, you can create one corridor array per number of exits (e.g. Corridors2 for corridors with 2 doors, Corridors3 for 3 doors, etc.). Then you can join the arrays depending on the current state of your generation process (by creating a temporary array that is filled with rooms of the arrays you want to join together). For example, you can use corridors with 3 or 4 doors at the beginning of the generation, then when you reached a specific number of room in your dungeon, you select corridors of 2 and 3 doors, and after another number of room reached, you select only corridors with 2 doors.

It's not mandatory to end the dungeon when reaching a number of room

In the wiki and example project, I'm using this way to end the generation, you can set any other condition to end the generation. You can also always return true in your Continue To Add Room function and let the generation ends naturally when there is no unconnected doors remaining. Be careful however in that case, you should make sure to either:

Stop using corridors at a certain point

You can define a condition that when met will prevent the use of corridors. For example, once you have placed all your mandatory rooms, you will just skip the placement of corridors and only place your specific gameplay rooms. It will feel less than a useless dead end for the players because even if they have to backtrack, they will have something to do at the dead end. This point can be combined with the previous point if using dead-end gameplay rooms with only 1 door: no more corridor will be placed and you can let the generation end when all the remaining corridor doors are filled.

VGO08 commented 3 weeks ago

Thank you, changing the generation to end naturally reduced walls spawned on unconnected doors from 25-30 walls to only 5-6 but for the rest it might change a lot in the quality of the generation but i was unable to do something i tried a random int with a bool to choose between random room with weigth of "Gameplay Rooms" and "Corridor Rooms" but each time i did it it gave me the Check IsValid function is valid? and because i cant do that i can do the sub array.

I didnt thinked about the stopping using corridor at certain point but do you mean when for example the Minimum nuber of room have been placed, there is a lot i need to learn and improve so could you give me hints and tips to when and how to achieve that, Thank you again for your help

BenPyton commented 3 weeks ago

What do you mean by "but it might change a lot in the quality of the generation"? Do you mean a change in good or bad quality, comparing of what you have currently?

could you give me hints and tips to when and how to achieve that

Well, I don't know what you exactly want and what you have coded currently...

First, if you didn't done that yet, write down on paper (or in a text editor) what dungeon layout you want to create (what rooms, how they place themselves according to other rooms, etc.).\ Draw a graph if possible of your dungeon layout on paper (or any drawing app, like this one).

[!NOTE] The screenshots of what you want in your previous message don't really help, because they're too dark and I don't understand the layout of the dungeons.

Once you have your description and/or a drawing of the graph you want to achieve, please share them here so I can help you coding your dungeon rules.

I'll share you here a screenshot of Choose Next Room I made for someone else on Discord, to show you how to make a branch based on a probability:

image

You can change the secret room array by your gameplay rooms, and normal roo array by your corridor rooms.

It also shows you how you can stop using room corridors based on a number of room and after you've placed specific rooms (the first branch).

Maybe this will help you setting it up.

VGO08 commented 3 weeks ago

Thats great, Okay so let me tell you what is my objective, before let me clarify i wanted to say change quality in a good way for sure and what i've coded is only like what you told me and the wiki/other guy issue (37 or something) nothing more, My game has different "maps" that are house, mall, shop, factory... for the horror-multiplayer genre, like a "real" design for mall and most of large places they have limited dead ends, sometimes a sort of maze-like and different paths design, there isnt a need to have a specific logic (like next to the bedroom a kitchen and two bathroom in a row ect) but really my main points are limited dead ends and a maze-like design, I don't also think there are any "special" rooms i want to add, could you clarify "how they place themselves according to other rooms" ? Also im really sorry of the poor image quality i've provided to you here are some better ones that i have found on google: lethalmap lethalmap1 so in the first map you can the real layout in game of what i wanted to show you, ignore the colors they are useless here Second image is an inspired design but works well with what i want, you can choose multiple paths that can go to a location or a same end location of another paths (also here you can ignore the legend i don't think it's useful),

BenPyton commented 3 weeks ago

Okay, I think I will be able to help you better now.

there isnt a need to have a specific logic (like next to the bedroom a kitchen and two bathroom in a row ect)

Well, there is always a need to have a specific logic. you can't just place a bunch of rooms randomly in a total chaos. It's not always logic about placing specific rooms at specific order, but more like "constraints" or "rules" so that the whole dungeon layout will looks like something real (like you said a house, a mall, a factory, etc.)

First, if I analyze your examples, you can see that you can draw a grid on them. This grid defines the Room Unit of the dungeon and will help the creation of loops.

Example of a Mall-like dungeon

Now, I will take the mall to create an example of a simple dungeon layout. This is to help you understand the process behind creating your code based on a desired result. I hope you will grasp the idea behing and you will be able creating any dungeon layout you want after that.

Basically, a mall is composed of 3 types of room:

Here a drawing of a layout we would want to generate (I keep it simple for the example):

mall-like_layout

[!NOTE] I have drawn only one floor of the dungeon to keep the drawing simple, but the open spaces are meant to have doors on multiple floors, so the generated dungeon will have multiple floors like a real mall.

We have our 3 types of rooms (blue for corridors, red for shops, green for open spaces). I've made 2 types of doors:

I've used only 4 rooms for that:

mall-like_rooms

[!NOTE] Those rooms are just the room definitions (size and doors), but several rooms can be created for each of them (for example different kind of shops), breaking the feeling of repetitiveness for players while keeping the dungeon layout simple.

Now, we will have to deduce the "constraints" or "rules" from that ideal layout:

Now, we can write the logic (in pseudo-code here) to tell which room will be generated after another. I'll use the terms currentRoom for the room already generated, and door for the door used to connect the next room to the currentRoom. I didn't created a "start" and an "end", so I decide to use an open space as first generated room, and stop adding corridorss after a number of room is reached (called minRoomCount in my pseudo-code).

if (door.type == shopDoor) and (currentRoom.type != shop) then
    return random shop from a list of shop room
else if (door.type == corridorDoor) and (rooms.count < minRoomCount) then
    randomNumber := get random number
    if (randomNumber < 10%) then 
        return random open space from a list of open space
    else
        randomNumber := get random number
        if (random number < 20%)
            return corridor 1x1
        else
            return corridor 3x1
        endif
    endif
endif

[!NOTE] This pseudo-code is a first draft and will need to be iterated on to get the best result. For example the percentage values can be tweaked, the conditions can be modified, new constraints can be added, etc.

Conclusion

We didn't wrote directly the code to generate the dungeon we want. Instead, we've got through several steps:

VGO08 commented 2 weeks ago

Hello again, I had some issues while trying to work on that, First I had a problem with the probabilities, i tryed what you put before the code to select between "secret rooms" and "normal rooms" but for me the image isnt here anymore but still i had reproduced it and it doesnt work, i only get the "IsValid" Error and when i remove checks there is 3-6rooms spawning, also i was wondering why we dont just use the function you created: "Get Random Data Weighted" that with one Map variable you can just, like you wrote in the Wiki, change values and it does change probabilities. I've had tryed it before opening this ""Issue"" and it worked well even if its not really precise for lots of different rooms. Secondly, the recent "pseudo code" so maybe if i understand you removed old screenshot and advice me to use a similar code, if i dont misunderstand it is in the "Choose Next Room" Function, but i dont really understand how it knows the door type, is it set during the "Choose Door" function ?

I really appreciate your help, really thank you :)

BenPyton commented 2 weeks ago

It's weird the image has disappeared... I'll update my old post with a copy of the image instead.

Please, tell me if my example of a mall-like generation in my previous post helped you understanding the workflow.

About your issue with probabilities

First I had a problem with the probabilities, i tryed what you put before the code to select between "secret rooms" and "normal rooms" but [...] it doesnt work, i only get the "IsValid" Error and when i remove checks there is 3-6rooms spawning,

Well, I would suggest you to post screenshots of your Choose Next Room Data, Continue to Add Room and Is Valid Dungeon so I can know exactly what you've done and where the error could be located. Without those screenshots, I can't help you more on your "IsValid" error...

About Get Random Data Weighted function

The use of this function depends mostly on the use case, and can be especially hard to use for "static" or "global" probabilities.

For example, if you want a secret room to have 10% chances of spawning, I call that a "global" probability, because the secret room always have 10% of proba, regardless of all other possible rooms. In that case, it is better to do like I said in my messages above.

However, if you need to have a room A to spawn 2 times more than a room B and three times more than a room C, you can use this function with values 6 for room A, 3 for room B and 2 for room C. This use case is made easy with this function.

So, you are right I could have used this function in my pseudo-code to replace this part:

randomNumber := get random number
if (random number < 20%)
    return corridor 1x1
else
    return corridor 3x1
endif

with that to get the same result (the total is 5 so 1/5 = 20% and 4/5 = 80%):

return weighted random room [corridor 1x1 = 1; corridor 3x1 = 4]

About the door type

The Choose Door is used only to know which door actor to spawn. It is not used during the generation process.

You have access to the door details in the Choose Next Room Data, it is provided as an input of the function:

image

The type is the Door Type data asset you can create and set it in the Room Data asset.

image

You can easily do comparisons of the door type in your Choose Next Room Data (you can also use door position or orientation too) If no door type is set (the default None), the Type in the Choose Next Room Data function will be null (this is the default door type).

VGO08 commented 2 weeks ago

image image image image

For the doors I now understand, I didnt saw that you could set the DoorType thats awesome, also i understand better for the probabilities and it is actually better the first one you have told me (the one you have that "global" probability), also the mall dungeon helped me a lot to understand how it works and the workflow for the code in ChooseNextRoom I'm actually trying to do the last code you wrote, it might be better than the screenshot that disapeared, Quick question about the screenshot, I dont understand how the Is Empty is used because I never add or remove Variables from the array (or the map that i use here so i can choose with probability which types and after in the map which room has more chance so a sort of combination) Please tell me if there anything else you need to know and I will share as soon as possible, Thanks for the help

BenPyton commented 2 weeks ago

Alright, I think I know what is wrong.

First, Your Continue to Add Room function has the opposite logic. You should return true when you want to continue the dungeon generation, and return false when you want to stop the generation.

image

Secondly, the screenshot that disappeared (and I re-added it to the original post) is a screenshot I've made for someone else. I've just re-posted it here to show you the correct way to check a probability. The other nodes are not really relevant for your case. In that screenshot, I've used the Is Empty because it was a requirement to generate one and only one room of each room data in list of secret room. The secret room array is copied to a temporary array in the Generation Init event, and each time a room from this list is added in the dungeon, it is removed from that temporary array (in the On Room Added event). This is why the temporary array is tested to be empty or not in the Choose Next Room, but in your case you can forget about this part.

So, this is the second part of what is causing your issues: since your Normal Rooms array is never empty, you will never generate the end room. And thus your Is Valid Dungeon will always return false.

image

With those modifications, your Ending Room will be placed as soon as you reach the Min Nb Rooms.

VGO08 commented 2 weeks ago

It is actually working right now thanks for that but before this when I tried the natural ending generation it made less unconnected doors but now I've got an average of 40 blocked doors but maybe i've got a dumb diversity of modules and making less and simpler like yours 1x1, 3x1 and 7x7 might affect it , also with my current ChooseNextRoom it doesnt affect if a door is a shop or a corridor door, is it ? Here I tryied doing it with the code that you mentionned before that checks if ShopDoor=CurrentDoor but here what ive got: image image I even changed in the map all the value to two and the "stairs" to one and even that is the result up here And here is the result with all set to 1: image We see that the doors match but why there is so much stairs and packed in a single tiny area because the other code made them spread and I had good results

BenPyton commented 2 weeks ago

You are on the right track.

You should pay more attention about the errors displayed on your screen: you are often returning a room RDT_LC3 that has no compatible door DTP_Door.

I think this is caused because your Choose Next Room function is not setup properly. I think you should start by renaming your variables to to describe better the intent. For example, rename your Normal Rooms array to Shop Rooms.

You are using your Normal Rooms and Corridor Rooms twice each. I think you must have different room arrays for each branches of your code. (Also be careful you are also comparing a room with a door type, so your first branch is never reached!!!)

image

About the room types

The room type feature is not built-in into the plugin, because it can be made differently depending on the project. Here are some ways of doing it:

About the fallback branch

The fallback branch is the last branch when all your other conditions redirected (the "false" path). In my pseudo-code, I forgot this fallback path, because no room should be placed. If you do that (returning nothing in the fallback) the plugin will throw an error saying the Choose Next Room returned null. You can consider to ignore those errors if you want. I think maybe I'll add the option in a future version to not throw this error, or throw only a warning, because it can be useful to not place a room some times.

VGO08 commented 2 weeks ago

So basically i do Room Data Blueprints for each "categories" so Shop Room, Corridors... and create childs of these classes? Creating new bunch of rooms from scratch isnt a problem because these were for testing and I didnt yet implemented the modeled rooms so no problem at all. And so basically solutions after the what I assume is creating parent classes are alternatives, if yes its great to hear because i didnt really understanded it well, i mean its more a headacke than the first one which is only maybe a bit longer, and so for checking the RoomType if it != basically if i do the first solution i will just need to put the Blueprint Shop class am I right, I really appreciate the help !

BenPyton commented 1 week ago

Yes, they are alternatives to differentiate rooms by their "type". I'll explain them in details.


1. Child Room Data Classes

The first point I've mentioned is to create child blueprint classes of the Room Data class. In all cases, I would always recommend to create a common blueprint class deriving from the Room Data class of the plugin, so you can add whatever project-specific variables in it shared by all your rooms. Then, you can create child classes of this common class for each type of room in your dungeon. For example "Shop" and "Corridor". If you want a hierarchical type system (meaning having "groups" of types, sub-types, etc.) you can create child classes of those types. For example, you could have "Large Corridor" and "Small Corridor" deriving from the "Corridor" type.

Using child classes for types allows you to define variables specific for those types. For example, your "Shop" rooms could have a variable to tell what type of item the sell (clothes, food, electronic, etc.). The biggest downside with this approach is that if you want to change the type (class) of a room data asset, you must re-create it with the new class, copy/paste common data from the old class and don't forget to change the data reference in the room level. (You can keep the room level, you just have to change the data asset reference in it).

This is what the class diagram (the hierarchy of the classes) should looks like:

image

The dotted classes is to show the optional sub-types. Here how it looks like in your editor (blue are the child classes, pink are the asset instances of these classes) :

image

If you want to check what type of room you have in your Choose Next Room, you must cast the room data into the class you want. You can then also access to the variables specific to this blueprint class if you want.

image


2. Variable in your Common Room Data Class

If you don't need specific variables in each of your room types, you can use a variable to differentiate your rooms by a "type". This way, if you want to change the type of a room, you just have to change this variable value. The biggest downside is that you can't have variables specific to the room types as all the room data assets will share the same blueprint class.

You can use whatever variable type you want:


3. Arrays of Room Data

This is maybe the less "clean" way of doing a room type, but can be useful in specific cases. You can create a new Data Asset class that store arrays of Room Data for each type of room you want. For example, you can have an array Corridor Rooms containing all the corridor room data, an array Shop Rooms with all the shop room data, etc. The main benefits of this way is that you can use Room Data class directly for you room data assets, without having to create at least a common child class to store a type variable, and also this allows you to switch the type data asset in your dungeon generator so your rooms can have different types depending on the context. The biggest downside is that you have to maintain those type data assets whenever you create or delete room data assets. It can also be less performant than the other when you have a lot of room data in your arrays, but this can be fixed easily by using sets instead of arrays. (sets are unordered as opposed to arrays, but are much faster to add/remove or check existence of an item in it with large amount of items)

image

VGO08 commented 1 week ago

So I did Child class but i'm not sure that casting works because like you did with the rooms drawings some places can have corridors and shop doors at the same time so the cast will always fail because it starting with a corridor room, and here is what it does even tho i used same rooms like before i did the common, shop and corridor class : image So obviously returned null is because in ChooseNextRoom there is a branch that leads to nothing but thats okay, but i really dont understand why it keeps just using the 1x1 corridor room, i have 2 shop rooms and 7 corridors room but only uses that, on that image i used that : image BP_RDT_Shop is the Child of Common RoomData and you can see it does spawn sometimes a bit room on the sides and also i needed to force check the IsValid because else it doesnt work and could generate the rooms and conditions i wanted, also I want to add that in the game i want to do multiple maps like mall, house, playground... but just saying that for your information i think i know how to implement it just because i think in some cases it could change a bit the structure,

BenPyton commented 1 week ago

You must use the cast to check if the data asset is from a certain class (you can convert the node to a pure cast if you want to link it to your AND node).

Also, it seems you didn't linked the Exec pin of your branch node on your screenshot.

image image

VGO08 commented 1 week ago

It was a copy of the code thats why it wasent linked and casting to the shop room didnt changed anything, really the issue here is the layout as you can see in the previous post and it doesnt generate enough room so i got each time a IsValid Error

BenPyton commented 1 week ago

So your issue now is that it generates too much corridors and not enough shops? If that's so, pleas show me the code where you choose which corridor to place (if you use the Get Random Room Weighted node, please show the map variable your passing to it). The fact it generates as many small corridors (with no shop doors on it) will prevent shop rooms to be generated. Maybe you inverted the probabilities between the small corridor and large corridor?

VGO08 commented 1 week ago

image image image image the two image are just two different result but same code With that: image But without that is valid here what it is : image image Basically the layout is just using the 1x1 corridor and no other corridors and making L or T shapes layout and sometimes some shops on the sides

BenPyton commented 1 week ago

Okay, I think your main issue here is your Normal Room Threshold value that should be tweaked.

From what I understand, you have separated your corridors with no shop doors from the one with shop doors, right? And this Normal Room Threshold value select between the 2 arrays? If that's so, your threshold should be tweaked so that more corridors with shop doors are generated than the corridors with no shop doors.

About your Is Valid Dungeon, it always returns false because now you don't generated your Ending Room at all in Choose Next Room. So the bottom part of your AND node in Is Valid Dungeon is always false.

Also, you've changed to a cast but now it missing a NOT node to get the proper condition like before.

image

VGO08 commented 1 week ago

I changed the code and its now way better and got rid of the IsValid Error but still there is way too much 1x1 corridors an some arent used image image like the generated map is like a long long corridor or some L or T shaped there is no interesting generation (i've got X shaped, T shaped, 3x1, 1x1 and stairs corridors so i dont know why it uses them less often) and for the error of the room i've fixed it i accidently put it in the list but its a special first room so just removed it

Edit : I have maybe found why it doesnt work i'll update the message to confirm if i found the issue or not

Edit : So basically i found that the last one (Normal NoDTP Door Var) was empty but i tryed replacing it with just the corridors Var and it made a really good layout that were interconnected and lots of paths so thats a good result, just for the question when i followed your pseudo-code it was without Normal Doors but all the corridors has at least 1 and its working so is there any specific reason ? Thank you for the not bool and teaking a bit the threshold helped me find that problem 💯

BenPyton commented 1 week ago

Glad to hear you found the cause of your issue!

when i followed your pseudo-code it was without Normal Doors but all the corridors has at least 1 and its working so is there any specific reason ?

I don't really understand your question... If you mean the pseudo-code does not test for a normal door, this is normal, because I've used only 2 door types, so when the Shop Door test is false, it is necessarily a Normal Door. So to explain better the logic:

EDIT: I've re-read my pseudo-code, and in fact there is no Normal Door, but Corridor Door that acts like it. And it is in fact tested.