From 7b1c7eedaca08afc054715d4cf80013c2121315b Mon Sep 17 00:00:00 2001 From: Shun Moriya <23472415+shun126@users.noreply.github.com> Date: Wed, 10 Sep 2025 01:39:40 +0900 Subject: [PATCH 1/2] * Add DungeonRoomSensorDatabase * Removed access to editor functions while in standalone mode. * Fixed several bugs --- CHANGELOG.md | 16 +++ DungeonGenerator.uplugin | 4 +- README.md | 16 +-- .../Private/Core/Generator.cpp | 2 +- .../Private/Core/RoomGeneration/Room.h | 2 +- .../Private/Core/RoomGeneration/Room.inl | 2 +- .../Private/DungeonGenerateActor.cpp | 29 +++-- .../Private/DungeonGenerateBase.cpp | 112 ++++++++++++------ .../Parameter/DungeonGenerateParameter.cpp | 1 + .../SubActor/DungeonRoomSensorDatabase.cpp | 53 +++++++++ .../Public/DungeonGenerateActor.h | 1 + .../Public/Mission/DungeonRoomParts.h | 20 ++++ .../Parameter/DungeonGenerateParameter.h | 39 ++++-- .../Public/Parameter/DungeonMeshSetDatabase.h | 16 +-- .../Parameter/DungeonMeshSetSelectionMethod.h | 21 ++++ .../SubActor/DungeonRoomSensorDatabase.h | 68 +++++++++++ .../Private/DungeonGeneratorEditorModule.cpp | 15 ++- .../DungeonRoomSensorDatabaseFactory.cpp | 20 ++++ .../DungeonRoomSensorDatabaseTypeActions.cpp | 34 ++++++ .../DungeonRoomSensorDatabaseFactory.h | 23 ++++ .../DungeonRoomSensorDatabaseTypeActions.h | 24 ++++ 21 files changed, 427 insertions(+), 91 deletions(-) create mode 100644 Source/DungeonGenerator/Private/SubActor/DungeonRoomSensorDatabase.cpp create mode 100644 Source/DungeonGenerator/Public/Parameter/DungeonMeshSetSelectionMethod.h create mode 100644 Source/DungeonGenerator/Public/SubActor/DungeonRoomSensorDatabase.h create mode 100644 Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseFactory.cpp create mode 100644 Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseTypeActions.cpp create mode 100644 Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseFactory.h create mode 100644 Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseTypeActions.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 299c5e4..3063c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log - Procedural 3D Dungeon Generator Plug-in +## 20250903-1.7.7 (60) +### Changes +* Add DungeonRoomSensorDatabase +* Fixed several bugs +### 変更点 +* DungeonRoomSensorDatabaseを追加 +* いくつかの不具合を修正 + +## 20250903-1.7.6 (59) +### Changes +* Removed access to editor functions while in standalone mode. +* Fixed several bugs +### 変更点 +* スタンドアローンモード中にエディタ機能へアクセスしていたので削除 +* いくつかの不具合を修正 + ## 20250831-1.7.5 (58) ### Changes * Enable/disable control of shadow generation in point light derived classes changed from per-partition to per-light. diff --git a/DungeonGenerator.uplugin b/DungeonGenerator.uplugin index 0a088a6..8a76e38 100644 --- a/DungeonGenerator.uplugin +++ b/DungeonGenerator.uplugin @@ -1,7 +1,7 @@ { "FileVersion": 3, - "Version": 58, - "VersionName": "1.7.5", + "Version": 60, + "VersionName": "1.7.7", "FriendlyName": "Dungeon Generator", "Description": "Procedural 3d dungeon generator plugin. Easy generation of levels, mini-maps and missions.", "Category": "Procedural Systems", diff --git a/README.md b/README.md index d5da62e..dc2448b 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@

Dungeon generator plugin for Unreal Engine 5

-

-Issues, -Discussions, -Wiki, -Doxygen -

[![license](https://img.shields.io/github/license/shun126/DungeonGenerator)](https://github.com/shun126/DungeonGenerator/blob/main/LICENSE) @@ -16,7 +10,7 @@ [![stars](https://img.shields.io/github/stars/shun126/DungeonGenerator?style=social)](https://github.com/shun126/DungeonGenerator/stargazers) [![youtube](https://img.shields.io/youtube/views/1igd4pls5x8?style=social)](https://youtu.be/1igd4pls5x8) -Please visit our website for full feature list: [https://happy-game-dev.undo.jp/](https://happy-game-dev.undo.jp/plugins/DungeonGenerator/) +Please visit our website for full feature list: [https://happy-game-dev.undo.jp/](https://happy-game-dev.undo.jp/plugins/DungeonGenerator/index.html) ![Screenshot](Document/Screenshot.gif) @@ -66,7 +60,7 @@ The foundational generation algorithm you shared was a major source of inspirati * Open Unreal Engine Editor and create a project using the First Person template or Third Person template. * Install the Dungeon Generator plugin via Epic Games Launcher, or copy it to the `Plugins` directory of your project. * Enable the plugin content. -* Open `Plugins/Dungeon Generator/Contents/Demonstration`. +* Open `Plugins/Dungeon Generator/Contents/Maps/Demonstration`. ![](Document/ContentBrowser.gif) @@ -80,11 +74,11 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY Or, [Fab](https://fab.com/s/f5587c55bad0) is releasing it under Epic license. If you need a license other than the GPL, please consider it. Proceeds will be used to fund the development of our game. # 👀 See also +* [Issues](https://github.com/shun126/UE5-DungeonGeneratorDemo/issues) +* [Discussions](https://github.com/shun126/UE5-DungeonGeneratorDemo/discussions) * [Wiki](https://github.com/shun126/UE5-DungeonGeneratorDemo/wiki) * [DeepWiki](https://deepwiki.com/shun126/DungeonGenerator) * [Doxygen](https://happy-game-dev.undo.jp/_doxygen/dungeon_generator/index.html) -* [Issues](https://github.com/shun126/UE5-DungeonGeneratorDemo/issues) -* [Discussions](https://github.com/shun126/UE5-DungeonGeneratorDemo/discussions) The [Fab](https://fab.com/s/f5587c55bad0) version includes the following enhancements. * Sub-levels can be applied as dungeon rooms @@ -97,6 +91,8 @@ The [Fab](https://fab.com/s/f5587c55bad0) version includes the following enhance This is an easy to use. Simply drop the DungeonGenerateActor into your level, set the grid scale and number of rooms and start generating out your structures. Please read the [Wiki](https://github.com/shun126/UE5-DungeonGeneratorDemo/wiki) for more information. +Please visit our website for full feature list: [https://happy-game-dev.undo.jp/](https://happy-game-dev.undo.jp/plugins/DungeonGenerator/index.html) + # 😀 Authors * Nonbiri ([X.com](https://x.com/happy_game_dev) / [YouTube](https://www.youtube.com/channel/UCkLXe57GpUyaOoj2ycREU1Q)) * Shun Moriya ([X.com](https://x.com/monjiro1972)) diff --git a/Source/DungeonGenerator/Private/Core/Generator.cpp b/Source/DungeonGenerator/Private/Core/Generator.cpp index 1a5d081..00e496d 100644 --- a/Source/DungeonGenerator/Private/Core/Generator.cpp +++ b/Source/DungeonGenerator/Private/Core/Generator.cpp @@ -1273,6 +1273,7 @@ namespace dungeon uint8_t depthRatioFromStart = 0; if (GetDeepestDepthFromStart() > 0) { + // TODO: ダンジョンの深さを0~255に正規化する関数を検討してください float depthFromStart = static_cast(room->GetDepthFromStart()); depthFromStart /= static_cast(GetDeepestDepthFromStart()); depthRatioFromStart = static_cast(depthFromStart * 255.f); @@ -1536,7 +1537,6 @@ namespace dungeon int32 count = static_cast(std::sqrt(static_cast(room->GetRect().Area()))); if (count >= 5) { - count /= 2; for (int32 i = 0; i < count; ++i) { const int32 x = mGenerateParameter.GetRandom()->Get(room->GetLeft(), room->GetRight()); diff --git a/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.h b/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.h index f60c99a..a99d157 100644 --- a/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.h +++ b/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.h @@ -80,7 +80,7 @@ namespace dungeon 識別子を取得 @return 識別子 */ - const Identifier& GetIdentifier() const noexcept; + const Identifier GetIdentifier() const noexcept; /** X座標を取得 diff --git a/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.inl b/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.inl index 0c0b68d..00a3a26 100644 --- a/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.inl +++ b/Source/DungeonGenerator/Private/Core/RoomGeneration/Room.inl @@ -10,7 +10,7 @@ All Rights Reserved. namespace dungeon { - inline const Identifier& Room::GetIdentifier() const noexcept + inline const Identifier Room::GetIdentifier() const noexcept { return mIdentifier; } diff --git a/Source/DungeonGenerator/Private/DungeonGenerateActor.cpp b/Source/DungeonGenerator/Private/DungeonGenerateActor.cpp index b173b34..b479891 100644 --- a/Source/DungeonGenerator/Private/DungeonGenerateActor.cpp +++ b/Source/DungeonGenerator/Private/DungeonGenerateActor.cpp @@ -60,6 +60,11 @@ void ADungeonGenerateActor::PostInitializeComponents() // Calling the parent class Super::PostInitializeComponents(); + if (AutoGenerateAtStart == true) + { + PostGenerateImplementation(); + } + //if (GetNetMode() != NM_Standalone) if (GetLocalRole() == ROLE_Authority) { @@ -201,16 +206,6 @@ void ADungeonGenerateActor::PreGenerateImplementation() return; } - if (GetActorRotation().Equals(FRotator::ZeroRotator) == false) - { - DUNGEON_GENERATOR_ERROR(TEXT("The actor's rotation is not applied in the generated dungeon.")); - } - - if (GetActorScale().Equals(FVector::OneVector) == false) - { - DUNGEON_GENERATOR_ERROR(TEXT("The actor's scale is not applied in the generated dungeon.")); - } - Dispose(true); // インスタンスメッシュを登録 @@ -313,6 +308,19 @@ void ADungeonGenerateActor::PreGenerateImplementation() EndDungeonGeneration(); } +void ADungeonGenerateActor::PostGenerateImplementation() const +{ + if (GetActorRotation().Equals(FRotator::ZeroRotator) == false) + { + DUNGEON_GENERATOR_ERROR(TEXT("The actor's rotation is not applied in the generated dungeon.")); + } + + if (GetActorScale().Equals(FVector::OneVector) == false) + { + DUNGEON_GENERATOR_ERROR(TEXT("The actor's scale is not applied in the generated dungeon.")); + } +} + void ADungeonGenerateActor::Dispose(const bool flushStreamLevels) { if (IsGenerated()) @@ -349,6 +357,7 @@ void ADungeonGenerateActor::MulticastOnGenerateDungeon_Implementation() DUNGEON_GENERATOR_LOG(TEXT("MulticastOnGenerateDungeon: %s"), HasAuthority() ? TEXT("Server") : TEXT("Client")); #endif PreGenerateImplementation(); + PostGenerateImplementation(); } void ADungeonGenerateActor::GenerateDungeonWithParameter(UDungeonGenerateParameter* dungeonGenerateParameter) diff --git a/Source/DungeonGenerator/Private/DungeonGenerateBase.cpp b/Source/DungeonGenerator/Private/DungeonGenerateBase.cpp index a0a5493..ec87f5a 100644 --- a/Source/DungeonGenerator/Private/DungeonGenerateBase.cpp +++ b/Source/DungeonGenerator/Private/DungeonGenerateBase.cpp @@ -54,6 +54,8 @@ ADungeonGenerateActorは配置可能(Placeable)、ADungeonGeneratedActorは配 #include #include +#include "SubActor/DungeonRoomSensorDatabase.h" + #if WITH_EDITOR // UnrealEd #include @@ -77,10 +79,21 @@ namespace const FString LevelsFolderPath = TEXT("/Levels/"); const FString InteriorsFolderPath = TEXT("Interiors"); - bool operator==(const EDungeonRoomItem left, const dungeon::Room::Item right) + constexpr bool operator==(const EDungeonRoomItem left, const dungeon::Room::Item right) { return static_cast(left) == static_cast(right); } + + constexpr EDungeonRoomItem Cast(const dungeon::Room::Item item) + { + return static_cast(item); + } + + constexpr EDungeonRoomLocatorParts Cast(const dungeon::Room::Parts parts) + { + return static_cast(parts); + } + } ADungeonGenerateBase::ADungeonGenerateBase(const FObjectInitializer& initializer) @@ -278,41 +291,45 @@ void ADungeonGenerateBase::DestroySpawnedActors(UWorld* world) UGameplayStatics::GetAllActorsWithTag(world, GetDungeonGeneratorTag(), actors); #if WITH_EDITOR - TArray deleteFolders; - - // TagにDungeonGeneratorTagがついているアクターのフォルダを回収 - for (const AActor* actor : actors) + // Standaloneモードによる起動ではGEditorやGEngineが無効になる + if (IsValid(GEditor)) { - if (IsValid(actor)) - { - const FFolder& folder = actor->GetFolder(); - - if (!folder.IsValid()) - continue; - if (folder.GetPath() == folder.GetEmptyPath()) - continue; + TArray deleteFolders; - if (const auto* actorFolder = folder.GetActorFolder()) + // TagにDungeonGeneratorTagがついているアクターのフォルダを回収 + for (const AActor* actor : actors) + { + if (IsValid(actor)) { - if (!actorFolder->IsValid()) + const FFolder& folder = actor->GetFolder(); + + if (!folder.IsValid()) continue; - } + if (folder.GetPath() == folder.GetEmptyPath()) + continue; + + if (const auto* actorFolder = folder.GetActorFolder()) + { + if (!actorFolder->IsValid()) + continue; + } - deleteFolders.AddUnique(folder); + deleteFolders.AddUnique(folder); + } } - } - // パスが長い順に並べ替え - deleteFolders.Sort([](const FFolder& l, const FFolder& r) + // パスが長い順に並べ替え + deleteFolders.Sort([](const FFolder& l, const FFolder& r) + { + return l.GetPath().GetStringLength() > r.GetPath().GetStringLength(); + } + ); + + // フォルダを削除 + for (FFolder& folder : deleteFolders) { - return l.GetPath().GetStringLength() > r.GetPath().GetStringLength(); + FActorFolders::Get().DeleteFolder(*world, folder); } - ); - - // フォルダを削除 - for (FFolder& folder : deleteFolders) - { - FActorFolders::Get().DeleteFolder(*world, folder); } #endif @@ -1297,18 +1314,35 @@ void ADungeonGenerateBase::CreateImplement_PrepareSpawnRoomSensor(RoomAndRoomSen // RoomSensorActorを生成 mGenerator->ForEach([this, &roomSensorCache](const std::shared_ptr& room) { - ADungeonRoomSensorBase* roomSensorActor = SpawnRoomSensorActorDeferred( - mParameter->GetRoomSensorClass(), - room->GetIdentifier(), - room->GetCenter() * mParameter->GetGridSize().To3D() + GetActorLocation(), - room->GetExtent() * mParameter->GetGridSize().To3D(), - static_cast(room->GetParts()), - static_cast(room->GetItem()), - room->GetBranchId(), - room->GetDepthFromStart(), - mGenerator->GetDeepestDepthFromStart() - ); - roomSensorCache[room.get()] = roomSensorActor; + auto* roomSensorClass = mParameter->GetRoomSensorClass(); + if (const auto* roomSensorDatabase = mParameter->GetRoomSensorDatabase()) + { + // TODO: ダンジョンの深さを0~255に正規化する関数を検討してください + auto depthFromStart = static_cast(room->GetDepthFromStart()); + depthFromStart /= static_cast(mGenerator->GetDeepestDepthFromStart()); + const auto depthRatioFromStart = static_cast(depthFromStart * 255.f); + + roomSensorClass = roomSensorDatabase->Select( + room->GetIdentifier(), + depthRatioFromStart, + GetSynchronizedRandom() + ); + } + if (roomSensorClass) + { + auto* roomSensorActor = SpawnRoomSensorActorDeferred( + roomSensorClass, + room->GetIdentifier(), + room->GetCenter() * mParameter->GetGridSize().To3D() + GetActorLocation(), + room->GetExtent() * mParameter->GetGridSize().To3D(), + static_cast(room->GetParts()), + static_cast(room->GetItem()), + room->GetBranchId(), + room->GetDepthFromStart(), + mGenerator->GetDeepestDepthFromStart() + ); + roomSensorCache[room.get()] = roomSensorActor; + } } ); diff --git a/Source/DungeonGenerator/Private/Parameter/DungeonGenerateParameter.cpp b/Source/DungeonGenerator/Private/Parameter/DungeonGenerateParameter.cpp index f06e6f3..e3b319b 100644 --- a/Source/DungeonGenerator/Private/Parameter/DungeonGenerateParameter.cpp +++ b/Source/DungeonGenerator/Private/Parameter/DungeonGenerateParameter.cpp @@ -45,6 +45,7 @@ UDungeonGenerateParameter* UDungeonGenerateParameter::GenerateRandomParameter(co parameter->DoorPartsSelectionMethod = sourceParameter->DoorPartsSelectionMethod; parameter->DoorParts = sourceParameter->DoorParts; parameter->DungeonRoomSensorClass = sourceParameter->DungeonRoomSensorClass; + parameter->DungeonRoomSensorDatabase = sourceParameter->DungeonRoomSensorDatabase; } return parameter; diff --git a/Source/DungeonGenerator/Private/SubActor/DungeonRoomSensorDatabase.cpp b/Source/DungeonGenerator/Private/SubActor/DungeonRoomSensorDatabase.cpp new file mode 100644 index 0000000..f792b62 --- /dev/null +++ b/Source/DungeonGenerator/Private/SubActor/DungeonRoomSensorDatabase.cpp @@ -0,0 +1,53 @@ +/** +@author Shun Moriya +@copyright 2023- Shun Moriya +All Rights Reserved. +*/ + +#include "SubActor/DungeonRoomSensorDatabase.h" +#include "Core/Math/Random.h" +#include "Core/Debug/Debug.h" +#include "Parameter/DungeonMeshSetDatabase.h" +#include + +UDungeonRoomSensorDatabase::UDungeonRoomSensorDatabase(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ +} + +UClass* UDungeonRoomSensorDatabase::Select(const uint16_t identifier, const uint8_t depthRatioFromStart, const std::shared_ptr& random) const +{ + TArray roomSensors; + roomSensors.Reserve(DungeonRoomSensorClass.Num()); + for (const auto& dungeonRoomSensorClass : DungeonRoomSensorClass) + { + if (IsValid(dungeonRoomSensorClass)) + roomSensors.Add(dungeonRoomSensorClass); + } + if (roomSensors.Num() <= 0) + return nullptr; + + // 抽選 + switch (SelectionMethod) + { + case EDungeonMeshSetSelectionMethod::Random: + { + const auto index = random->Get(roomSensors.Num()); + return roomSensors[index]; + } + case EDungeonMeshSetSelectionMethod::Identifier: + { + const auto index = identifier % roomSensors.Num(); + return roomSensors[index]; + } + case EDungeonMeshSetSelectionMethod::DepthFromStart: + { + const float ratio = static_cast(depthRatioFromStart) / 255.f; + const float index = static_cast(roomSensors.Num() - 1) * ratio; + return roomSensors[static_cast(std::round(index))]; + } + default: + DUNGEON_GENERATOR_ERROR(TEXT("Set the correct SelectionMethod")); + return nullptr; + } +} diff --git a/Source/DungeonGenerator/Public/DungeonGenerateActor.h b/Source/DungeonGenerator/Public/DungeonGenerateActor.h index 0be10d5..d23c78f 100644 --- a/Source/DungeonGenerator/Public/DungeonGenerateActor.h +++ b/Source/DungeonGenerator/Public/DungeonGenerateActor.h @@ -153,6 +153,7 @@ class DUNGEONGENERATOR_API ADungeonGenerateActor : public ADungeonGenerateBase void ApplyInstancedMeshCullDistance(); void PreGenerateImplementation(); + void PostGenerateImplementation() const; #if WITH_EDITOR void DrawDebugInformation() const; diff --git a/Source/DungeonGenerator/Public/Mission/DungeonRoomParts.h b/Source/DungeonGenerator/Public/Mission/DungeonRoomParts.h index 53dbf7d..c1f6703 100644 --- a/Source/DungeonGenerator/Public/Mission/DungeonRoomParts.h +++ b/Source/DungeonGenerator/Public/Mission/DungeonRoomParts.h @@ -68,3 +68,23 @@ inline constexpr bool Equal(const EDungeonRoomLocatorParts locatorParts, const E { return Equal(parts, locatorParts); } + +/** + * Cast from EDungeonRoomLocatorParts to EDungeonRoomParts + * + * EDungeonRoomLocatorPartsからEDungeonRoomPartsへのキャスト + */ +inline constexpr EDungeonRoomParts Cast(const EDungeonRoomLocatorParts parts) +{ + return static_cast(parts); +} + +/** + * Cast from EDungeonRoomParts to EDungeonRoomLocatorParts + * + * EDungeonRoomPartsからEDungeonRoomLocatorPartsへのキャスト + */ +inline constexpr EDungeonRoomLocatorParts Cast(const EDungeonRoomParts parts) +{ + return static_cast(parts); +} diff --git a/Source/DungeonGenerator/Public/Parameter/DungeonGenerateParameter.h b/Source/DungeonGenerator/Public/Parameter/DungeonGenerateParameter.h index 458ef1b..a7a2965 100644 --- a/Source/DungeonGenerator/Public/Parameter/DungeonGenerateParameter.h +++ b/Source/DungeonGenerator/Public/Parameter/DungeonGenerateParameter.h @@ -14,6 +14,7 @@ All Rights Reserved. #include "DungeonGenerateParameter.generated.h" // forward declaration +class UDungeonRoomSensorDatabase; /** Frequency of generation @@ -68,6 +69,7 @@ class DUNGEONGENERATOR_API UDungeonGenerateParameter : public UObject UClass* GetRoomSensorClass() const; + UDungeonRoomSensorDatabase* GetRoomSensorDatabase() const; // Converts from a grid coordinate system to a world coordinate system FVector ToWorld(const FIntVector& location) const; @@ -399,20 +401,32 @@ void EachPillarParts(const std::function& functi /** - Specify the DungeonRoomSensorBase class, - which is a box sensor that covers the room and controls doors and enemy spawn. - - DungeonRoomSensorBaseクラスを指定して下さい。 - DungeonRoomSensorBaseは部屋を覆う箱センサーで、ドアや敵のスポーンを制御します。 - */ - UPROPERTY(EditAnywhere, Category = "DungeonGenerator|RoomSensor", BlueprintReadWrite, meta = (AllowedClasses = "DungeonRoomSensorBase")) + * Specify the DungeonRoomSensorBase class, + * which is a box sensor that covers the room and controls doors and enemy spawn. + * DungeonRoomSensorDatabase takes precedence + * + * DungeonRoomSensorBaseクラスを指定して下さい。 + * DungeonRoomSensorBaseは部屋を覆う箱センサーで、ドアや敵のスポーンを制御します。 + * DungeonRoomSensorDatabaseが優先されます + */ + UPROPERTY(EditAnywhere, Category = "DungeonGenerator|RoomSensor", BlueprintReadWrite, meta = (AllowedClasses = "DungeonRoomSensorBase", DeprecatedProperty, ToolTip = "This variable is deprecated. Please use DungeonRoomSensorDatabase instead.")) TObjectPtr DungeonRoomSensorClass; /** - PluginVersion + * Specify the room sensor database + * The room sensor database specifies the arrangement and direction in the room + * + * ルームセンサーのデータベースを指定して下さい + * ルームセンサーのデータベースは部屋の中の配置や演出を指定します + */ + UPROPERTY(EditAnywhere, Category = "DungeonGenerator|RoomSensor", BlueprintReadWrite) + TObjectPtr DungeonRoomSensorDatabase; - プラグインバージョン - */ + /** + * PluginVersion + * + * プラグインバージョン + */ UPROPERTY(BlueprintReadOnly, Category = "DungeonGenerator") uint8 PluginVersion; @@ -510,6 +524,11 @@ inline UClass* UDungeonGenerateParameter::GetRoomSensorClass() const return DungeonRoomSensorClass; } +inline UDungeonRoomSensorDatabase* UDungeonGenerateParameter::GetRoomSensorDatabase() const +{ + return DungeonRoomSensorDatabase; +} + inline void UDungeonGenerateParameter::EachFloorParts(const std::function& function) const { EachRoomFloorParts(function); diff --git a/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetDatabase.h b/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetDatabase.h index 9a70d90..d62b1e8 100644 --- a/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetDatabase.h +++ b/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetDatabase.h @@ -5,8 +5,8 @@ All Rights Reserved. */ #pragma once +#include "DungeonMeshSetSelectionMethod.h" #include "Parameter/DungeonMeshSet.h" -#include #include #include "DungeonMeshSetDatabase.generated.h" @@ -16,18 +16,6 @@ namespace dungeon class Random; } -/** -Part Selection Method -パーツを選択する方法 -*/ -UENUM(BlueprintType) -enum class EDungeonMeshSetSelectionMethod : uint8 -{ - Identifier, - DepthFromStart, - Random, -}; - /** Database of dungeon mesh sets ダンジョンのメッシュセットのデータベース @@ -73,7 +61,7 @@ class DUNGEONGENERATOR_API UDungeonMeshSetDatabase : public UObject パーツを選択する方法 */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DungeonGenerator") - EDungeonMeshSetSelectionMethod SelectionMethod = EDungeonMeshSetSelectionMethod::Identifier; + EDungeonMeshSetSelectionMethod SelectionMethod = EDungeonMeshSetSelectionMethod::DepthFromStart; /** Set the DungeonRoomMeshSet; multiple DungeonRoomMeshSets can be set. diff --git a/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetSelectionMethod.h b/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetSelectionMethod.h new file mode 100644 index 0000000..3a8d147 --- /dev/null +++ b/Source/DungeonGenerator/Public/Parameter/DungeonMeshSetSelectionMethod.h @@ -0,0 +1,21 @@ +/** +@author Shun Moriya +@copyright 2025- Shun Moriya +All Rights Reserved. +*/ + +#pragma once +#include +#include "DungeonMeshSetSelectionMethod.generated.h" + +/** +Part Selection Method +パーツを選択する方法 +*/ +UENUM(BlueprintType) +enum class EDungeonMeshSetSelectionMethod : uint8 +{ + Identifier, + DepthFromStart, + Random, +}; diff --git a/Source/DungeonGenerator/Public/SubActor/DungeonRoomSensorDatabase.h b/Source/DungeonGenerator/Public/SubActor/DungeonRoomSensorDatabase.h new file mode 100644 index 0000000..f33e07a --- /dev/null +++ b/Source/DungeonGenerator/Public/SubActor/DungeonRoomSensorDatabase.h @@ -0,0 +1,68 @@ +/** +@author Shun Moriya +@copyright 2023- Shun Moriya +All Rights Reserved. +*/ + +#pragma once +#include "Parameter/DungeonMeshSetSelectionMethod.h" +#include +#include "DungeonRoomSensorDatabase.generated.h" + +namespace dungeon +{ + class Random; +} + +/** + * Database class that manages dungeon room intrusion detection sensors + * + * UDungeonRoomSensorDatabase is a database class that holds and manages multiple UDungeonRoomSensor + * Database class that holds and manages multiple UDungeonRoomSensors. + * draws lots and selects available sensors based on set conditions. + * + * ダンジョンの部屋侵入検知センサーを管理するデータベースクラス + * + * UDungeonRoomSensorDatabase は、複数の UDungeonRoomSensor を保持・管理する + * データベースクラスです。 + * 設定された条件を元に、利用可能なセンサーを抽選し選択します。 +*/ +UCLASS(ClassGroup = "DungeonGenerator") +class DUNGEONGENERATOR_API UDungeonRoomSensorDatabase : public UObject +{ + GENERATED_BODY() + +public: + /** + * constructor + */ + explicit UDungeonRoomSensorDatabase(const FObjectInitializer& ObjectInitializer); + + /** + * destructor + */ + virtual ~UDungeonRoomSensorDatabase() override = default; + + /** + * Select the DungeonRoomSensor class that matches the condition of the argument + * + * 引数の条件にあったDungeonRoomSensorのクラスを選択します + */ + UClass* Select(const uint16_t identifier, const uint8_t depthRatioFromStart, const std::shared_ptr& random) const; + +protected: + /** + * Room Sensor Generation Rules + * ルームセンサーの生成ルール + */ + UPROPERTY(EditAnywhere, Category = "DungeonGenerator") + EDungeonMeshSetSelectionMethod SelectionMethod = EDungeonMeshSetSelectionMethod::Identifier; + + /** + * Register the RoomSensor to be placed. + * + * 配置するRoomSensorを登録して下さい。 + */ + UPROPERTY(EditAnywhere, Category = "DungeonGenerator", meta = (AllowedClasses = "DungeonRoomSensorBase")) + TArray> DungeonRoomSensorClass; +}; diff --git a/Source/DungeonGeneratorEditor/Private/DungeonGeneratorEditorModule.cpp b/Source/DungeonGeneratorEditor/Private/DungeonGeneratorEditorModule.cpp index 42632c4..fa465b1 100644 --- a/Source/DungeonGeneratorEditor/Private/DungeonGeneratorEditorModule.cpp +++ b/Source/DungeonGeneratorEditor/Private/DungeonGeneratorEditorModule.cpp @@ -7,6 +7,7 @@ All Rights Reserved. #include "DungeonGeneratorEditorModule.h" #include "DungeonGeneratorStyle.h" #include "DungeonGeneratorCommands.h" +#include "SubActor/DungeonRoomSensorDatabaseTypeActions.h" #include "Parameter/DungeonMeshSetDatabaseTypeActions.h" #include "Parameter/DungeonGenerateParameterTypeActions.h" #include "BuildInfomation.h" @@ -80,6 +81,12 @@ void FDungeonGenerateEditorModule::StartupModule() AssetTools.RegisterAssetTypeActions(actionType.ToSharedRef()); } + + // Register FDungeonRoomSensorDatabaseTypeActions + { + TSharedPtr actionType = MakeShareable(new FDungeonRoomSensorDatabaseTypeActions(gameAssetCategory)); + AssetTools.RegisterAssetTypeActions(actionType.ToSharedRef()); + } } void FDungeonGenerateEditorModule::ShutdownModule() @@ -324,9 +331,11 @@ void FDungeonGenerateEditorModule::DisposeDungeon(UWorld* world, const bool flus UWorld* FDungeonGenerateEditorModule::GetWorldFromGameViewport() { - if (const FWorldContext* worldContext = GEngine->GetWorldContextFromGameViewport(GEngine->GameViewport)) - return worldContext->World(); - + if (IsValid(GEngine)) + { + if (const auto* worldContext = GEngine->GetWorldContextFromGameViewport(GEngine->GameViewport)) + return worldContext->World(); + } return nullptr; } diff --git a/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseFactory.cpp b/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseFactory.cpp new file mode 100644 index 0000000..cc17953 --- /dev/null +++ b/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseFactory.cpp @@ -0,0 +1,20 @@ +/** +@author Shun Moriya +@copyright 2025- Shun Moriya +All Rights Reserved. +*/ + +#include "SubActor/DungeonRoomSensorDatabaseFactory.h" +#include "SubActor/DungeonRoomSensorDatabase.h" + +UDungeonRoomSensorDatabaseFactory::UDungeonRoomSensorDatabaseFactory() +{ + SupportedClass = UDungeonRoomSensorDatabase::StaticClass(); + bCreateNew = true; + bEditAfterNew = true; +} + +UObject* UDungeonRoomSensorDatabaseFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + return NewObject(InParent, InClass, InName, Flags); +} diff --git a/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseTypeActions.cpp b/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseTypeActions.cpp new file mode 100644 index 0000000..1973967 --- /dev/null +++ b/Source/DungeonGeneratorEditor/Private/SubActor/DungeonRoomSensorDatabaseTypeActions.cpp @@ -0,0 +1,34 @@ +/** +@author Shun Moriya +@copyright 2025- Shun Moriya +All Rights Reserved. +*/ + +#include "SubActor/DungeonRoomSensorDatabaseTypeActions.h" +#include "SubActor/DungeonRoomSensorDatabase.h" + +FDungeonRoomSensorDatabaseTypeActions::FDungeonRoomSensorDatabaseTypeActions(EAssetTypeCategories::Type InAssetCategory) + : mAssetCategory(InAssetCategory) +{ +} + +FText FDungeonRoomSensorDatabaseTypeActions::GetName() const +{ + return FText::FromName(TEXT("Room sensor database")); +} + +UClass* FDungeonRoomSensorDatabaseTypeActions::GetSupportedClass() const +{ + return UDungeonRoomSensorDatabase::StaticClass(); +} + +uint32 FDungeonRoomSensorDatabaseTypeActions::GetCategories() +{ + return mAssetCategory; +} + +FColor FDungeonRoomSensorDatabaseTypeActions::GetTypeColor() const +{ + static constexpr FColor Color(56, 156, 156); + return Color; +} diff --git a/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseFactory.h b/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseFactory.h new file mode 100644 index 0000000..a13d873 --- /dev/null +++ b/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseFactory.h @@ -0,0 +1,23 @@ +/** +@author Shun Moriya +@copyright 2025- Shun Moriya +All Rights Reserved. +*/ + +#pragma once +#include +#include +#include "DungeonRoomSensorDatabaseFactory.generated.h" + +UCLASS() +class DUNGEONGENERATOREDITOR_API UDungeonRoomSensorDatabaseFactory : public UFactory +{ + GENERATED_BODY() + +public: + UDungeonRoomSensorDatabaseFactory(); + virtual ~UDungeonRoomSensorDatabaseFactory() override = default; + + // UFactory overrides + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; +}; diff --git a/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseTypeActions.h b/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseTypeActions.h new file mode 100644 index 0000000..a89e1b8 --- /dev/null +++ b/Source/DungeonGeneratorEditor/Public/SubActor/DungeonRoomSensorDatabaseTypeActions.h @@ -0,0 +1,24 @@ +/** +@author Shun Moriya +@copyright 2025- Shun Moriya +All Rights Reserved. +*/ + +#pragma once +#include "DungeonAssetTypeActionsBase.h" + +class FDungeonRoomSensorDatabaseTypeActions : public FDungeonAssetTypeActionsBase +{ +public: + explicit FDungeonRoomSensorDatabaseTypeActions(EAssetTypeCategories::Type InAssetCategory); + virtual ~FDungeonRoomSensorDatabaseTypeActions() override = default; + + // IAssetTypeActions Implementation + virtual FText GetName() const override; + virtual UClass* GetSupportedClass() const override; + virtual uint32 GetCategories() override; + virtual FColor GetTypeColor() const override; + +private: + EAssetTypeCategories::Type mAssetCategory; +}; From 08096550bf00981077a73c49a0b394a77284d768 Mon Sep 17 00:00:00 2001 From: Shun Moriya <23472415+shun126@users.noreply.github.com> Date: Sat, 13 Sep 2025 01:35:25 +0900 Subject: [PATCH 2/2] Addressed to work even if mini-map is not yet supported. --- Content/Maps/Demonstration.umap | Bin 38541 -> 53010 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Content/Maps/Demonstration.umap b/Content/Maps/Demonstration.umap index c16bc8ce830b7a45ae3745515e394dc9f1b21f31..59bc59d99d722db451dceb70d6600d0a6c6dac46 100644 GIT binary patch literal 53010 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 literal 38541 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