BenPyton / ProceduralDungeon

This is an Unreal Engine 4/5 plugin to generate procedural dungeon.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Is there a way to make sure that two rooms connects only by specific doors?

deividasflaved opened this issue · comments

I want to have various size doors for my normal rooms, but I can't seem to be able to make sure that rooms would connect only via the same doors. Is there any workaround?

Without almost none C++ expierence I tried to do some tweaks to plugin and seems like I succeeded achieving something what will suit my basic needs.

Hi @deividasflaved,

Currently there's no way to set different types of doors for the generation process.

I know why it could be interesting to do that, and maybe I'll implement this feature in a future release.

However, you can spawn different doors in the ChooseDoor function, so depending on the rooms connected to it.

I hope this answers your question.
I'll leave this issue open to keep it in mind.

Best regards.

Without almost none C++ expierence I tried to do some tweaks to plugin and seems like I succeeded achieving something what will suit my basic needs.

Great!

If you want you can share here your code snippets of your changes, so I could tell you if it is good or give you some advices to improve your code.

In all cases, feel free to modify the plugin's code as you like to meet your needs, it is made for that! ;)

Best regards.

First thing I did was add new FIntVector to FDoorDef struct:

USTRUCT()
struct FDoorDef
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, Category = "DoorDef")
	FIntVector Position;
	UPROPERTY(EditAnywhere, Category = "DoorDef")
	EDoorDirection Direction;
	UPROPERTY(EditAnywhere, Category = "DoorDef")
	FIntVector Size;
};

Then I added this check in DungeonGenerator.cpp at AddNewRooms method:

newRoom = NewObject<URoom>();
			newRoom->Init(def, this, RoomList.Num());
			int doorIndex = def->RandomDoor ? Random.RandRange(0, newRoom->GetRoomData()->GetNbDoor() - 1) : 0;
                        //check if door size matches
			if (newRoom->GetRoomData()->Doors[doorIndex].Size != ParentRoom.GetRoomData()->Doors[i].Size) {
				newRoom = nullptr;
				continue;
			}

			// Place the room at its world position with the correct rotation
			EDoorDirection parentDoorDir = ParentRoom.GetDoorWorldOrientation(i);
			FIntVector newRoomPos = ParentRoom.GetDoorWorldPosition(i) + URoom::GetDirection(parentDoorDir);
			newRoom->SetPositionAndRotationFromDoor(doorIndex, newRoomPos, URoom::Opposite(parentDoorDir));

Next I edited draw debug for door. Added calculation to DoorSize

void ADoor::DrawDebug(UWorld* World, FIntVector DoorCell, EDoorDirection DoorRot, FTransform Transform, bool includeOffset, bool isConnected, FIntVector doorSize)
{
	if (URoom::DrawDebug())
	{
		FVector DoorSize = URoom::DoorSize() * FVector(doorSize.X, doorSize.Y, doorSize.Z);
		FQuat DoorRotation = Transform.GetRotation() * URoom::GetRotation(DoorRot == EDoorDirection::NbDirection ? EDoorDirection::North : DoorRot);
		FVector DoorPosition = Transform.TransformPosition(URoom::GetRealDoorPosition(DoorCell, DoorRot, includeOffset) + FVector(0, 0, DoorSize.Z * 0.5f));

		// Door frame
		DrawDebugBox(World, DoorPosition, DoorSize * 0.5f, DoorRotation, FColor::Blue);

		if (isConnected)
		{
			// Arrow (there is a room on the other side OR in the editor preview)
			DrawDebugDirectionalArrow(World, DoorPosition, DoorPosition + DoorRotation * FVector(300, 0, 0), 300, FColor::Blue);
		}
		else
		{
			// Cross (there is no room on the other side)
			FVector HalfSize = DoorRotation * FVector(0, DoorSize.Y, DoorSize.Z) * 0.5f;
			FVector HalfSizeConjugate = DoorRotation * FVector(0, DoorSize.Y, -DoorSize.Z) * 0.5f;
			DrawDebugLine(World, DoorPosition - HalfSize, DoorPosition + HalfSize, FColor::Blue);
			DrawDebugLine(World, DoorPosition - HalfSizeConjugate, DoorPosition + HalfSizeConjugate, FColor::Blue);
		}
	}
}

And lastly to know what size doors to use (might be troublesome if doors are same size, but for now it's fine, I am just using different sizes for different doors), I added size parameter to blueprint ChooseDoor. And I pass one of the two doors sizes since they should be matching, because I don't allow different door sizes in earlier checks.

// Return the door which will be spawned between Current Room and Next Room
	UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Door"))
	TSubclassOf<ADoor> ChooseDoor(URoomData* CurrentRoom, URoomData* NextRoom, FIntVector DoorSize);

Prolly there was a cleaner workaround or solution, but this seems to suit my needs.

Also one more thing. Dunno if I broke something or it is supposed to be like this, but box for room boundary in editor doesn't refresh until I reload Level, or until I change any value in LevelBlueprint. Seems like PostEditChangeProperty doesn't fire in RoomLevel if I change RoomSize in DataAsset.

Hi @deividasflaved,

Your code changes are pretty decent!

(might be troublesome if doors are same size, but for now it's fine, I am just using different sizes for different doors)

Relying only on the size for different types of door is not really generalized, but if it sufficient for your project it is okay. :)

I have some improvements to suggest you:

  • First, you just skip the room add for a door of ParentRoom if the ChooseNextRoomData doesn't return a room with the randomly chosen door of the same size. Then there will never be a room generated for this door, it is just skipped.
    To avoid that, you could add the door index in the arguments passed to the function, like this:
// in DungeonGenerator.h

UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Next Room"))
URoomData* ChooseNextRoomData(URoomData* CurrentRoom, int DoorIndex);

// in the AddNewRooms

URoomData* def = ChooseNextRoomData(ParentRoom.GetRoomData(), i);

Then, in the blueprint you can access the door's data from the data of CurrentRoom argument to choose an appropriate room here.
You can even test if the returned def contains a compatible door (and if not displaying an error)

  • Secondly, the algorithm after will get a random door index over all doors of the newRoom data. This can lead to choose a wrong door to connect over the compatible ones.
    Instead, you could make a list of all compatible door indices, and then get a random index in this list, so you are sure to get a compatible door to connect. (You could even use the list to do the check in the point above 😉 )

  • And lastly, you should also change the TryConnectToExistingDoors in the file Room.cpp to check the door compatibility. Thus if CanLoop is checked the doors won't mismatch neither.
    So I suggest you to create a static function in the struct FDoorDef in ProceduralDungeonTypes.h to check compatibility of 2 doors like this:

static bool AreCompatible(const FDoorDef& A, const FDoorDef& B)
{
    return A.Size == B.Size;
}

And then use it like this:

if (FDoorDef::AreCompatible(DoorA, DoorB))
{
    // ...
}

So, in final, I would suggest you this code inAddNewRooms to have a more robust generation:

URoomData* def = ChooseNextRoomData(ParentRoom.GetRoomData(), i); 
if (!IsValid(def)) 
{
    LogError("ChooseNextRoomData returned null."); 
    continue; 
} 

// *** here we create a list of the compatible door indices ***
TArray<int> compatDoorIndices;
for(int k = 0; k < def->GetNbDoor(); ++k)
{
    // here your compatibility check
    if (FDoorDef::AreCompatible(def->Doors[k], ParentRoom.GetRoomData()->Doors[i]))
        compatDoorIndices.Add(k);
}

// *** here we check if there is at least one compatible door ***
if (compatDoorIndices.Num() <= 0)
{
    LogError("ChooseNextRoomData returned a room with no compatible door");
    continue;
}

// Create room from roomdef and set connections with current room
newRoom = NewObject<URoom>();
newRoom->Init(def, this, RoomList.Num());

// *** here we change the random to use the door indices array instead.
int doorIndex = compatDoorIndices[(def->RandomDoor) ? Random.RandRange(0, compatDoorIndices.Num() - 1) : 0];

// Place the room at its world position with the correct rotation
EDoorDirection parentDoorDir = ParentRoom.GetDoorWorldOrientation(i);
FIntVector newRoomPos = ParentRoom.GetDoorWorldPosition(i) + URoom::GetDirection(parentDoorDir);
newRoom->SetPositionAndRotationFromDoor(doorIndex, newRoomPos, URoom::Opposite(parentDoorDir));

(I did not tested this code)

Seems like PostEditChangeProperty doesn't fire in RoomLevel if I change RoomSize in DataAsset.

Yes, this is normal since nothing changes in the RoomLevel blueprint (it is the RoomData that have a property changed and therefor calls its PostEditChangeProperty)

This issue comes from something else and I don't know where... The box draw is in the Tick of RoomLevel so it is updated each frame. Maybe you unchecked the tick boolean in the level blueprint?
On my side it is working great, when I change the room data, the box in the level editor changes instantaneously.

Did some tests and I think it works without any problems.

Ran into another problem, probably stupid one and easily solvable, but my lack of knowledge doesn't let me solve it.
I have door index in ChooseNextRoom, but I can't seem to find a way to access Doors list in blueprints. I can access it from Class defaults, but not in blueprint code.

Seems like I found a solution to my earlier comment. Had to add BlueprintReadOnly

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DoorDef")
	FIntVector Size = FIntVector(1, 1, 1);
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Doors")
	TArray<FDoorDef> Doors;

Also Debug box not refreshing solution was to add UpdateBounds() before DrawDebugBox

#if WITH_EDITOR
	// TODO: Place the debug draw in an editor module of the plugin
	if (URoom::DrawDebug())
	{
		FTransform RoomTransform = (Room != nullptr) ? Room->GetTransform() : FTransform::Identity;

		// Pivot
		DrawDebugSphere(GetWorld(), DungeonTransform.TransformPosition(RoomTransform.GetLocation()), 100.0f, 4, FColor::Magenta);
		UpdateBounds();
		// Room bounds
		DrawDebugBox(GetWorld(), DungeonTransform.TransformPosition(Bounds.Center), Bounds.Extent, DungeonTransform.GetRotation(), IsPlayerInside() ? FColor::Green : FColor::Red);

		// Doors
		FVector DoorSize = URoom::DoorSize();
		for (int i = 0; i < Data->GetNbDoor(); i++)
		{
			bool isConnected = Room == nullptr || Room->IsConnected(i);
			ADoor::DrawDebug(GetWorld(), Data->Doors[i].Position, Data->Doors[i].Direction, RoomTransform * DungeonTransform, true, isConnected, Data->Doors[i].Size);
		}
	}
#endif

Hi @deividasflaved ,

Did some tests and I think it works without any problems.

Good to hear!

Had to add BlueprintReadOnly

You did it right! I would suggest also to add BlueprintType in the USTRUCT() macro of FDoorDef in the file ProceduralDungeonTypes.h.
It is not mandatory to access it in blueprint, but it is better if you want to create variables of this type in blueprint.

Also Debug box not refreshing solution was to add UpdateBounds() before DrawDebugBox

Ah, right! You have the latest code on master branch. I didn't really tested it yet and I missed this bug... 😓

I have already setup projects to test my changes in runtime game, but I changed this part of code and didn't tested level editor 😅

Thank you for spotting this issue I'll add correction for it! 😉

Best.

I think I've got this plugin where I want it to be for now. Will be more than enough for my first game.

Needed procedural generation with premade rooms and this tool is amazing. Looking forward to future releases and possibly Your version of different sizes doors implementation.

Thank You for Your hard work and Thank You for help with my problem.

on the same vein as this post, i did some finagling on the code, I added a "door type" concept where you should have to define the door types in a table, maybe later add a custom size as well, so only matching types can be joined (a godsend for the art teams in the wild probably, I know the art team in my company already requested something like this)

the other change I added was a way to make a room preserve it's rotation when being placed (always appears in the same orientation), as for some cases it's desirable to have that kind of thing happen (mostly for isometric or fixed camera view where a load of work on the art department can be saved by cutting some corners or to prevent walls blocking camera view, etc)

// In ProceduralDungeonTypes.h:

// new struct to define infinite door types in a table... very basic for now, I only care about the row names in reality
USTRUCT(BlueprintType)
struct FDoorType : public FTableRowBase
{
	GENERATED_BODY()
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FName DoorType;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FText Description;
};

USTRUCT(BlueprintType)
struct FDoorDef
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, Category = "DoorDef")
	FIntVector Position{0};
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DoorDef")
	EDoorDirection Direction{EDoorDirection::North};
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DoorDef")
	FDataTableRowHandle Type;

// ...

	bool IsCompatible(const FDoorDef& Other) const
	{
		return Type==Other.Type;
	}

	bool PreservesRotation(const EDoorDirection& OtherDirection) const
	{
		return (Direction == EDoorDirection::North && OtherDirection == EDoorDirection::South)
			|| (Direction == EDoorDirection::West && OtherDirection == EDoorDirection::East)
			|| (Direction == EDoorDirection::East && OtherDirection == EDoorDirection::West)
			|| (Direction == EDoorDirection::South && OtherDirection == EDoorDirection::North);
	}

Some changes are needed in RoomData as well:

// in RoomData.h
public:
// ...
	UPROPERTY(EditAnywhere, Category = "Room")
	bool ShouldPreserveRotation{false};

The bulk of the changes are in the DungeonGenerator.cpp AddNewRooms function, I also added the change suggested by Ben to get the door index into the ChooseNextRoomData function

// in DungeonGenerator.cpp AddNewRooms function: about line 250
	// Create room from roomdef and set connections with current room
	newRoom = NewObject<URoom>();
	newRoom->Init(def, this, RoomList.Num());

	// new code
	const EDoorDirection parentDoorDir = ParentRoom.GetDoorWorldOrientation(i);
	TArray<int> validDoorIndexes;
	const FDoorDef parentDoor = ParentRoom.GetRoomData()->Doors[i];
	const TArray<FDoorDef> possibleNewDoors = newRoom->GetRoomData()->Doors;
	const bool shouldPreserveRot = newRoom->GetRoomData()->ShouldPreserveRotation;
	for(int j = 0; j < newRoom->GetRoomData()->GetNbDoor(); j++)
	{
		if (possibleNewDoors[j].IsCompatible(parentDoor) && (!shouldPreserveRot || possibleNewDoors[j].PreservesRotation(parentDoorDir)))
			validDoorIndexes.Add(j);
	}
	if(validDoorIndexes.IsEmpty())
	{
		LogError("ChooseNextRoomData return a room with no compatible door");
		continue;
	}
	const int newDoorIndex = def->RandomDoor ? validDoorIndexes[Random.RandRange(0, validDoorIndexes.Num() - 1)] : validDoorIndexes[0];
	// end new code

	// Place the room at its world position with the correct rotation
	const FIntVector newRoomPos = ParentRoom.GetDoorWorldPosition(i) + URoom::GetDirection(parentDoorDir);

I changed the indentation on the previous block for legibility here on the comments, the ChooseNextRoomData function change (receive the door index) is optional though, this change I'm proposing should work regardless if you want that or not.

Hi @ImBlackMagic

This is a really a great addition!
Maybe it could be easy to combine the size and type?

What if you add an FVector for the size in the FDoorType?
(the door size from the plugin settings will be removed)
Then we could access the size from table with something like:

FDoorType* TableRow = Type->GetRow<FDoorType>("Something meaningful here");
if (TableRow)
{
    //some code using TableRow->Size;
}

So, the size will not be specified for each door definition, but for each door type only.
We don't need anymore to check size compatibility, as the door type check will ensure the same door size too.
Then, the size will just be a visual hint for designers and artists, as it is right now.
Also we would be able to visualize it in the door actor blueprint viewport maybe.
I don't know if we can access data table rows like this in editor, but it is something to try!

I don't use much often data tables, so I don't know if this is a viable option.
However, I will try that when I have some times.


For the PreserveRotation you could maybe simplify it with:

bool PreservesRotation(const EDoorDirection& OtherDirection) const
{
    return Direction == URoom::Opposite(OtherDirection);
}

Also a great addition I didn't thought of!

When I will working on it, I'll try to make it modifiable with a room parameter.
Something like that maybe:

// in URoomData class
EDoorDirection PreservedFacingDirection;

// in DungeonGenerator when calling it
const EDoorDirection& DirectionOffset = newRoom->GetRoomData()->PreservedFacingDirection
if ( /*...*/ possibleNewDoors[j].PreservesRotation(URoom::Sub(parentDoorDir, DirectionOffset))) 
{ /*...*/ }

Thus, if for some reason the designer would like to change the direction of the room with a preserved rotation, he just has to change this variable in data instead of moving all actors and meshes in the level.

It's not a feature I'm using, so what do you think?

Awesome!

The door sizes are unnecessary if you use door types, they would be more of a visual reference than anything else, as door class you have is what the real door size is at the end of the day, heck, you could even do away with that and not care at all and design the connections (could be completely open as well), and the editor door size is just a reference on where the actual connection to the next room will happen

Datatables are a pretty powerful tool, in this case I went with a datatable rowbase (plugin user should create a datatable to control door types) because the design and art teams at my company requested the feature be added (control what doors connect), and as I'm a very lazy person, I can't be bothered to have a enum for door types and have requests to add new types again and again, better let them control that on their own... At least that's what we use datatables a lot of the time, we have a lot of parametrized classes that they can configure in tables so they can be generated correctly during runtime. We also initially had a handful of enums (blueprint enums aargh), but as we moved from blueprints to C++ (big performance gain, we meassured it), having enums wasn't an option unless it was a fairly static enum (not much changes if at all, like the cardinal directions)

I also opens the door (pun not intended) to have a list of compatible door types, I did this for rooms instead, here is the code I came up with:
In ProceduralDungeonTypes.h

USTRUCT(BlueprintType)
struct FRoomType : public FTableRowBase
{
	GENERATED_BODY()
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FName Type;
	// Title property doesn't work on Datatable row handle arrays >:C
	// RowType restricts what datatables appear when filling this array
	UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (TitleProperty = "RowName", ToolTip = "Single direction A->B type of compatibility", RowType="/Script/ProceduralDungeon.RoomType"))
	TArray<FDataTableRowHandle> CompatibleTypes;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FText Description;
	// Visible anywhere for testing purposes, used on other parts of code assuming you only have 1 room type datatable
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	TArray<FName> CompatibleRoomTypeNames;

#if WITH_EDITOR
	const TArray<FName>& UpdateCompatibleRoomTypeNames()
	{
		CompatibleRoomTypeNames.Empty();
		for(const auto& allowedType : CompatibleTypes)
		{
			CompatibleRoomTypeNames.Add(allowedType.RowName);
		}
		return CompatibleRoomTypeNames;
	}
	// pretty handy function I learned existed by accident trying to emulate your code
	virtual void OnDataTableChanged(const UDataTable* InDataTable, const FName InRowName) override
	{
		FDataTableRowHandle rowHandle;
		rowHandle.DataTable = InDataTable;
		rowHandle.RowName = InRowName;
		FRoomType* rowStruct = rowHandle.GetRow<FRoomType>("");
		if(!rowStruct)
			return;
		rowStruct->UpdateCompatibleRoomTypeNames();
	}
#endif
};

In RoomData.h

// ...
// add as a public member
	UPROPERTY(EditAnywhere, Category = "Room", meta = (RowType="/Script/ProceduralDungeon.RoomType"))
	FDataTableRowHandle RoomType;
// ...

The main idea of the room types is give the option to a designer to decide if a room can be connected to another (for example, 2 rooms with monsters shouldn't be connected)

The you only have to check if the room type is in the list of compatible room types of the other room.


The PreservesRotation simplification looks pretty handy, thanks

The PreservedFacingDirection looks awesome, although the quick implementation looks like it won't achieve the desired result,
For example, the default PreservedFacingDirection is North, so a West door would need to be connected to a East facing door. In the case your preserved facing direction is East, then that East door is now a South door, so it needs to be connected to a North door. Maybe we can do some shenanigans with the direction enum (although it would require the directions to have 0 through 3 values, there's a 255 throwing a wrench in the works), you could do a sum modulo 4:

North: 0 // 0 degrees
East: 1 // 90 degrees
South: 2 // 180 degrees
West: 3 // 270 degrees

// some examples
North + East := East // 90 degrees
East + South := West // 270 degrees
South + West = 5 :=(mod4) 1 = East // 180 + 270 := 90 degrees

This only works if you redefine the values of the enum, you could have another hidden enum value, I don't quite understand why one of them is 255.

Another thing I would also suggest is adding a tooltip to the directions (North could be "faces positive X, 0 degrees", and so on), at least for me that's a great help (I added that on my mutated version of the plugin)

Another thing a designer in my team asked was if I could make it so some orientations were valid (for example, the room could face North and West, but not East nor South), even though I told him it would be somewhat of a hassle (the PreservesRotation would be quite convoluted), and that I wouldn't be implementing that, I can't deny it's a good idea. So maybe you could consider making the PreservedFacingDirection an array, and with the enum trick I exposed above it shouldn't be that much of a hassle as I initially thought.

On of the challenges I came about is that the RoomData door directions aren't the same as the actual room door directions (when placed in the world if a room doesn't need it's rotation preserved), so I had to pass the parent door direction to choose next room as well to decide the next room (array filtering and stuff), maybe you could add some blueprint functionality to the room class and pass the room itself instead of its room data, at least, I don't quite understand if the door directions are updated for a room data of a placed room that was rotated (I'm fairly certain not, but I could be wrong)

This is getting quite long, feel free to hit me up if you need more ideas, my team has an unending stream of them

Hi @ImBlackMagic

The door sizes are unnecessary if you use door types, they would be more of a visual reference than anything else

Yes, exactly. And in the current version of the plugin, the door size is already only a visual hint for designers and artists, so they acn know what the space the door classes will take in the room (so they do not put a prop static mesh in the way).
I was just suggesting to add the size editable per door type in the door type table instead of a unique door size adjustable in the plugin settings as currently done in the plugin 🙂


having enums wasn't an option unless it was a fairly static enum (not much changes if at all, like the cardinal directions)

Yes I agree with you, enums was never an option for me too for door types.

Datatables are a pretty powerful tool

Yes I know, I am just not familiar with all the features a datatable can offer in C++ when defined in blueprint by designers...
I prefer to use data assets over data tables (like I did for RoomData) and so I use mostly data assets in my projects. Because we can use inheritance and each asset is separated (for locking in versiong system), while a database is a unique asset and has static data structure.
However, using a data table for door types seems a great idea to me, better than data assets (since advantages I listed above are not useful for door types imho).


The main idea of the room types is give the option to a designer to decide if a room can be connected to another (for example, 2 rooms with monsters shouldn't be connected)

This is a good feature as well!
However, this can be done by plugin user very easily, even in pure blueprint with the current version of the plugin (you can create a blueprint child class of RoomData and add what you want in it, so your data table row handle too, and adding in blueprint the compatibility check function)
I think it is not really necessary to implement this feature, but yes it can be useful for a lot of people if it was.

In my opinion, I would go instead for a tagging system with compatibility rules for tags.
For example you have a datatable of tags, like monster and treasure, and you affect in room data instances all the tags you want. You also have to define rules (I don't know how yet) where you say "monster tag not compatible with monster tag" and "treasure tag not compatible with treasure tag". Thus a room with both tag monster and treasure can't have any other room connected to it with any tag monster or treasure.

I'll implement that feature only at the end if I have some time to do it.


This only works if you redefine the values of the enum, you could have another hidden enum value, I don't quite understand why one of them is 255.

For the PreserveDirection feature, you're right, the code I wrote in my previous comment will not works correctly with rotated rooms, we have to apply the room rotation to the door rotation in order to make it work 😓

Also, you don't have to modify the enum though. The function URoom::Add() and URoom::Sub() will handle correctly the rotations. (the 255 for east is because it will be updated to -1 when casted to int8 type, but actually I don't remember why I did that instead of modulo in unsigned type...)
However, you are right, I should be able improve it by using uint8 and do a module 4 for adding and substracting directions (more performant than the current implementation I've done)

maybe you could consider making the PreservedFacingDirection an array

Yes, that a good idea too!


maybe you could add some blueprint functionality to the room class and pass the room itself instead of its room data

Yes, it is something I was thinking of doing for the feature of issue #25 because plugin user would need to have access to URoom instances in blueprint before spawning them, so we can do some init (like placing key in some room to unlock doors of another room) and then check dungeon validation with it in IsValidDungeon blueprint function.
However, it is not as easy because at the beginning URoom was not designed to be accessible via blueprint. So I need to make a lot of changes.
What you can do instead is to pass the placed room direction so you can rotate the door directions with the room direction to get the real door directions.


feel free to hit me up if you need more ideas, my team has an unending stream of them

Everyone is always welcomed to create new issues with ideas for features 😄

Hi @BenPyton

On the data asset vs datatable is pretty important to recognize their differences and their uses, like you said, data assets can be inherited from, while datatables have a static structure (unless you modify the underlying struct), so on things you don't want a designer to get their grubby mits on and make a mess because of a change in structure, you can use a datatable.

And while data assets are very flexible, you have to edit each of them individually making it somewhat of a hassle, while the data inside of a datatable is inside it all together and very "easy" to edit. There's also the point that you can export datatables to csv or json files and then re import them from a csv or json, allowing some work to be done in external tools, or even make them an external database, something data assets can't achieve.

Datatable rows have a neat function called GetRow<FStruct>(messageString) that returns a pointer to the struct value of that row in the table, or nullptr if the row wasn't found. it's a pretty neat feature they have, and you could even make inheritance based structs for different tables, and use get rows accordingly to the type you need.

We use datatables a lot for configuring abilities and other game related mechanics, although I'm now thinking of having a hybrid approach, listing data assets inside datatables so we can have mutated data inside the data assets for configuration, while still having the flexibility of listing them in a table for easy configuration (characters use abilities listed in the abilities table and we get the data dynamically that way for example)

On the tags idea that's exactly what I implemented in the struct I posted before, the CompatibleTypes property contain a valid list of transitions from that room type, an example might be like you said, a "Treasure" type room can only connect to "Transition" type rooms and "Monster" type rooms for example, the way I implemented it makes it so it's only one directional, for example, you can go from a "Treasure" to "Transition", but not the other way.

This is the RoomCard structure (WIP) that I came up with for our project (it's way outside the realms of the plugin tho), I implemented the compatibility check inside this structure


USTRUCT(BlueprintType)
struct FST_RoomCard : public FTableRowBase
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadWrite, EditAnywhere)
    URoomData* Room{nullptr};

    UPROPERTY(BlueprintReadWrite, EditAnywhere)
    int RoomNumberThreshold{0};

    UPROPERTY(BlueprintReadWrite, EditAnywhere)
    int Count{1};

    UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
    int AvailableCount{0};

#if WITH_EDITORONLY_DATA
    // Don't take care of it, it is just used to put a name in the editor details panel.
    UPROPERTY(Transient, VisibleInstanceOnly, Category = "DoorDef", meta = (DisplayName = "Name"))
    FString EdName;
#endif

    FST_RoomCard()
    {
    }

#if WITH_EDITOR
    const FString& UpdateEdName()
    {
        const FString RoomName = nullptr != Room ? Room->GetName() : "None";
        const FString RoomType = nullptr != Room ? Room->RoomType.RowName.ToString() : "None";
        EdName = FString::Printf(TEXT("[%s] Thresh %d Count %d Type %s"), *RoomName, RoomNumberThreshold, Count,
                                 *RoomType);
        return EdName;
    }

    int UpdateAvailableCount()
    {
        AvailableCount = Count;
        return AvailableCount;
    }
#endif

    bool IsCompatible(const TArray<FName>& AllowedRoomTypes, const FDoorDef& ParentDoor, EDoorDirection DoorDirection,
                      bool PreserveRotation, int CurrentRoomNumber) const
    {
        if (!UKismetSystemLibrary::IsValid(Room) || AvailableCount == 0 || RoomNumberThreshold > CurrentRoomNumber)
            return false;
        if (!AllowedRoomTypes.Contains(Room->RoomType.RowName) && !AllowedRoomTypes.IsEmpty())
            return false;
        const FDoorDef* CompatibleDoor = Room->Doors.FindByPredicate(
            [ParentDoor, PreserveRotation, DoorDirection](const FDoorDef& Door)
            {
                return Door.IsCompatible(ParentDoor) && (!PreserveRotation || Door.PreservesRotation(DoorDirection));
            });
        if (!CompatibleDoor)
            return false;
        return true;
    }
};

In the IsCompatible function you can see how I check if another room data is compatible with the room data in the room card, part of that function could go inside the room data class as well and maybe even make it a blueprint callable function (now that I think about it, that should be the way I do that as well), it was originally a lambda function inside my dungeon generator child class, but it was fugly to say the least and not very functional, so I moved it there, I'm now thinking of moving it to the data asset.

In case you're curious, I have a room card array in my custom dungeon generator child class (called room card deck lol), the idea of the card is that you have that amount of that room type to put into the dungeon, and each time you locate one the available count goes down, the room number threshold is there so a room can't be placed unless there's a certain amount of rooms placed in the dungeon already (you don't want a treasure room to appear to early, or a boss fight for example)


On the URoom side of things, you should be able to "change" it easily, add the Blueprintable tag to the class, add UPROPERTY(BlueprintReadOnly, VisibleAnywhere) to the properties, UFUNCTION(BlueprintCallable) to the functions, and adjust some of the accessibility of some things and you should be golden (maybe add native events later, but I don't think it's necessary)


Another thing that's already pretty offtopic from this is looking at the possibility of having some actors inside a room be replicated for multiplayer, from what I saw in the code this is absolutely not an easy thing to do (if at all possible), this would make the use of the plugin much much easier for multiplayer use and have less implementation barriers (make a "spawner" for an actor you want to be replicated, it's not pretty)

Thanks for engagement, it's really appreciated!

I've pushed the commit which adds this feature on the branch 3.0.

Actually I've opted (once again) for data assets instead of data table, for multiple reasons.

I've worked with data tables until the end, and changed to data assets when I've stumbled upon some weird and complex implementations for the ChooseDoor function.

Here are some arguments (from the most to the least importance) why I've dropped the data table in favor to data assets:

  • An FDataTableRowHandle can't be used as a key for a map or a set. Using only the row name, or a member of the row struct is not really safe. Using the row struct directly as a map key doesn't allow default (null) door type when the row handle is not set.
  • Updating name or deleting a row in the table doesn't automatically update or delete the references used in 'RoomData' assets or Door actors (not noobproof)0. While data assets do.
  • Simplifying a lot the use in the overriden functions in blueprint. Instead of having a row handle plus a custom BP node to test validity, or a bool as a supplementary function input to tell if it is valid, we just have to use the IsValid node for the data asset like any other object types in BP, and using a map to select a door class for example.
  • Having the possibilty with data assets to get all the references to a certain door type, in one click using the asset reference viewer.
  • With a data asset, we just have to set the ref, doesn't need to have a name in addition. This also allow a better search with the asset picker when we have a lot of door types.

Here are some arguments in favor to data tables, but with counter arguments why they didn't changed my final decision:

  • Data table takes less disk space than multiple data assets for the same number of door type. Even if it will becomes not negligible with a lot of door types, it is not that significant. Around 1.5kB per door type data asset, we should need hundreds of thousands of door types in order to become a concern.
  • Data tables are centralized and easier to work on multiple door types at the same time. It's true, but we don't change that often door types to be a real advantage. I think it's better to be able to organize like a folder for each type of door containing the data asset of the door type and all door actors of that door type for example.
  • Data tables are easily imported from csv or downloaded from exernal source. However, door types is not a thing that really changes that often nor something with hundreds or thousands of items to edit to make that feature really useful.

So, the noobproofness of data assets (be able to change all references of a deleted asset, or renaming a data asset doesn't break references) and the lack of a real advantage of data table over the data assets for this specific case did not convince me to keep the data table in the end.

Beside that, the logic of door compatibility is the same as described earlier in this thread.

(This comment is mostly for future reference, to remind me later why I've chosen data assets over data table)

Awesome!

thanks for the update! much appreciated, and what you say makes a lot of sense, the "advantages" of data tables over data assets are not really that much of an advantage if you put it that way

We are currently not working with either sets nor maps as they don't have replication support for multiplayer applications, although we really have to be careful where they can be used (variable/property doesn't need to be replicated) and where they can be replicated, for now we are erring in the side of caution.

The same could be said of Room Types being a data asset instead of a reference in a data table, it would be cleaner as well

Again, thanks for the update!

Feature added in the plugin version 3.0.0