Ajout du projet Depths sur Git

This commit is contained in:
2026-04-30 12:24:52 +02:00
commit a143ea22c7
6651 changed files with 77423 additions and 0 deletions
+521
View File
@@ -0,0 +1,521 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.8.1] - 2026-04-04
### Fixed
- Fixed auto-migration of `Door Type` variable from `Door` actor to its `Door Component`.
## [3.8.0] - 2026-04-02
### Added
- Added the `Door Interface` to be implemented on an actor or an actor component.
- Added a `Door Component` to add door logic and events on any actor (already implements `Door Interface`).
- Added `Dungeon` input to the `Room Constraint`'s `Check` function.
- Added a `Count Limit` built-in constraint, to limit the number of the room data generated in the dungeon.
### Changed
- Now, multiple bounding boxes can be defined for a room, allowing more freedom and creativity in your room design.
- You can add or remove boxes directly from the array in the `Room Data` asset.
- Edit them with the Dungeon Room editor mode.
- `Door` actors now uses the new `Door Component` for their logic.
- The doors' `Open`/`Lock` states are moved from the actors to the `Room Connection`, allowing you to lock or open individual doors earlier (e.g. during the `Initialize Dungeon` event).
### Fixed
- Fixed Linux compilation error
### Deprecated
- Deprecating `Door` actors as their logic has been moved into the `Door Component`. The `Door` actor class will be removed in a future version of the plugin.
To convert your existing doors, reparent them to the `Actor` class (or any other actor class you want) and add the `Door Component` on them. Then, bind to the events (like `On Open`, `On Lock`, etc.) to the component events instead.
- Deprecating `First Point` and `Second Point` variables of `Room Data` as they are superseded by the array `Bounding Boxes`. They will be removed in a future version of the plugin.
Conversion is automatically done on asset load, but to make it permanent and avoiding data loss when they will be removed from the plugin, you'll need to resave your assets after updating the plugin.
## [3.7.1] - 2026-02-04
### Fixed
- Fixed `Room Batch Size` setting not exposed to editor.
- Fixed `Actor Enter Room` event called instead of the `Actor Exit Room` event by the `Room Observer` component when an actor exits a room.
- Fixed random missing navmesh in some rooms after dungeon generation.
## [3.7.0] - 2025-11-16
### Added
- Added a `Get Level Room Data` node accessible from actors, even during their construction script, allowing the actor to setup itself when placed in the room.
- Added a new Data Asset called `Dungeon Settings` to allow overriding the `Room Unit` in the `Dungeon Generator` class.
- Added a compatibility list in `Door Type` assets, so different door types may be compatible
- Added `Try Place Room At Location` for custom dungeon algorithms to place a room without using a `Door Def` structure.
- Added a relevancy system for the rooms (independent from the room visibilities).
- You can set a distance from which a room is considered relevant to the player.
- A relevant room has a value to tell how close the player is to the room (the lower, the closest) with 0 the room where the player is in.
- Some new nodes in the `Room` instances are available to access this relevancy level, and an event dispatcher to be notified when this relevancy changes.
- Added room constraints:
- New base class `RoomConstraint` to create your own constraints.
- Added built-in location and direction constraints.
- Added new node `Does Pass All Constraints` to check whether a `Room Data` passes all its constraints.
- Added new node `Get Random Room Candidate` to ease the selection of a `RoomCandidate` struct, optionally using their scores as weights.
### Changed
- Replaced the hotfix for Steam multiplayer with a proper fix (this impact the saved dungeon, making it not compatible with previous plugin versions)
- Now the generation algorithm can be asynchronous:
- Updated the `Dungeon Generator Base` class to allow spreading the `Create Dungeon` work on multiple frames.
- Added a `Yield Generation` node to tell the generator to call the `Create Dungeon` again the next frame (must be used in the `Create Dungeon` function only).
- Updated the `Create Dungeon` function in `Dungeon Generator` to limit number of room generated per frame (can be set with `Room Batch Size` in the actor's details).
- Room Visibility system now handles properly local multiplayer (splitscreens)
- The `Get Visibility Pawn` overridable function now takes a `Player Controller` as input.
- `Filter and Sort Rooms` now checks if a room candidate passes all its constraints to include it in the output candidates.
### Fixed
- Fixed room visibilities to be updated properly when toggling or changing occlusion distance.
## [3.6.4] - 2025-09-30
### Fixed
- Fixed last compilation errors on Linux platforms
## [3.6.3] - 2025-09-18
### Fixed
- Fixed the saving/loading when the dungeon contains some null doors
- Removed a leftover warning message when creating the dungeon save system
- Hotfix for Steam multiplayer games (`Room Data` was garbage collected)
- Fixed a compilation error on Linux platforms
## [3.6.2] - 2025-08-31
### Fixed
- Fixed dungeon bounds rotation the proper way now (without breaking the `Room::GetBoundsExtents`)
## [3.6.1] - 2025-08-27
### Fixed
- Fixed debug dungeon bounds not properly rotated with a rotated dungeon.
- Fixed random crashes happening in editor or packaged game caused by a missing "Dungeon Graph" sub-object.
- Fixed `Count Connected Doors` node returning unconnected door count instead.
## [3.6.0] - 2025-06-09
### Added
- Custom dungeon generation algorithm now available in Blueprint too
- New `Filter And Sort Rooms` node that allows you to pick the best room and door index to fit in a specific place.
- Added several new blueprint nodes very useful when working with `Door Def` and `Vector Int`.
- Added a `Generation Success` event in the `Dungeon Generator` class, as opposite to the `Generation Failed` event.
- Internally introduced new cell-based type of room bounds, not fully used yet (only used for `Filter And Sort Rooms` at this time) but will allow great changes in the future.
### Changed
- Now the `Door Def` structure has read and write access in Blueprint, you can create them and update them by yourself.
- Now, `Room Connection` is accessible in Blueprint, no need to loop on room instances then door anymore.
- `Custom Room Data` instances are now created early in the generation process, allowing the use of them in the generation algorithm.
### Fixed
- Fixed a log message format when loading a dungeon from a save file.
## [3.5.1] - 2025-02-28
### Changed
- Updated the plugin description.
### Fixed
- Fixed crash on multiplayer when using subobject replication list (like in Lyra example project).
## [3.5.0] - 2025-01-28
### Added
- [EXPERIMENTAL] Added a save/load system for the dungeon, made to be easy to integrate in your save games.
- Added a `Deterministic Random` component to provide an easy access for an *independent* random stream in your actors, deterministic with the dungeon's seed.
- Added a `Simple Guid` component to easily provide a runtime guid for your actors, also compatible with the deterministic random component and the new experimental dungeon saving feature.
- Added a boolean in the `Dungeon Generator` to block interactions with the navigation system (no navmesh rebuild forced by the plugin).
- Added a boolean in the plugin settings to hide the debug drawing in game (display the debug drawings only in editor's viewport, not in PIE or packaged game)
- `Door Type` assets now have 2 new variables:
- A color variable to customize the color of the door's debug box.
- An offset variable to allow a different door offset per door type.
- Added some new nodes:
- A `Number of Room Between` node taking the `Read Only Room` interface directly as inputs.
- A `Get Room At` to retrieve a room at a certain dungeon cell.
- A node to convert a `Door Direction` enum into a `Int Vector`.
### Changed
- You can now search the functions in the `Rooms` variable directly in the generator, the variable getter will be created and linked automatically.
- Door debug colors are not used anymore to display the status of the doors (valid/invalid, can/can't place) because their color can be customized.
- The room's debug drawings are now always displayed when in the editor's room mode.
- Added `Room Instance` in inputs of the `Choose Door` function (the room data input is now deprecated in favor to `Get Room Data` of the room instance)
### Deprecated
- Deprecated the `Get Random Stream` node because the generator's random stream is not updated when using this node. You will need to use the `Random Stream` variable getter instead.
### Fixed
- Fixed multiple issues with multiplayer synchronization (e.g. when clients reconnect, etc.).
- Fixed compatibility of door actors with the `Room Visibility` components. Doors can now be used in room levels without visibility issues.
- Fixed UE 4.27 circular dependency error when compiling/packaging the game with the plugin.
- Fixed crash in editor's room mode when removing the `Room Data` while the Door tool is active.
## [3.4.1] - 2024-11-21
### Fixed
- Fixed compilation issues in UE 5.5
## [3.4.0] - 2024-10-18
### Added
- Added a room count limit in plugin settings to prevent some infinite loops during the dungeon generation.
- Added various utility functions:
- `Get Connected Room Index`, `Get Doors With` and `Get Room ID` in `Room` class.
- `Get Path Between` and `NumberOfRoomBetween` in `Dungeon Graph` class (variable `Rooms` in the dungeon generator).
- `Get Owning Room` and `Get Owning Room Custom Data` in a blueprint library (accessible from any actor/component placed in room levels).
- `Get Compatible Doors`, `Has Door of Type`, `Has Custom Data`, `Get Size` and `Get Volume` in `Door Data` class.
- `Added Level Component` field in `Room Custom Data` to allow attaching a component automatically on the `Room Level` blueprint script.
- Added a `Force Visibility` node in `Room` class to force the visibility even when the player is not in it (useful for e.g. cutscenes).
- Added `Get Visibility Pawn` overridable function in the `Dungeon Generator` class to customize which pawn is used for the room visibility (useful for e.g. spectating another player).
- Split the `Dungeon Generator` into a `Dungeon Generator Base` class, allowing to create custom generation algorithms (only C++ as of now).
- Added a `Read Only Room` interface to allow access to some variables during the generation process.
- Added an input `Current Room Instance` of type `Read Only Room` in `Choose Next Room`.
- Added an input `Room Instance` of type `Read Only Room` in `On Room Added`.
- Added a node Discard Room to explicitly tell we don't want to place a room in `Choose Next Room` (prevent error when returning null)
- Added `Auto Discard Room If Null` variable in `Dungeon Generator`
### Fixed
- Fixed player's components triggering room visibility even when ignoring the `Room Object Type`.
## [3.3.1] - 2024-07-18
### Fixed
- Fixed random crash on clients when unloading the dungeon.
- Fixed dungeon generator's begin initialization state triggered twice (missing break).
- Added back error logs on screen even when on screen debug is disabled in plugin.
- Now support push based replication and registered subobject lists (now compatible with Lyra example project + maybe compatible with Iris network subsystem).
- Fixed Door Tool in the Dungeon Room editor mode, to allow door removal and prevent door overlap when Room Unit is smaller than Door Size.
- Fixed debug cross of locked rooms not properly rotated when the room is rotated.
## [3.3.0] - 2024-06-23
### Added
- Added a blueprint node to select a random room with weights (non-uniform random).
- Added dungeon limits for room placement. If set, rooms will not be placed outside of the limits.
- Added a `Static Room Visibility` component with reduced computation over the non-static one (used for directly placed actors in room levels that will never go out of it at runtime).
- A tag `Ignore Room Culling` can be added to directly placed actors in room levels so they will be ignored by the room level automatic occlusion (before, only replicated actors were ignored). You can use a `(Static) Room Visibility` component to manage their visibility like you want instead.
- Added a `Get Progress` function in `Dungeon Generator` class to keep track of generation progress and make progress bars easily.
- Added an `On Fail To Add Room` event, called after exhausted all tries of placing a room without success.
- Added a `Cleanup Room` function in `Room Data` class as a counterpart of the `Initialize Room` one.
- Can now tweak the max generation tries and max room placement tries in the plugin's settings.
- Added accessor to generator's `Rooms` variable in C++ outside of child classes.
- The door debug box (blue) will now turn orange if multiple doors are on the same place.
- Now the `RoomData`'s level will be automatically filled if empty when the data is selected in the Dungeon Room Editor mode.
### Fixed
- Crash when running in new PIE window in 'Play as Client' when a DungeonGenerator actor is in the launched level.
- Fixed missing call to `On Room Added` event for the first room.
- Fixed `RootComponent` of door actors to be able to edit them in editor.
- Fixed door actors so that they always spawn, regardless of the actor's parameter.
## [3.2.2] - 2024-05-17
### Fixed
- Fixed warning `AddReferencedObject` for UE 5.3 and newer.
- Fixed UE 5.4 compilation issues
## [3.2.1] - 2024-05-13
### Fixed
- Fixed warning `TriggerType` name conflicting with the `Enhanced Input` system.
- Fixed compilation error due to un-initialized `FMargin3D` members.
- Fixed UE 5.4 PIE issues
## [3.2.0] - 2024-04-07
### Added
- Added console commands to temporarily tweak some plugin settings during runtime (even in packaged development game) and to generate/unload dungeons.
- Added blueprint nodes to access most of the plugin settings, or to change `Occlusion Culling` and `Occlusion Distance` values.
- Added a `Flipped` output in `Choose Door` function to choose which room the door is facing.
- Added `On Actor Enter Room` and `On Actor Exit Room` delegates in the `Room Level` blueprint.
- Added `Room Observer` components that automatically binds on those new delegates.
- Added some functions to the `Room` instances to know if doors are connected or to get connected `Room` instances.
- Added room bounds center and extent blueprint accessors in `Room` instances and `Room Level`.
- Added `Can Loop` boolean in `Dungeon Generator` to be able to toggle it per-actor.
- Added optional world meshes collision checks before placing a room during the generation process.
- Added a plugin setting to change the room trigger collision type.
- Added plugin settings to customize the door's debug arrow.
- Added door deletion in `Dungeon Room` editor mode when right-clicking on an existing door with the `Door` tool.
- Added utility buttons in the `Dungeon Room` editor mode to make selected box volumes fit in the room bounds, and to delete all invalid doors.
- Added an optional camera rotation pivot at the center of the room (when `Orbit Around Actor Selection` is enabled in your Editor Preferences).
### Fixed
- Fixed undo/redo in `Dungeon Room` editor mode.
- Fixed the widget mode (translate, rotate, scale) while in the `Dungeon Room` editor mode to have a better actor modification while staying in this mode.
- Fixed missing includes in C++ causing sometimes compilation issues.
### Deprecated
- Deprecated `Can Loop` in the plugin's settings. It will be removed in a future version. Leave it ticked and use the one in the `Dungeon Generator` actor instead.
## [3.1.2] - 2024-03-06
### Fixed
- Fixed crash when compiling room level blueprint after exiting the room editor mode.
- Fixed crash at runtime when the room data reference in the room level and the level reference in room data do not match.
- Fixed debug drawings of rooms and doors not displayed in packaged development game.
- Fixed room visibility not updated when occlusion culling settings are changed at runtime in packaged game.
- Fixed compilation errors caused by missing includes that could happen sometimes when using unity build.
## [3.1.1] - 2024-02-17
### Fixed
- Workaround for the occlusion of replicated actors placed directly in a room level.
- They are now ignored by the room level, so you can add the `Room Visibility Component` on them to manage their visibility with the rooms.
- Fixed door actors wrong rotation (flipped on Y axis).
- Fixed room visibilities not updated during PIE when toggling on/off the occlusion culling or changing the occlusion distance.
- Clamp occlusion distance to 1 to get at least a visible room (the one the player is in).
## [3.1.0] - 2024-01-28
### Added
- Added an `Unload` function in the `Dungeon Generator` actor.
- Added `Door Index` output in `Choose Next Room` function. [#39]
- Added `Room Visitor` interface to create custom behaviors on actors and components when an actor enters/exits a room.
- Added door accessors from the room instances in blueprint.
- Added accessor to the `Dungeon Generator`'s random stream from room instances in blueprint.
### Fixed
- Replaced the custom `ProceduralLevelStreaming` class with the `LevelStreamingDynamic` using the custom instance name.
- This should be transparent for the plugin's users, but the level instances should now have recent Unreal Engine's modifications on them (>UE4.26).
- Improved network performances with the use of `Net Dormancy` to notify the Unreal's network system only when there are changes in the dungeon or door state.
- Updated the `Room Visibility Component` to use the new `Room Visitor` interface (should be transparent for plugin's users).
## [3.0.1] - 2023-10-24
### Fixed
- Fixed UE 5.3 compilation issues [#35]
## [3.0.0] - 2023-10-14
### Added
- New editor mode to ease room creation.
- New asset category to speed up data asset creation.
- Added a door type system to allow various door sizes in the dungeon [#21] .
- Added per room instance custom data.
- Added an initialization step in the dungeon generator for the room instances to be initialized before `IsValidDungeon` being called.
- Added a function to check if the player can reach one room from another (no locked room between them) [#25].
- Added a `Spectate` blueprint node because it is not in the base unreal engine.
### Changed
- Moved room handling in a subclass of the `DungeonGenerator`, you can access most of the previous functions from the Rooms variable of the dungeon generator now.
- Reworked the dungeon networking: it does no longer use an RPC to generate the dungeon on all clients. Now the server generates the dungeon and replicates all rooms to the clients. The dungeon now uses a state machine to handle the generation process, allowing late joining clients to load properly the dungeon.
- Room bounds can now be in negative too.
- Room's "origin" (magenta sphere) is no longer shown since not really useful and a little confusing (can be enabled via a plugin's setting)
- New plugin icon.
### Fixed
- Fixed wrong seed value when the dungeon' seed is set to `Auto Increment`.
- Fixed `ContinueToAddRoom` not called with each `ChooseNextRoomData`.
## [2.1.2] - 2023-07-18
### Added
- Added an error message and the `GenerationFailed` event when the dungeon generator didn't generated a valid dungeon after exhausting all the retries (#29). Also now the dungeon will not spawn anymore in that case.
### Fixed
- Fixed occlusion culling system not disabled by the plugin's setting.
- Fixed compilation issues (#26).
- Fixed room triggers returned by "trace by channel" functions (#27).
## [2.1.1] - 2023-03-26
### Added
- Added `RoomData` assets validation to notify users when something is not correctly setup (levels and doors).
### Changed
- Make doors replicating over network, and thus usable in multiplayer games.
### Fixed
- Improved a little the `TriggerDoor` class with a native event to check which actors can trigger the door.
- Deprecated the `OpenDoor` and `CloseDoor` functions in favor to a unique `Open(bool)` function.
- Various fixes.
## [2.1.0] - 2023-03-11
### Added
- Added Dynamic actors occlusion culling
- Added visibility distance to occlusion culling system for room visibility
### Changed
- Greatly improved occlusion culling performance (using Octree instead of box overlapping)
- Improved debug drawings (draw a cross for not connected doors instead of arrows, draw current player's room in green instead of red)
### Deprecated
- Deprecated `RoomLockerBase` and exposed the locking state of rooms in room's level blueprints and door actors.
## [2.0.2] - 2022-09-11
### Added
- Added a DungeonGenerator parameter to place the dungeon's rooms on the actor location and rotation (default disabled)
### Fixed
- Removed static streaming level id, allowing multiple DungeonGenerator actors generating at the same time.
- Fixed blue bounding box for doors showing the offset in the door blueprint preview.
## [2.0.1] - 2021-11-13
### Added
- Added accessors in blueprint to get and set the seed
- Added back some utility functions to count and check existence with data types (in addition to data instances)
## [2.0.0] - 2021-11-01
### Changed
- Now use RoomData instances instead of blueprint classes
## [1.2.2] - 2021-08-19
### Changed
- Updated the year in the LICENSE file, and added the license notice in code files.
### Fixed
- Fixed crashes when returning none in ChooseFirstRoomData or ChooseNextRoomData.
## [1.2.1] - 2021-04-14
### Fixed
- Fixed packaging errors when installing the plugin in the engine. (now, blueprint only project can use the plugin)
## [1.2.0] - 2021-03-25
### Added
- Added an option to allow room loop in the dungeon.
### Fixed
- Fixed multiple instances of door at one door place.
## [1.1.0] - 2021-02-21
### Added
- Added the `Depth First` and `Breadth First` generation types.
- Added option to display the generation logs on screen too.
### Changed
- Refactored the DungeonGenerator code to allow override of functions in C++ as well.
## [1.0.3] - 2021-02-16
### Fixed
- Fixed a crash caused by a conflict between GC and the C++ native delete.
- Fixed a possible crash when too many rooms are generated (avoid a stack overflow from recursivity call to AddRoom)
## [1.0.2] - 2021-01-11
### Fixed
- Fixed enum EDirection ambiguity with RigLogic plugin.
## [1.0.1] - 2019-09-10
### Added
- Added Draw Debug toggle in the plugin settings.
### Fixed
- Fixed some bugs.
## [1.0.0] - 2019-05-30
### Added
- Initial Release
[3.7.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.7.1...v3.8.0
[3.7.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.6.4...v3.7.0
[3.6.4]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.6.3...v3.6.4
[3.6.3]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.6.2...v3.6.3
[3.6.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.6.1...v3.6.2
[3.6.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.6.0...v3.6.1
[3.6.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.5.1...v3.6.0
[3.5.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.5.0...v3.5.1
[3.5.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.4.1...v3.5.0
[3.4.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.4.0...v3.4.1
[3.4.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.3.1...v3.4.0
[3.3.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.3.0...v3.3.1
[3.3.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.2.2...v3.3.0
[3.2.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.2.1...v3.2.2
[3.2.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.2.0...v3.2.1
[3.2.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.1.2...v3.2.0
[3.1.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.1.1...v3.1.2
[3.1.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.1.0...v3.1.1
[3.1.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.0.1...v3.1.0
[3.0.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.1.2...v3.0.0
[2.1.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.1.1...v2.1.2
[2.1.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.1.0...v2.1.1
[2.1.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.0.2...v2.1.0
[2.0.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.0.1...v2.0.2
[2.0.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.2.2...v2.0.0
[1.2.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.2.1...v1.2.2
[1.2.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.0.3...v1.1.0
[1.0.3]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/BenPyton/ProceduralDungeon/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/BenPyton/ProceduralDungeon/releases/tag/v1.0.0
+489
View File
@@ -0,0 +1,489 @@
CeCILL-C FREE SOFTWARE LICENSE AGREEMENT
Notice
This Agreement is a Free Software license agreement that is the result
of discussions between its authors in order to ensure compliance with
the two main principles guiding its drafting:
* firstly, compliance with the principles governing the distribution
of Free Software: access to source code, broad rights granted to
users,
* secondly, the election of a governing law, French law, with which
it is conformant, both as regards the law of torts and
intellectual property law, and the protection that it offers to
both authors and holders of the economic rights over software.
The authors of the CeCILL-C (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre])
license are:
Commissariat à l'Energie Atomique - CEA, a public scientific, technical
and industrial research establishment, having its principal place of
business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France.
Centre National de la Recherche Scientifique - CNRS, a public scientific
and technological establishment, having its principal place of business
at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
Institut National de Recherche en Informatique et en Automatique -
INRIA, a public scientific and technological establishment, having its
principal place of business at Domaine de Voluceau, Rocquencourt, BP
105, 78153 Le Chesnay cedex, France.
Preamble
The purpose of this Free Software license agreement is to grant users
the right to modify and re-use the software governed by this license.
The exercising of this right is conditional upon the obligation to make
available to the community the modifications made to the source code of
the software so as to contribute to its evolution.
In consideration of access to the source code and the rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty and the software's author, the holder of the
economic rights, and the successive licensors only have limited liability.
In this respect, the risks associated with loading, using, modifying
and/or developing or reproducing the software by the user are brought to
the user's attention, given its Free Software status, which may make it
complicated to use, with the result that its use is reserved for
developers and experienced professionals having in-depth computer
knowledge. Users are therefore encouraged to load and test the
suitability of the software as regards their requirements in conditions
enabling the security of their systems and/or data to be ensured and,
more generally, to use and operate it in the same conditions of
security. This Agreement may be freely reproduced and published,
provided it is not altered, and that no provisions are either added or
removed herefrom.
This Agreement may apply to any or all software for which the holder of
the economic rights decides to submit the use thereof to its provisions.
Article 1 - DEFINITIONS
For the purpose of this Agreement, when the following expressions
commence with a capital letter, they shall have the following meaning:
Agreement: means this license agreement, and its possible subsequent
versions and annexes.
Software: means the software in its Object Code and/or Source Code form
and, where applicable, its documentation, "as is" when the Licensee
accepts the Agreement.
Initial Software: means the Software in its Source Code and possibly its
Object Code form and, where applicable, its documentation, "as is" when
it is first distributed under the terms and conditions of the Agreement.
Modified Software: means the Software modified by at least one
Integrated Contribution.
Source Code: means all the Software's instructions and program lines to
which access is required so as to modify the Software.
Object Code: means the binary files originating from the compilation of
the Source Code.
Holder: means the holder(s) of the economic rights over the Initial
Software.
Licensee: means the Software user(s) having accepted the Agreement.
Contributor: means a Licensee having made at least one Integrated
Contribution.
Licensor: means the Holder, or any other individual or legal entity, who
distributes the Software under the Agreement.
Integrated Contribution: means any or all modifications, corrections,
translations, adaptations and/or new functions integrated into the
Source Code by any or all Contributors.
Related Module: means a set of sources files including their
documentation that, without modification to the Source Code, enables
supplementary functions or services in addition to those offered by the
Software.
Derivative Software: means any combination of the Software, modified or
not, and of a Related Module.
Parties: mean both the Licensee and the Licensor.
These expressions may be used both in singular and plural form.
Article 2 - PURPOSE
The purpose of the Agreement is the grant by the Licensor to the
Licensee of a non-exclusive, transferable and worldwide license for the
Software as set forth in Article 5 hereinafter for the whole term of the
protection granted by the rights over said Software.
Article 3 - ACCEPTANCE
3.1 The Licensee shall be deemed as having accepted the terms and
conditions of this Agreement upon the occurrence of the first of the
following events:
* (i) loading the Software by any or all means, notably, by
downloading from a remote server, or by loading from a physical
medium;
* (ii) the first time the Licensee exercises any of the rights
granted hereunder.
3.2 One copy of the Agreement, containing a notice relating to the
characteristics of the Software, to the limited warranty, and to the
fact that its use is restricted to experienced users has been provided
to the Licensee prior to its acceptance as set forth in Article 3.1
hereinabove, and the Licensee hereby acknowledges that it has read and
understood it.
Article 4 - EFFECTIVE DATE AND TERM
4.1 EFFECTIVE DATE
The Agreement shall become effective on the date when it is accepted by
the Licensee as set forth in Article 3.1.
4.2 TERM
The Agreement shall remain in force for the entire legal term of
protection of the economic rights over the Software.
Article 5 - SCOPE OF RIGHTS GRANTED
The Licensor hereby grants to the Licensee, who accepts, the following
rights over the Software for any or all use, and for the term of the
Agreement, on the basis of the terms and conditions set forth hereinafter.
Besides, if the Licensor owns or comes to own one or more patents
protecting all or part of the functions of the Software or of its
components, the Licensor undertakes not to enforce the rights granted by
these patents against successive Licensees using, exploiting or
modifying the Software. If these patents are transferred, the Licensor
undertakes to have the transferees subscribe to the obligations set
forth in this paragraph.
5.1 RIGHT OF USE
The Licensee is authorized to use the Software, without any limitation
as to its fields of application, with it being hereinafter specified
that this comprises:
1. permanent or temporary reproduction of all or part of the Software
by any or all means and in any or all form.
2. loading, displaying, running, or storing the Software on any or
all medium.
3. entitlement to observe, study or test its operation so as to
determine the ideas and principles behind any or all constituent
elements of said Software. This shall apply when the Licensee
carries out any or all loading, displaying, running, transmission
or storage operation as regards the Software, that it is entitled
to carry out hereunder.
5.2 RIGHT OF MODIFICATION
The right of modification includes the right to translate, adapt,
arrange, or make any or all modifications to the Software, and the right
to reproduce the resulting software. It includes, in particular, the
right to create a Derivative Software.
The Licensee is authorized to make any or all modification to the
Software provided that it includes an explicit notice that it is the
author of said modification and indicates the date of the creation thereof.
5.3 RIGHT OF DISTRIBUTION
In particular, the right of distribution includes the right to publish,
transmit and communicate the Software to the general public on any or
all medium, and by any or all means, and the right to market, either in
consideration of a fee, or free of charge, one or more copies of the
Software by any means.
The Licensee is further authorized to distribute copies of the modified
or unmodified Software to third parties according to the terms and
conditions set forth hereinafter.
5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
The Licensee is authorized to distribute true copies of the Software in
Source Code or Object Code form, provided that said distribution
complies with all the provisions of the Agreement and is accompanied by:
1. a copy of the Agreement,
2. a notice relating to the limitation of both the Licensor's
warranty and liability as set forth in Articles 8 and 9,
and that, in the event that only the Object Code of the Software is
redistributed, the Licensee allows effective access to the full Source
Code of the Software at a minimum during the entire period of its
distribution of the Software, it being understood that the additional
cost of acquiring the Source Code shall not exceed the cost of
transferring the data.
5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
When the Licensee makes an Integrated Contribution to the Software, the
terms and conditions for the distribution of the resulting Modified
Software become subject to all the provisions of this Agreement.
The Licensee is authorized to distribute the Modified Software, in
source code or object code form, provided that said distribution
complies with all the provisions of the Agreement and is accompanied by:
1. a copy of the Agreement,
2. a notice relating to the limitation of both the Licensor's
warranty and liability as set forth in Articles 8 and 9,
and that, in the event that only the object code of the Modified
Software is redistributed, the Licensee allows effective access to the
full source code of the Modified Software at a minimum during the entire
period of its distribution of the Modified Software, it being understood
that the additional cost of acquiring the source code shall not exceed
the cost of transferring the data.
5.3.3 DISTRIBUTION OF DERIVATIVE SOFTWARE
When the Licensee creates Derivative Software, this Derivative Software
may be distributed under a license agreement other than this Agreement,
subject to compliance with the requirement to include a notice
concerning the rights over the Software as defined in Article 6.4.
In the event the creation of the Derivative Software required modification
of the Source Code, the Licensee undertakes that:
1. the resulting Modified Software will be governed by this Agreement,
2. the Integrated Contributions in the resulting Modified Software
will be clearly identified and documented,
3. the Licensee will allow effective access to the source code of the
Modified Software, at a minimum during the entire period of
distribution of the Derivative Software, such that such
modifications may be carried over in a subsequent version of the
Software; it being understood that the additional cost of
purchasing the source code of the Modified Software shall not
exceed the cost of transferring the data.
5.3.4 COMPATIBILITY WITH THE CeCILL LICENSE
When a Modified Software contains an Integrated Contribution subject to
the CeCILL license agreement, or when a Derivative Software contains a
Related Module subject to the CeCILL license agreement, the provisions
set forth in the third item of Article 6.4 are optional.
Article 6 - INTELLECTUAL PROPERTY
6.1 OVER THE INITIAL SOFTWARE
The Holder owns the economic rights over the Initial Software. Any or
all use of the Initial Software is subject to compliance with the terms
and conditions under which the Holder has elected to distribute its work
and no one shall be entitled to modify the terms and conditions for the
distribution of said Initial Software.
The Holder undertakes that the Initial Software will remain ruled at
least by this Agreement, for the duration set forth in Article 4.2.
6.2 OVER THE INTEGRATED CONTRIBUTIONS
The Licensee who develops an Integrated Contribution is the owner of the
intellectual property rights over this Contribution as defined by
applicable law.
6.3 OVER THE RELATED MODULES
The Licensee who develops a Related Module is the owner of the
intellectual property rights over this Related Module as defined by
applicable law and is free to choose the type of agreement that shall
govern its distribution under the conditions defined in Article 5.3.3.
6.4 NOTICE OF RIGHTS
The Licensee expressly undertakes:
1. not to remove, or modify, in any manner, the intellectual property
notices attached to the Software;
2. to reproduce said notices, in an identical manner, in the copies
of the Software modified or not;
3. to ensure that use of the Software, its intellectual property
notices and the fact that it is governed by the Agreement is
indicated in a text that is easily accessible, specifically from
the interface of any Derivative Software.
The Licensee undertakes not to directly or indirectly infringe the
intellectual property rights of the Holder and/or Contributors on the
Software and to take, where applicable, vis-à-vis its staff, any and all
measures required to ensure respect of said intellectual property rights
of the Holder and/or Contributors.
Article 7 - RELATED SERVICES
7.1 Under no circumstances shall the Agreement oblige the Licensor to
provide technical assistance or maintenance services for the Software.
However, the Licensor is entitled to offer this type of services. The
terms and conditions of such technical assistance, and/or such
maintenance, shall be set forth in a separate instrument. Only the
Licensor offering said maintenance and/or technical assistance services
shall incur liability therefor.
7.2 Similarly, any Licensor is entitled to offer to its licensees, under
its sole responsibility, a warranty, that shall only be binding upon
itself, for the redistribution of the Software and/or the Modified
Software, under terms and conditions that it is free to decide. Said
warranty, and the financial terms and conditions of its application,
shall be subject of a separate instrument executed between the Licensor
and the Licensee.
Article 8 - LIABILITY
8.1 Subject to the provisions of Article 8.2, the Licensee shall be
entitled to claim compensation for any direct loss it may have suffered
from the Software as a result of a fault on the part of the relevant
Licensor, subject to providing evidence thereof.
8.2 The Licensor's liability is limited to the commitments made under
this Agreement and shall not be incurred as a result of in particular:
(i) loss due the Licensee's total or partial failure to fulfill its
obligations, (ii) direct or consequential loss that is suffered by the
Licensee due to the use or performance of the Software, and (iii) more
generally, any consequential loss. In particular the Parties expressly
agree that any or all pecuniary or business loss (i.e. loss of data,
loss of profits, operating loss, loss of customers or orders,
opportunity cost, any disturbance to business activities) or any or all
legal proceedings instituted against the Licensee by a third party,
shall constitute consequential loss and shall not provide entitlement to
any or all compensation from the Licensor.
Article 9 - WARRANTY
9.1 The Licensee acknowledges that the scientific and technical
state-of-the-art when the Software was distributed did not enable all
possible uses to be tested and verified, nor for the presence of
possible defects to be detected. In this respect, the Licensee's
attention has been drawn to the risks associated with loading, using,
modifying and/or developing and reproducing the Software which are
reserved for experienced users.
The Licensee shall be responsible for verifying, by any or all means,
the suitability of the product for its requirements, its good working
order, and for ensuring that it shall not cause damage to either persons
or properties.
9.2 The Licensor hereby represents, in good faith, that it is entitled
to grant all the rights over the Software (including in particular the
rights set forth in Article 5).
9.3 The Licensee acknowledges that the Software is supplied "as is" by
the Licensor without any other express or tacit warranty, other than
that provided for in Article 9.2 and, in particular, without any warranty
as to its commercial value, its secured, safe, innovative or relevant
nature.
Specifically, the Licensor does not warrant that the Software is free
from any error, that it will operate without interruption, that it will
be compatible with the Licensee's own equipment and software
configuration, nor that it will meet the Licensee's requirements.
9.4 The Licensor does not either expressly or tacitly warrant that the
Software does not infringe any third party intellectual property right
relating to a patent, software or any other property right. Therefore,
the Licensor disclaims any and all liability towards the Licensee
arising out of any or all proceedings for infringement that may be
instituted in respect of the use, modification and redistribution of the
Software. Nevertheless, should such proceedings be instituted against
the Licensee, the Licensor shall provide it with technical and legal
assistance for its defense. Such technical and legal assistance shall be
decided on a case-by-case basis between the relevant Licensor and the
Licensee pursuant to a memorandum of understanding. The Licensor
disclaims any and all liability as regards the Licensee's use of the
name of the Software. No warranty is given as regards the existence of
prior rights over the name of the Software or as regards the existence
of a trademark.
Article 10 - TERMINATION
10.1 In the event of a breach by the Licensee of its obligations
hereunder, the Licensor may automatically terminate this Agreement
thirty (30) days after notice has been sent to the Licensee and has
remained ineffective.
10.2 A Licensee whose Agreement is terminated shall no longer be
authorized to use, modify or distribute the Software. However, any
licenses that it may have granted prior to termination of the Agreement
shall remain valid subject to their having been granted in compliance
with the terms and conditions hereof.
Article 11 - MISCELLANEOUS
11.1 EXCUSABLE EVENTS
Neither Party shall be liable for any or all delay, or failure to
perform the Agreement, that may be attributable to an event of force
majeure, an act of God or an outside cause, such as defective
functioning or interruptions of the electricity or telecommunications
networks, network paralysis following a virus attack, intervention by
government authorities, natural disasters, water damage, earthquakes,
fire, explosions, strikes and labor unrest, war, etc.
11.2 Any failure by either Party, on one or more occasions, to invoke
one or more of the provisions hereof, shall under no circumstances be
interpreted as being a waiver by the interested Party of its right to
invoke said provision(s) subsequently.
11.3 The Agreement cancels and replaces any or all previous agreements,
whether written or oral, between the Parties and having the same
purpose, and constitutes the entirety of the agreement between said
Parties concerning said purpose. No supplement or modification to the
terms and conditions hereof shall be effective as between the Parties
unless it is made in writing and signed by their duly authorized
representatives.
11.4 In the event that one or more of the provisions hereof were to
conflict with a current or future applicable act or legislative text,
said act or legislative text shall prevail, and the Parties shall make
the necessary amendments so as to comply with said act or legislative
text. All other provisions shall remain effective. Similarly, invalidity
of a provision of the Agreement, for any reason whatsoever, shall not
cause the Agreement as a whole to be invalid.
11.5 LANGUAGE
The Agreement is drafted in both French and English and both versions
are deemed authentic.
Article 12 - NEW VERSIONS OF THE AGREEMENT
12.1 Any person is authorized to duplicate and distribute copies of this
Agreement.
12.2 So as to ensure coherence, the wording of this Agreement is
protected and may only be modified by the authors of the License, who
reserve the right to periodically publish updates or new versions of the
Agreement, each with a separate number. These subsequent versions may
address new issues encountered by Free Software.
12.3 Any Software distributed under a given version of the Agreement may
only be subsequently distributed under the same version of the Agreement
or a subsequent version.
Article 13 - GOVERNING LAW AND JURISDICTION
13.1 The Agreement is governed by French law. The Parties agree to
endeavor to seek an amicable solution to any disagreements or disputes
that may arise during the performance of the Agreement.
13.2 Failing an amicable solution within two (2) months as from their
occurrence, and unless emergency proceedings are necessary, the
disagreements or disputes shall be referred to the Paris Courts having
jurisdiction, by the more diligent Party.
Version 1.0 dated 2006-09-05.
+11
View File
@@ -0,0 +1,11 @@
# Multi-Licensing Terms v1.0
This software ("Software") is available under the following licenses, with only one license applying based on the source from which you obtain it. By using this Software, you agree to the terms of the applicable license based on the source from which you obtained it. If you do not accept the terms of the applicable license, you must refrain from using the Software.
The order of precedence from highest to lowest is as follows:
1. **Fab EULA**: Applicable when obtained from Fab (<https://fab.com>). Full terms are available at <https://fab.com/eula>.
2. **CeCILL-C License**: Applicable when obtained from any source not explicitly designated above as having an alternative license (this designation may be updated in future versions of this Agreement). Full terms are available in the `LICENSE-CeCILL-C` file included with this Software or at <https://cecill.info/licences/Licence_CeCILL-C_V1-en.html>.
For questions regarding this Multi-Licensing Terms or the licenses, please contact us at <contact@chat-cripant.com>.
@@ -0,0 +1,40 @@
{
"FileVersion": 3,
"Version": 30801,
"VersionName": "3.8.1",
"FriendlyName": "Procedural Dungeon",
"Description": "Create rich and diverse dungeon experiences that combine the best of both worlds: the creativity and precision of handmade room designs, paired with the unpredictability and excitement of procedural generation.",
"Category": "Procedural",
"CreatedBy": "Ben Pyton",
"CreatedByURL": "https://chat-cripant.com",
"DocsURL": "https://BenPyton.github.io/ProceduralDungeon",
"MarketplaceURL": "",
"SupportURL": "https://github.com/BenPyton/ProceduralDungeon/issues",
"EngineVersion": "5.7.0",
"CanContainContent": false,
"Installed": true,
"Modules": [
{
"Name": "ProceduralDungeon",
"Type": "Runtime",
"LoadingPhase": "PreDefault",
"PlatformAllowList": [
"Win64",
"Mac",
"Linux",
"IOS",
"Android"
]
},
{
"Name": "ProceduralDungeonEditor",
"Type": "Editor",
"LoadingPhase": "Default",
"PlatformAllowList": [
"Win64",
"Mac",
"Linux"
]
}
]
}
+187
View File
@@ -0,0 +1,187 @@
# Procedural Dungeon Plugin
[![Plugin version number](https://img.shields.io/github/v/release/BenPyton/ProceduralDungeon?label=Version)](https://github.com/BenPyton/ProceduralDungeon/releases/latest)
[![Unreal Engine Supported Versions](https://img.shields.io/badge/Unreal_Engine-4.27_%7C_5.3_%7C_5.4_%7C_5.5_%7C_5.6_%7C_5.7-9455CE?logo=unrealengine)](https://github.com/BenPyton/ProceduralDungeon/releases)
[![License](https://img.shields.io/badge/License-CeCILL--C_or_Fab_EULA-blue)](LICENSE)
![Download count](https://img.shields.io/github/downloads/BenPyton/ProceduralDungeon/total?label=Downloads)
[![Actively Maintained](https://img.shields.io/badge/Maintenance%20Level-Actively%20Maintained-green.svg)](https://gist.github.com/cheerfulstoic/d107229326a01ff0f333a1d3476e068d)
[![Discord](https://img.shields.io/discord/1182461404092055574?logo=discord&logoColor=white&label=Discord&color=%235865F2)][Discord]
## Overview
This Unreal Engine plugin allows you to create rich, diverse dungeon experiences that combine the best of both worlds: the creativity and precision of handmade room designs, paired with the unpredictability and excitement of procedural generation.
Rooms are handmade Unreal levels instanced in the world.
You define their size and their doors as well as several other parameters.
You code your own rules in blueprints or C++, with great flexibility and customization, to generate the dungeons ***you*** want.
You can get this plugin on [Fab](https://www.fab.com/fr/listings/62cbdceb-ae3c-4fe4-adac-39d751a15df9) too!
If you have any bug or crash, please [open an issue](https://github.com/BenPyton/proceduralDungeon/issues/new?template=bug_report.yml) in the Github repo.\
If you have suggestions, questions or need help to use the plugin you can join the [Discord server][Discord] dedicated to this plugin.\
If you want to contribute, feel free to create a pull request (*contributions to the wiki are also welcomed!*).
## Features
- Handcrafted rooms, giving full control of their design by the level designers.
- Generation rules defined in blueprint or C++, allowing flexible and powerful procedural generation.
- A new editor mode to ease the creation and edition of the rooms.
- Several interfaces and components for your actors (RoomVisitor, RoomObserver, DeterministicRandom, etc.)
- Different door types, allowing more complex dungeons.
- Optional room culling system, allowing to render only the relevant rooms to the player.
- Ready for multiplayer games (push model and subobject lists are implemented).
- :construction:[^experimental] Save/Load nodes for the dungeon, easy to use with any game save system (blueprint or C++)
[^experimental]: :construction: : Experimental features
## Example
Some hand-made rooms defined with bounds and doors:\
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomSpawn.gif" alt="Animated GIF" width="125"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomA.gif" alt="Animated GIF" width="125"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomB.gif" alt="Animated GIF" width="125"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomC.gif" alt="Animated GIF" width="125"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomD.gif" alt="Animated GIF" width="125"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonDemo_RoomExit.gif" alt="Animated GIF" width="125"/>
Some extremely simple generation rules:
- A special room (red) used to spawn the player.
- Then 10 rooms chosen randomly in a list (blue, green, yellow, cyan).
- Then a special room (purple) used as an end goal for the player.
- Finally 10 other rooms chosen randomly.
Possible results:\
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonResult.gif" alt="Animated GIF" width="400"/>
<img src="https://github.com/BenPyton/ProceduralDungeon/wiki/Images/ProceduralDungeonResult2.gif" alt="Animated GIF" width="400"/>
[Demo Video on Youtube](http://www.youtube.com/watch?v=DmyNEd0YtDE "Procedural Dungeon Demo")<br>
You can find an example project [here](https://github.com/BenPyton/DungeonExample) too.
## How to use it
Follow the [Getting Started guide on the wiki](https://benpyton.github.io/ProceduralDungeon/guides/Introduction) to start working with the plugin.
If you want more details about how it works internally, you can read the [wiki](https://benpyton.github.io/ProceduralDungeon/guides/Home).
You have also access in the wiki to all the exposed [classes and nodes](https://benpyton.github.io/ProceduralDungeon/api) in Blueprint.
You can also join the [Discord server][Discord] dedicated to this plugin if you want to ask question or get help from the community.
## Installation
Install it like any other Unreal Engine plugin.
If you have any trouble with installation, read the [Installation](https://benpyton.github.io/ProceduralDungeon/guides/Getting-Started/Installation) page of the wiki.
## FAQ
<details>
<summary><b>Can I generate levels during runtime? What I mean is if I can generate a new dungeon while the player is in it.</b></summary>
> Yes, you can generate during runtime.\
> If you call the `Generate` function, then the previous rooms unload, and a new dungeon generate and load new rooms.\
> There is no map travel during the process, the player remains in the master map, only the dungeon's rooms are loaded/unloaded.
</details>
<details>
<summary><b>How large can I make the map?</b></summary>
> You are only limited by the performance of the machine your game runs on.\
> Mostly, the performance of the dungeon depends on the complexity of your rooms/meshes, and the hardware of your computer. The more details and diversity of actors there are, the more resources will be consumed on the computer.\
> To be able to generate a very large map, you will need to optimize the meshes/textures for the RAM and GPU, the collisions and number of dynamic actors (enemies, etc.) for CPU, etc.\
> The simple occlusion culling system I provide in the plugin is one (rudimentary) way to optimize the GPU side (less drawing).
> It is far from perfect but a good start.\
> You will need to do the other optimizations yourself.
</details>
<details>
<summary><b>Can I save and load dungeons?</b></summary>
> Since version 3.5 of the plugin, There are [some nodes](https://benpyton.github.io/ProceduralDungeon/guides/Advanced-Features/Saving-Dungeon) to help you easily setup a save/load of the dungeon.
> It'll need some works on your side but it is definitely possible to do it.\
> The dungeon save should be compatible with any save system you are using.
> In C++ you can also use some functions for archive-based save systems ( `StructuredArchive` compatible too).
</details>
<details>
<summary><b>How many different rooms can I have?</b></summary>
> You can have the number of room you want, there is not really a limit in the plugin.
</details>
<details>
<summary><b>It is pretty much up to my creativity to design whatever I want, right? If I want rooms to have enemies or anything like that, I can just create it in the level, right?</b></summary>
> Yes, you can design everything you want in the room. It is the purpose of the plugin: providing a generic way to generate a dungeon, without any compromise on the DA nor the game design.\
> If you don't want to create the rooms manually, you may use other procedural plugins (like PCG) to create the content of the rooms (I've never tested that myself though).
</details>
<details>
<summary><b>How does the room culling work for multiplayer?</b></summary>
> The room culling system built in the plugin is client side. It will show only the room where the local player is and any adjacent rooms.\
> You can read further details about the room culling system of this plugin from the [wiki page](https://benpyton.github.io/ProceduralDungeon/guides/Advanced-Features/Occlusion-Culling).\
> You can also disable the room culling system from the [plugin's settings](https://benpyton.github.io/ProceduralDungeon/guides/Getting-Started/Plugin-Settings) and do it yourself in another way.
</details>
<details>
<summary><b>Is there a seed?</b></summary>
> Yes, there is a seed for the dungeon generation.\
> I made a parameter in the [`DungeonGenerator`](https://benpyton.github.io/ProceduralDungeon/guides/Getting-Started/Generating-Dungeon/Dungeon-Generator#seed-type) actor to have different types of seed:
>
> - You can have a fixed seed you can set in the actor which will be always used (useful for testing and debugging purpose, or to set manually the seed in Blueprint or C++).
> - You can have an incrementing seed, using the fixed seed for the first generation, then adding a value to it at each generation (useful for demonstration purpose).
> - You can have a random seed generated for each generation (for released game mostly, or to test quickly a lot of dungeon generations).
</details>
<details>
<summary><b>Can I have some sort of flow to the dungeon? Like have a secret room spawn only once and have boss rooms only spawn 4 rooms?</b></summary>
> Yes, you can define the flow you want for your dungeon. It is the purpose of the plugin.\
> There is the function [`ChooseNextRoomData`][ChooseNextRoom] where you define what I call your "rules" of the dungeon.\
> You can, for example, check a minimum number of room before spawning a secret room, and then don't spawn it if you already have one in the dungeon.\
> If you need help on how to define your dungeon rules, you can check this [example](https://benpyton.github.io/ProceduralDungeon/guides/Best-Practices/Workflows/Dungeon-Generation-Algorithm) and get help on the [Discord server][Discord] dedicated to this plugin.
</details>
<details>
<summary><b>Can I increase the difficulty of the dungeon? Lets say room level 1 is easy and room level 5 is hard, can I tell the dungeon to not go from level 1 to level 5?</b></summary>
> Of course, you can do that sort of thing!
> For this difficulty example, you should create a child blueprint of `RoomData` class to add new parameters like a `DifficultyLevel`, which you can set a different value for each room in your `RoomData` assets.\
> Then for example, in your [`ChooseNextRoomData`][ChooseNextRoom] function you can choose a room depending on its difficulty level compared to the difficulty level of the previous room.
</details>
## License
This plugin is available under different licenses depending on the source from which it was obtained:
- The [Fab EULA](https://fab.com/eula) applies when obtained from the Fab marketplace.
- The [CeCILL-C license](https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
Please refer to the accompanying [LICENSE file](LICENSE.md) for further details.
You can also take a look in [the wiki](https://benpyton.github.io/ProceduralDungeon/guides/Copyrights-and-Licenses) for the differences in the license terms.
## *Support Me*
If you like my plugin, please consider leaving a tip, it would mean so much to me! 😊
[![Ko-fi](https://img.shields.io/badge/Ko--fi-FF5E5B?logo=kofi&logoColor=fff&style=for-the-badge)](https://ko-fi.com/M4M3NW2JV)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-FFDD00?logo=buymeacoffee&logoColor=000&style=for-the-badge)](https://buymeacoffee.com/benpyton)
[![Liberapay](https://img.shields.io/badge/Liberapay-F6C915?logo=liberapay&logoColor=000&style=for-the-badge)](https://liberapay.com/BenPyton/donate)
[![PayPal](https://img.shields.io/badge/PayPal-003087?logo=paypal&logoColor=fff&style=for-the-badge)](https://www.paypal.com/donate/?hosted_button_id=9VWP66JU5DZXN)
[Discord]: https://discord.gg/YE2dPda2CC
[ChooseNextRoom]: https://benpyton.github.io/ProceduralDungeon/guides/Getting-Started/Generating-Dungeon/Choose-Next-Room-Data
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "BoundsParams.h"
FBoxMinAndMax FBoundsParams::GetBox() const
{
return FBoxMinAndMax(
FIntVector(
(bLimitMinX) ? -MinX : INT32_MIN,
(bLimitMinY) ? -MinY : INT32_MIN,
(bLimitMinZ) ? -MinZ : INT32_MIN
),
FIntVector(
(bLimitMaxX) ? MaxX + 1 : INT32_MAX,
(bLimitMaxY) ? MaxY + 1 : INT32_MAX,
(bLimitMaxZ) ? MaxZ + 1 : INT32_MAX
)
);
}
@@ -0,0 +1,75 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/DeterministicRandomComponent.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "DungeonBlueprintLibrary.h"
#include "Room.h"
#include "Interfaces/RoomActorGuid.h"
#include "DungeonGeneratorBase.h"
namespace
{
// Utility functions to xor 2 guids
FGuid operator^(const FGuid& A, const FGuid& B)
{
return FGuid(A.A ^ B.A, A.B ^ B.B, A.C ^ B.C, A.D ^ B.D);
}
void operator^=(FGuid& A, const FGuid& B)
{
A = A ^ B;
}
} //namespace
UDeterministicRandomComponent::UDeterministicRandomComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UDeterministicRandomComponent::OnRegister()
{
Super::OnRegister();
AActor* OwnerActor = GetOwner();
const int32 Seed = GenerateDeterministicSeed(OwnerActor);
Random.Initialize(Seed);
DungeonLog_Debug("[%s] Initial seed set to: %d.", *GetNameSafe(OwnerActor), Random.GetInitialSeed());
}
// *WARNING*: Updating the algorithm may break systems relying on this generated seed.
// The best approach is to not rely on this generated seed.
int32 UDeterministicRandomComponent::GenerateDeterministicSeed(AActor* Actor)
{
FGuid Guid;
int64 Salt = 0;
// Use the guid of the actor if available.
UObject* GuidImplementer = IRoomActorGuid::GetImplementer(Actor);
if (IsValid(GuidImplementer))
{
Guid = IRoomActorGuid::Execute_GetGuid(GuidImplementer);
}
// Get the room ID as the salt if available.
if (const URoom* OwningRoom = UDungeonBlueprintLibrary::GetOwningRoom(Actor))
{
Salt = OwningRoom->GetRoomID();
// Will also use the generator guid and seed if relevant.
if (const ADungeonGeneratorBase* Generator = OwningRoom->Generator())
{
Guid ^= Generator->GetGuid();
const int64 Seed = static_cast<int64>(Generator->GetSeed());
Salt ^= Seed << 32;
}
}
return Random::Guid2Seed(Guid, Salt);
}
@@ -0,0 +1,185 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/DoorComponent.h"
#include "Room.h"
#include "RoomLevel.h"
#include "RoomConnection.h"
#include "DrawDebugHelpers.h"
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
#include "DungeonGenerator.h"
#include "DoorType.h"
#include "ProceduralDungeonUtils.h"
#include "Utils/ReplicationUtils.h"
#include "ProceduralDungeonLog.h"
UDoorComponent::UDoorComponent()
{
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
SetIsReplicatedByDefault(true);
}
void UDoorComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
FDoRepLifetimeParams Params;
Params.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS(UDoorComponent, RoomConnection, Params);
}
// Called every frame
void UDoorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
const URoom* RoomA = GetRoomA();
const URoom* RoomB = GetRoomB();
// Tells if the door actor has been spawned by the dungeon generator or not.
// At least one of the room is valid when spawned by the dungeon generator.
// Both rooms are invalid if door has been spawned by another way.
const bool bSpawnedByDungeon = IsValid(RoomA) || IsValid(RoomB);
// The door manages itself its own visibility only when it has been spawned by the dungeon generator.
// If the door is placed in a RoomLevel or spawned by the user in other means, it is the responsibility
// of the RoomLevel or the user to manage the door's visibility.
if (bSpawnedByDungeon)
{
const bool bRoomAVisible = IsValid(RoomA) && RoomA->IsVisible();
const bool bRoomBVisible = IsValid(RoomB) && RoomB->IsVisible();
// Update door visibility
// A door is hidden ONLY when ALL those conditions are met:
// - The Room Culling is enabled.
// - The door is not `Always Visible`.
// - Both connected rooms are not visible.
// @TODO: this should not work with multiplayer games, because bHidden is replicated!
// It works only because it is updated each frame on clients too!
// The behavior will change if bHidden is updated once in a wile by an event instead!
// So, I should find another way to hide the actor... (avoiding if possible RootComponent::SetVisible)
AActor* Owner = GetOwner();
if (IsValid(Owner))
{
Owner->SetActorHiddenInGame(Dungeon::OcclusionCulling()
&& !bAlwaysVisible
&& !(bRoomAVisible || bRoomBVisible)
);
}
}
// Update door's lock state
// A door is locked when ALL those conditions are met:
// - The door is not `Always Unlocked`.
// - The user tells the door should be locked.
// - The door is spawned by the dungeon generator AND one of the connected rooms is locked or missing.
const bool bPrevLocked = bLocked;
const bool bRoomALocked = !IsValid(RoomA) || RoomA->IsLocked();
const bool bRoomBLocked = !IsValid(RoomB) || RoomB->IsLocked();
bLocked = !bAlwaysUnlocked && (ShouldBeLocked() || (bSpawnedByDungeon && (bRoomALocked || bRoomBLocked)));
if (bLocked != bPrevLocked)
{
DungeonLog_Debug("Door %s locked: %d", *GetNameSafe(this), bLocked);
OnDoorLock(bLocked);
OnDoorLock_BP(bLocked);
OnDoorLocked.Broadcast(this, bLocked);
}
// Update door's open state
const bool bPrevIsOpen = bIsOpen;
bIsOpen = ShouldBeOpen() && !bLocked;
if (bIsOpen != bPrevIsOpen)
{
DungeonLog_Debug("Door %s open: %d", *GetNameSafe(this), bIsOpen);
OnDoorOpen(bIsOpen);
OnDoorOpen_BP(bIsOpen);
OnDoorOpened.Broadcast(this, bIsOpen);
}
#if ENABLE_DRAW_DEBUG
// TODO: Place it in an editor module of the plugin
if (Dungeon::DrawDebug() && GetWorld()->WorldType == EWorldType::EditorPreview)
{
FDoorDef DoorDef;
DoorDef.Direction = EDoorDirection::NbDirection;
DoorDef.Type = Type;
FDoorDef::DrawDebug(GetWorld(), DoorDef, FVector::ZeroVector);
}
#endif // ENABLE_DRAW_DEBUG
}
void UDoorComponent::SetRoomConnection_Implementation(URoomConnection* InRoomConnection)
{
check(OwnerHasAuthority());
SET_COMPONENT_REPLICATED_PROPERTY_VALUE(RoomConnection, InRoomConnection);
}
void UDoorComponent::Open(bool bOpen)
{
if (!OwnerHasAuthority())
return;
if (!IsValid(RoomConnection))
return;
RoomConnection->SetDoorOpen(bOpen);
}
void UDoorComponent::Lock(bool bLock)
{
if (!OwnerHasAuthority())
return;
if (!IsValid(RoomConnection))
return;
RoomConnection->SetDoorLocked(bLock);
}
bool UDoorComponent::ShouldBeOpen() const
{
if (!IsValid(RoomConnection))
return false;
return RoomConnection->IsDoorOpen();
}
bool UDoorComponent::ShouldBeLocked() const
{
if (!IsValid(RoomConnection))
return false;
return RoomConnection->IsDoorLocked();
}
URoom* UDoorComponent::GetRoomA() const
{
if (!IsValid(RoomConnection))
return nullptr;
return RoomConnection->GetRoomA().Get();
}
URoom* UDoorComponent::GetRoomB() const
{
if (!IsValid(RoomConnection))
return nullptr;
return RoomConnection->GetRoomB().Get();
}
bool UDoorComponent::OwnerHasAuthority() const
{
AActor* Owner = GetOwner();
if (!IsValid(Owner))
return false;
return Owner->HasAuthority();
}
@@ -0,0 +1,61 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/RoomObserverComponent.h"
#include "ProceduralDungeonLog.h"
#include "RoomLevel.h"
URoomObserverComponent::URoomObserverComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void URoomObserverComponent::OnRoomEnter_Implementation(ARoomLevel* RoomLevel)
{
DungeonLog_Debug("[Observer] '%s' Enters Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
BindToLevel(RoomLevel, true);
}
void URoomObserverComponent::OnRoomExit_Implementation(ARoomLevel* RoomLevel)
{
DungeonLog_Debug("[Observer] '%s' Exits Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
BindToLevel(RoomLevel, false);
}
void URoomObserverComponent::OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor)
{
// Just forward the call to the delegate.
ActorEnterRoomEvent.Broadcast(RoomLevel, Actor);
}
void URoomObserverComponent::OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor)
{
// Just forward the call to the delegate.
ActorExitRoomEvent.Broadcast(RoomLevel, Actor);
}
void URoomObserverComponent::BindToLevel(ARoomLevel* RoomLevel, bool Bind)
{
if (BoundLevels.Contains(RoomLevel) == Bind)
return;
if (!IsValid(RoomLevel))
return;
if (Bind)
{
RoomLevel->ActorEnterRoomEvent.AddDynamic(this, &URoomObserverComponent::OnActorEnterRoom);
RoomLevel->ActorExitRoomEvent.AddDynamic(this, &URoomObserverComponent::OnActorExitRoom);
BoundLevels.Add(RoomLevel);
}
else
{
RoomLevel->ActorEnterRoomEvent.RemoveDynamic(this, &URoomObserverComponent::OnActorEnterRoom);
RoomLevel->ActorExitRoomEvent.RemoveDynamic(this, &URoomObserverComponent::OnActorExitRoom);
BoundLevels.Remove(RoomLevel);
}
}
@@ -0,0 +1,108 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/SimpleGuidComponent.h"
#include "GameFramework/Actor.h"
#include "ProceduralDungeonLog.h"
#if GUID_DEBUG_ENABLED
#define LOG_GUID_INFO(...) DungeonLog_InfoSilent(##__VA_ARGS__)
#else
#define LOG_GUID_INFO(...)
#endif
USimpleGuidComponent::USimpleGuidComponent()
{
PrimaryComponentTick.bCanEverTick = false;
LOG_GUID_INFO("[%s.SimpleGuidComponent] Construct Component", *GetNameSafe(GetOwner()));
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::OnRegister()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] Register Component", *GetNameSafe(GetOwner()));
Super::OnRegister();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
if (!Guid.IsValid() && IsValid(GetOwner()))
{
#if WITH_EDITOR
Guid = GetOwner()->GetActorGuid();
LOG_GUID_INFO("-- Guid Not Valid! Retrieving Guid from actor: %s", *Guid.ToString());
#else
LOG_GUID_INFO("-- Guid Not Valid! Can't retreive guid from actor!");
#endif
}
}
FGuid USimpleGuidComponent::GetGuid_Implementation() const
{
return Guid;
}
bool USimpleGuidComponent::ShouldSaveActor_Implementation() const
{
return bSaveActorInDungeon;
}
void USimpleGuidComponent::Serialize(FArchive& Ar)
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] Serialize(Ar) Component (%s)", *GetNameSafe(GetOwner()), Ar.IsLoading() ? TEXT("load") : TEXT("save"));
Super::Serialize(Ar);
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::Serialize(FStructuredArchive::FRecord Record)
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] Serialize(Record) Component", *GetNameSafe(GetOwner()));
Super::Serialize(Record);
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
#if GUID_DEBUG_ENABLED
void USimpleGuidComponent::PostInitProperties()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] PostInitProperties Component", *GetNameSafe(GetOwner()));
Super::PostInitProperties();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::PreSave(FObjectPreSaveContext SaveContext)
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] PreSave Component", *GetNameSafe(GetOwner()));
Super::PreSave(SaveContext);
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::PostLoad()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] PostLoad Component", *GetNameSafe(GetOwner()));
Super::PostLoad();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::OnComponentCreated()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] Created Component", *GetNameSafe(GetOwner()));
Super::OnComponentCreated();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::InitializeComponent()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] Initialize Component", *GetNameSafe(GetOwner()));
Super::InitializeComponent();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
void USimpleGuidComponent::BeginPlay()
{
LOG_GUID_INFO("[%s.SimpleGuidComponent] BeginPlay Component", *GetNameSafe(GetOwner()));
Super::BeginPlay();
LOG_GUID_INFO("-- Guid: %s", *Guid.ToString());
}
#endif
@@ -0,0 +1,63 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/StaticRoomObserverComponent.h"
#include "RoomLevel.h"
#include "Engine/Level.h"
UStaticRoomObserverComponent::UStaticRoomObserverComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UStaticRoomObserverComponent::BeginPlay()
{
Super::BeginPlay();
BindToLevel(true);
}
void UStaticRoomObserverComponent::EndPlay(EEndPlayReason::Type Reason)
{
Super::EndPlay(Reason);
BindToLevel(false);
}
void UStaticRoomObserverComponent::OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor)
{
// Just forward the call to the delegate.
ActorEnterRoomEvent.Broadcast(RoomLevel, Actor);
}
void UStaticRoomObserverComponent::OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor)
{
// Just forward the call to the delegate.
ActorExitRoomEvent.Broadcast(RoomLevel, Actor);
}
void UStaticRoomObserverComponent::BindToLevel(bool Bind)
{
if (bBound == Bind)
return;
ULevel* Level = GetComponentLevel();
check(IsValid(Level));
ARoomLevel* RoomLevel = Cast<ARoomLevel>(Level->GetLevelScriptActor());
if (!IsValid(RoomLevel))
return;
if (Bind)
{
RoomLevel->ActorEnterRoomEvent.AddDynamic(this, &UStaticRoomObserverComponent::OnActorEnterRoom);
RoomLevel->ActorExitRoomEvent.AddDynamic(this, &UStaticRoomObserverComponent::OnActorExitRoom);
}
else
{
RoomLevel->ActorEnterRoomEvent.RemoveDynamic(this, &UStaticRoomObserverComponent::OnActorEnterRoom);
RoomLevel->ActorExitRoomEvent.RemoveDynamic(this, &UStaticRoomObserverComponent::OnActorExitRoom);
}
bBound = Bind;
}
@@ -0,0 +1,141 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Components/StaticRoomVisibilityComponent.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "RoomLevel.h"
UStaticRoomVisibilityComponent::UStaticRoomVisibilityComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}
void UStaticRoomVisibilityComponent::BeginPlay()
{
Super::BeginPlay();
UpdateVisibility();
RegisterVisibilityDelegate(GetOwnerRoomLevel(), true);
}
void UStaticRoomVisibilityComponent::EndPlay(EEndPlayReason::Type Reason)
{
Super::EndPlay(Reason);
RegisterVisibilityDelegate(GetOwnerRoomLevel(), false);
}
bool UStaticRoomVisibilityComponent::IsVisible()
{
return (Dungeon::OcclusionCulling() && Dungeon::OccludeDynamicActors()) ? VisibilityEnablers.Num() > 0 : true;
}
void UStaticRoomVisibilityComponent::SetVisible(UObject* Owner, bool Visible)
{
const bool bOldVisible = IsVisible();
if (Visible)
VisibilityEnablers.Add(Owner);
else
VisibilityEnablers.Remove(Owner);
const bool bNewVisible = IsVisible();
DungeonLog_InfoSilent("Visibility of '%s' Changed: %d (before: %d)", *GetNameSafe(GetOwner()), bNewVisible, bOldVisible);
if (bOldVisible == bNewVisible)
return;
UpdateVisibility();
DungeonLog_InfoSilent("Dispatch Room Visibility Event of '%s'", *GetNameSafe(GetOwner()));
OnRoomVisibilityChanged.Broadcast(GetOwner(), bNewVisible);
}
void UStaticRoomVisibilityComponent::ResetVisible(UObject* Owner)
{
SetVisible(Owner, false);
}
void UStaticRoomVisibilityComponent::SetVisibilityMode(EVisibilityMode Mode)
{
VisibilityMode = Mode;
UpdateVisibility();
}
ARoomLevel* UStaticRoomVisibilityComponent::GetOwnerRoomLevel() const
{
ULevel* Level = GetOwner()->GetLevel();
if (!IsValid(Level))
return nullptr;
return Cast<ARoomLevel>(Level->GetLevelScriptActor());
}
void UStaticRoomVisibilityComponent::UpdateVisibility()
{
CleanEnablers();
AActor* Actor = GetOwner();
if (!IsValid(Actor))
return;
// Can't use Actor->SetActorHiddenInGame() because it is replicated over network.
// So instead we use the Root->SetVisibility() and propagate it to its children.
// TODO: try to use something better than that (non-replicated but actor-wide).
USceneComponent* Root = Actor->GetRootComponent();
if (!IsValid(Root))
return;
switch (VisibilityMode)
{
case EVisibilityMode::Default:
Root->SetVisibility(IsVisible(), true);
break;
case EVisibilityMode::ForceHidden:
Root->SetVisibility(false, true);
break;
case EVisibilityMode::ForceVisible:
Root->SetVisibility(true, true);
break;
case EVisibilityMode::Custom:
// The user handles the visibility
break;
default:
checkNoEntry();
break;
}
}
void UStaticRoomVisibilityComponent::RegisterVisibilityDelegate(ARoomLevel* RoomLevel, bool Register)
{
if (!IsValid(RoomLevel))
return;
if (Register)
RoomLevel->VisibilityChangedEvent.AddDynamic(this, &UStaticRoomVisibilityComponent::RoomVisibilityChanged);
else
RoomLevel->VisibilityChangedEvent.RemoveDynamic(this, &UStaticRoomVisibilityComponent::RoomVisibilityChanged);
SetVisible(RoomLevel, RoomLevel->IsVisible());
}
void UStaticRoomVisibilityComponent::RoomVisibilityChanged(ARoomLevel* RoomLevel, bool IsVisible)
{
DungeonLog_InfoSilent("[%s] Room '%s' Visibility Changed: %d", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel), IsVisible);
SetVisible(RoomLevel, IsVisible);
}
void UStaticRoomVisibilityComponent::CleanEnablers()
{
TSet<TWeakObjectPtr<UObject>> ObjPtrToRemove;
for (TWeakObjectPtr<UObject> ObjPtr : VisibilityEnablers)
{
if (!ObjPtr.IsValid())
ObjPtrToRemove.Add(ObjPtr);
}
for (TWeakObjectPtr<UObject> ObjPtr : ObjPtrToRemove)
{
VisibilityEnablers.Remove(ObjPtr);
}
}
@@ -0,0 +1,168 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Door.h"
#include "DoorType.h"
#include "ProceduralDungeonLog.h"
#include "Components/DoorComponent.h"
ADoor::ADoor()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
bAlwaysRelevant = true; // prevent the doors from despawning on clients when server's player is too far
NetDormancy = ENetDormancy::DORM_DormantAll;
DefaultSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
RootComponent = DefaultSceneComponent;
DoorComponent = CreateDefaultSubobject<UDoorComponent>(TEXT("DoorComponent"));
}
void ADoor::PostInitializeComponents()
{
Super::PostInitializeComponents();
if (!IsValid(DoorComponent))
return;
// Forward the component events to the actor events for retro-compatibility
DoorComponent->OnDoorLocked.AddDynamic(this, &ADoor::DispatchDoorLock);
DoorComponent->OnDoorOpened.AddDynamic(this, &ADoor::DispatchDoorOpen);
}
bool ADoor::IsLocked() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->IsLocked();
}
bool ADoor::IsOpen() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->IsOpen();
}
void ADoor::Open(bool bOpen)
{
if (!IsValid(DoorComponent))
return;
DoorComponent->Open(bOpen);
}
void ADoor::Lock(bool bLock)
{
if (!IsValid(DoorComponent))
return;
DoorComponent->Lock(bLock);
}
bool ADoor::ShouldBeOpened() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->ShouldBeOpen();
}
bool ADoor::ShouldBeLocked() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->ShouldBeLocked();
}
const UDoorType* ADoor::GetDoorType() const
{
if (!IsValid(DoorComponent))
return nullptr;
return DoorComponent->GetDoorType();
}
URoom* ADoor::GetRoomA() const
{
if (!IsValid(DoorComponent))
return nullptr;
return DoorComponent->GetRoomA();
}
URoom* ADoor::GetRoomB() const
{
if (!IsValid(DoorComponent))
return nullptr;
return DoorComponent->GetRoomB();
}
void ADoor::DispatchDoorLock(UDoorComponent* Component, bool IsLocked)
{
if (IsLocked)
{
OnDoorLock();
OnDoorLock_BP();
}
else
{
OnDoorUnlock();
OnDoorUnlock_BP();
}
}
void ADoor::DispatchDoorOpen(UDoorComponent* Component, bool IsOpened)
{
if (IsOpened)
{
OnDoorOpen();
OnDoorOpen_BP();
}
else
{
OnDoorClose();
OnDoorClose_BP();
}
}
bool ADoor::GetAlwaysVisible() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->IsAlwaysVisible();
}
bool ADoor::GetAlwaysUnlocked() const
{
if (!IsValid(DoorComponent))
return false;
return DoorComponent->IsAlwaysUnlocked();
}
void ADoor::SetAlwaysVisible(bool bInAlwaysVisible)
{
if (!IsValid(DoorComponent))
return;
DoorComponent->SetAlwaysVisible(bInAlwaysVisible);
}
void ADoor::SetAlwaysUnlocked(bool bInAlwaysUnlocked)
{
if (!IsValid(DoorComponent))
return;
DoorComponent->SetAlwaysUnlocked(bInAlwaysUnlocked);
}
@@ -0,0 +1,51 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DoorType.h"
#include "ProceduralDungeonUtils.h"
UDoorType::UDoorType()
: UDataAsset()
{
Size = Dungeon::DefaultDoorSize();
Offset = Dungeon::DoorOffset();
#if WITH_EDITORONLY_DATA
Color = FColor::Blue;
Description = FText::FromString(TEXT("No Description"));
#endif
bCompatibleWithItself = true;
}
FVector UDoorType::GetSize(const UDoorType* DoorType)
{
return IsValid(DoorType) ? DoorType->Size : Dungeon::DefaultDoorSize();
}
float UDoorType::GetOffset(const UDoorType* DoorType)
{
return IsValid(DoorType) ? DoorType->Offset : Dungeon::DoorOffset();
}
FColor UDoorType::GetColor(const UDoorType* DoorType)
{
return IsValid(DoorType) ? DoorType->Color : Dungeon::DefaultDoorColor();
}
bool UDoorType::AreCompatible(const UDoorType* A, const UDoorType* B)
{
// If both are null, they are compatible
if (!IsValid(A) && !IsValid(B))
return true;
// If only one of them is null, they are not compatible
if (!IsValid(A) || !IsValid(B))
return false;
if (A == B)
return A->bCompatibleWithItself;
return A->Compatibility.Contains(B) || B->Compatibility.Contains(A);
}
@@ -0,0 +1,208 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonBlueprintLibrary.h"
#include "Interfaces/DoorInterface.h"
#include "DoorType.h"
#include "ProceduralDungeonUtils.h"
#include "GameFramework/Actor.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "GameFramework/Pawn.h"
#include "RoomLevel.h"
#include "Room.h"
#include "RoomCustomData.h"
#include "Engine/Engine.h" // GEngine
bool UDungeonBlueprintLibrary::IsDoorOfType(const TSubclassOf<AActor> DoorClass, const UDoorType* DoorType)
{
AActor* Door = DoorClass.GetDefaultObject();
if (!IsValid(Door))
return false;
UObject* Implementer = ActorUtils::GetInterfaceImplementer<UDoorInterface>(Door);
if (!IsValid(Implementer))
return DoorType == nullptr;
const UDoorType* ActualDoorType = IDoorInterface::Execute_GetDoorType(Implementer);
return ActualDoorType == DoorType;
}
bool UDungeonBlueprintLibrary::CompareDataTableRows(const FDataTableRowHandle& A, const FDataTableRowHandle& B)
{
return A == B;
}
const ARoomLevel* UDungeonBlueprintLibrary::GetLevelScript(const AActor* Target)
{
if (!IsValid(Target))
return nullptr;
if (const ARoomLevel* SelfLevel = Cast<ARoomLevel>(Target))
{
return SelfLevel;
}
ULevel* Level = Target->GetLevel();
if (!IsValid(Level))
return nullptr;
ARoomLevel* RoomLevel = Cast<ARoomLevel>(Level->GetLevelScriptActor());
if (!IsValid(RoomLevel))
return nullptr;
return RoomLevel;
}
URoom* UDungeonBlueprintLibrary::GetOwningRoom(const AActor* Target)
{
if (const ARoomLevel* SelfLevel = GetLevelScript(Target))
{
return SelfLevel->GetRoom();
}
return nullptr;
}
bool UDungeonBlueprintLibrary::GetOwningRoomCustomData(const AActor* Target, TSubclassOf<URoomCustomData> CustomDataClass, URoomCustomData*& CustomData)
{
CustomData = nullptr;
URoom* OwningRoom = GetOwningRoom(Target);
if (!IsValid(OwningRoom))
return false;
OwningRoom->GetCustomData(CustomDataClass, CustomData);
return IsValid(CustomData);
}
const URoomData* UDungeonBlueprintLibrary::GetLevelRoomData(const AActor* Target)
{
if (const ARoomLevel* SelfLevel = GetLevelScript(Target))
{
return SelfLevel->GetRoomData();
}
return nullptr;
}
FDoorDef UDungeonBlueprintLibrary::DoorDef_GetOpposite(const FDoorDef& DoorDef)
{
return (DoorDef) ? DoorDef.GetOpposite() : DoorDef;
}
// ===== Plugin Settings Accessors =====
FIntVector UDungeonBlueprintLibrary::IntVector_Next(const FIntVector& Vector, const EDoorDirection& Direction)
{
return Vector + ToIntVector(Direction);
}
FIntVector UDungeonBlueprintLibrary::IntVector_Rotate(const FIntVector& Vector, const EDoorDirection& Direction)
{
return Rotate(Vector, Direction);
}
FIntVector UDungeonBlueprintLibrary::Dungeon_TransformPosition(const FIntVector& LocalPos, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return Transform(LocalPos, Translation, Rotation);
}
FIntVector UDungeonBlueprintLibrary::Dungeon_InverseTransformPosition(const FIntVector& DungeonPos, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return InverseTransform(DungeonPos, Translation, Rotation);
}
FDoorDef UDungeonBlueprintLibrary::Dungeon_TransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return FDoorDef::Transform(DoorDef, Translation, Rotation);
}
FDoorDef UDungeonBlueprintLibrary::Dungeon_InverseTransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return FDoorDef::InverseTransform(DoorDef, Translation, Rotation);
}
FIntVector UDungeonBlueprintLibrary::IntVector_Add(const FIntVector& A, const FIntVector& B)
{
return A + B;
}
FIntVector UDungeonBlueprintLibrary::IntVector_Subtract(const FIntVector& A, const FIntVector& B)
{
return A - B;
}
bool UDungeonBlueprintLibrary::IntVector_Equal(const FIntVector& A, const FIntVector& B)
{
return A == B;
}
bool UDungeonBlueprintLibrary::IntVector_NotEqual(const FIntVector& A, const FIntVector& B)
{
return A != B;
}
FVector UDungeonBlueprintLibrary::Settings_RoomUnit()
{
return Dungeon::RoomUnit();
}
FVector UDungeonBlueprintLibrary::Settings_DefaultDoorSize()
{
return Dungeon::DefaultDoorSize();
}
float UDungeonBlueprintLibrary::Settings_DoorOffset()
{
return Dungeon::DoorOffset();
}
bool UDungeonBlueprintLibrary::Settings_OcclusionCulling()
{
return Dungeon::OcclusionCulling();
}
void UDungeonBlueprintLibrary::Settings_SetOcclusionCulling(bool Enable)
{
Dungeon::EnableOcclusionCulling(Enable);
}
int32 UDungeonBlueprintLibrary::Settings_OcclusionDistance()
{
return Dungeon::OcclusionDistance();
}
void UDungeonBlueprintLibrary::Settings_SetOcclusionDistance(int32 Distance)
{
Dungeon::SetOcclusionDistance(Distance);
}
bool UDungeonBlueprintLibrary::Settings_OccludeDynamicActors()
{
return Dungeon::OccludeDynamicActors();
}
// ===== Gameplay Utility Functions =====
void UDungeonBlueprintLibrary::Spectate(APlayerController* Controller, bool DestroyPawn)
{
if (!Controller)
return;
if (!Controller->HasAuthority())
return;
APawn* PreviousPawn = Controller->GetPawn();
Controller->PlayerState->SetIsSpectator(true);
Controller->ChangeState(NAME_Spectating);
Controller->bPlayerIsWaiting = true;
Controller->ClientGotoState(NAME_Spectating);
if (DestroyPawn && IsValid(PreviousPawn))
{
PreviousPawn->Destroy();
}
}
@@ -0,0 +1,343 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonGenerator.h"
#include "RoomData.h"
#include "Room.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "DungeonGraph.h"
// Sets default values
ADungeonGenerator::ADungeonGenerator()
: Super()
{
PrimaryActorTick.bCanEverTick = true;
GenerationType = EGenerationType::DFS;
}
bool ADungeonGenerator::CreateDungeon_Implementation()
{
// Only server generate the dungeon
// DungeonGraph will be replicated to all clients
if (!HasAuthority())
return false;
switch (CurrentState)
{
case EState::Idle:
DungeonLog_Debug("--- Idle State");
// Maybe move from plugin settings to generator's variable?
CurrentTriesLeft = Dungeon::MaxGenerationTryBeforeGivingUp();
CurrentState = EState::Initializing;
// No break to execute immediatly the Initializing state
case EState::Initializing: {
DungeonLog_Debug("--- Initializing State");
--CurrentTriesLeft;
// Reset generation data
StartNewDungeon();
// Create the list with the correct mode (depth or breadth)
TQueueOrStack<URoom*>::EMode listMode;
switch (GenerationType)
{
case EGenerationType::DFS:
listMode = TQueueOrStack<URoom*>::EMode::STACK;
break;
case EGenerationType::BFS:
listMode = TQueueOrStack<URoom*>::EMode::QUEUE;
break;
default:
DungeonLog_Error("GenerationType value is not supported.");
return false;
}
URoomData* def = ChooseFirstRoomData();
if (!IsValid(def))
{
DungeonLog_Error("ChooseFirstRoomData returned null.");
}
else
{
// Create the first room
URoom* root = CreateRoomInstance(def);
AddRoomToDungeon(root, /*DoorsToConnect = */ {}, /*bFailIfNotConnected = */ false);
// Build the list of rooms
PendingRooms.SetMode(listMode);
PendingRooms.Push(root);
CurrentState = EState::AddingRooms;
}
// No break to execute immediatly the AddingRooms state
}
case EState::AddingRooms: {
DungeonLog_Debug("--- AddingRooms State");
TArray<URoom*> NewRooms;
int BatchCount = RoomBatchSize;
while (!PendingRooms.IsEmpty() && BatchCount > 0)
{
--BatchCount;
URoom* CurrentRoom = PendingRooms.Pop();
check(IsValid(CurrentRoom)); // CurrentRoom should always be valid
if (!AddNewRooms(*CurrentRoom, NewRooms))
{
// Stop generation here
DungeonLog_Debug("--- Stopping generation as AddNewRooms returned false.");
PendingRooms.Empty();
break;
}
DungeonLog_Debug("--- %d rooms added to the dungeon.", NewRooms.Num());
for (URoom* room : NewRooms)
{
PendingRooms.Push(room);
}
}
if (!PendingRooms.IsEmpty())
{
DungeonLog_Debug("--- Still pending rooms, yielding.");
}
else
{
DungeonLog_Debug("--- No more pending rooms, finalizing.");
CurrentState = EState::Finalizing;
}
// Proceed to next tick
YieldGeneration();
break;
}
case EState::Finalizing:
DungeonLog_Debug("--- Finalizing State");
// Initialize the dungeon by eg. altering the room instances
FinalizeDungeon();
CurrentState = EState::Idle;
if (!IsValidDungeon())
{
DungeonLog_Debug("--- Dungeon is not valid, tries left: %d", CurrentTriesLeft);
if (CurrentTriesLeft <= 0)
{
DungeonLog_Error("Generated dungeon is not valid after %d tries. Make sure your ChooseNextRoomData and IsValidDungeon functions are correct.", Dungeon::MaxGenerationTryBeforeGivingUp());
return false;
}
else
{
CurrentState = EState::Initializing;
YieldGeneration();
}
}
break;
default:
DungeonLog_Error("CurrentState value is not supported.");
return false;
}
return true;
}
bool ADungeonGenerator::AddNewRooms(URoom& ParentRoom, TArray<URoom*>& AddedRooms)
{
check(HasAuthority());
int nbDoor = ParentRoom.GetRoomData()->GetNbDoor();
if (nbDoor <= 0)
DungeonLog_Error("The room data '%s' has no door! Nothing could be generated with it!", *GetNameSafe(ParentRoom.GetRoomData()));
// Cache world before loops
const UWorld* World = GetWorld();
const FBoxMinAndMax DungeonBounds = DungeonLimits.GetBox();
AddedRooms.Reset();
bool shouldContinue = false;
for (int i = 0; shouldContinue = ContinueToAddRoom(), i < nbDoor && shouldContinue; ++i)
{
if (ParentRoom.IsConnected(i))
continue;
// Get the door definition in its world position and direction
FDoorDef doorDef = ParentRoom.GetDoorDef(i);
// Get the door definition for the next room
const FDoorDef newRoomDoor = doorDef.GetOpposite();
if (!DungeonBounds.IsInside(newRoomDoor.Position))
continue;
// Maybe move from plugin settings to generator's variable?
int nbTries = Dungeon::MaxRoomPlacementTryBeforeGivingUp();
URoom* newRoom = nullptr;
int doorIndex = -1;
// Try to place a new room
do
{
nbTries--;
bDiscardRoom = false;
URoomData* roomDef = ChooseNextRoomData(ParentRoom.GetRoomData(), &ParentRoom, doorDef, doorIndex);
if (!IsValid(roomDef))
{
bDiscardRoom |= bAutoDiscardRoomIfNull;
if (bDiscardRoom)
{
break;
}
else
{
DungeonLog_Error("ChooseNextRoomData returned null.");
continue;
}
}
if (doorIndex >= roomDef->Doors.Num())
{
DungeonLog_Error("ChooseNextRoomData returned door index '%d' which is out of range in the RoomData '%s' door list (max: %d).", doorIndex, *roomDef->GetName(), roomDef->Doors.Num() - 1);
continue;
}
// Get all compatible door indices from the chosen room data
TArray<int> compatibleDoors;
roomDef->GetCompatibleDoors(doorDef, compatibleDoors);
if (compatibleDoors.Num() <= 0)
{
DungeonLog_Error("ChooseNextRoomData returned room data '%s' with no compatible door (door type: '%s').", *roomDef->GetName(), *doorDef.GetTypeName());
continue;
}
// Get only doors if the new room could fit in the dungeon bounds
for (int n = compatibleDoors.Num() - 1; n >= 0; --n)
{
if (!roomDef->IsRoomInBounds(DungeonBounds, compatibleDoors[n], newRoomDoor))
compatibleDoors.RemoveAt(n);
}
if (compatibleDoors.Num() <= 0)
{
DungeonLog_Warning("ChooseNextRoomData returned room data '%s' that could not fit in dungeon bounds.", *roomDef->GetName());
continue;
}
if (roomDef->RandomDoor || (doorIndex < 0))
doorIndex = compatibleDoors[GetRandomStream().RandRange(0, compatibleDoors.Num() - 1)];
else if (!compatibleDoors.Contains(doorIndex))
{
DungeonLog_Error("ChooseNextRoomData returned door index '%d' (RoomData '%s') which its type '%s' is not compatible with '%s'.", doorIndex, *roomDef->GetName(), *roomDef->Doors[doorIndex].GetTypeName(), *doorDef.GetTypeName());
continue;
}
// Create new room instance from roomdef
newRoom = CreateRoomInstance(roomDef);
// Place the room at targeted door position if possible
if (!TryPlaceRoom(newRoom, doorIndex, newRoomDoor, World))
{
// The object will be automatically deleted by the GC
newRoom = nullptr;
}
} while (nbTries > 0 && newRoom == nullptr);
// If we explicitely want to not place a room, then goes to next door
if (bDiscardRoom)
continue;
// Plugin-wide setting is deprecated, will be removed in v4.0
const bool bConnectAllDoors = bCanLoop && Dungeon::CanLoop();
if (AddRoomToDungeon(newRoom, bConnectAllDoors ? TArray<int> {} : TArray<int> {doorIndex}))
{
AddedRooms.Add(newRoom);
}
else // No room can be placed and all placement tries exhausted
{
// @TODO: Find a way to move this call in AddRoomToDungeon
OnFailedToAddRoom(ParentRoom.GetRoomData(), doorDef);
}
}
// Maybe move from plugin settings to generator's variable?
const bool bRoomLimitReached = Graph->Count() > Dungeon::RoomLimit();
if (bRoomLimitReached)
{
DungeonLog_Warning("Dungeon has reached the room limit of %d! Check your 'Continue To Add Room' to make sure your dungeon is not in an infinite loop, or increase the room limit in the plugin settings if this is intentional.", Dungeon::RoomLimit());
}
return shouldContinue && !bRoomLimitReached;
}
// ===== Default Native Events Implementations =====
URoomData* ADungeonGenerator::ChooseFirstRoomData_Implementation()
{
DungeonLog_Error("Error: ChooseFirstRoomData not implemented");
return nullptr;
}
URoomData* ADungeonGenerator::ChooseNextRoomData_Implementation(const URoomData* CurrentRoom, const TScriptInterface<IReadOnlyRoom>& CurrentRoomInstance, const FDoorDef& DoorData, int& DoorIndex)
{
DungeonLog_Error("Error: ChooseNextRoomData not implemented");
return nullptr;
}
bool ADungeonGenerator::IsValidDungeon_Implementation()
{
DungeonLog_Error("Error: IsValidDungeon not implemented");
return false;
}
bool ADungeonGenerator::ContinueToAddRoom_Implementation()
{
DungeonLog_Error("Error: ContinueToAddRoom not implemented");
return false;
}
// ===== Utility Functions (Deprectated!!!) =====
bool ADungeonGenerator::HasAlreadyRoomData(URoomData* RoomData)
{
return Graph->HasAlreadyRoomData(RoomData);
}
bool ADungeonGenerator::HasAlreadyOneRoomDataFrom(TArray<URoomData*> RoomDataList)
{
return Graph->HasAlreadyOneRoomDataFrom(RoomDataList);
}
int ADungeonGenerator::CountRoomData(URoomData* RoomData)
{
return Graph->CountRoomData(RoomData);
}
int ADungeonGenerator::CountTotalRoomData(TArray<URoomData*> RoomDataList)
{
return Graph->CountTotalRoomData(RoomDataList);
}
bool ADungeonGenerator::HasAlreadyRoomType(TSubclassOf<URoomData> RoomType)
{
return Graph->HasAlreadyRoomType(RoomType);
}
bool ADungeonGenerator::HasAlreadyOneRoomTypeFrom(TArray<TSubclassOf<URoomData>> RoomTypeList)
{
return Graph->HasAlreadyOneRoomTypeFrom(RoomTypeList);
}
int ADungeonGenerator::CountRoomType(TSubclassOf<URoomData> RoomType)
{
return Graph->CountRoomType(RoomType);
}
int ADungeonGenerator::CountTotalRoomType(TArray<TSubclassOf<URoomData>> RoomTypeList)
{
return Graph->CountTotalRoomType(RoomTypeList);
}
int ADungeonGenerator::GetNbRoom()
{
return Graph->Count();
}
@@ -0,0 +1,845 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonGraph.h"
#include "Utils/ReplicationUtils.h"
#include "ProceduralDungeonLog.h"
#include "Containers/Queue.h"
#include "DungeonGenerator.h"
#include "Room.h"
#include "RoomData.h"
#include "RoomCustomData.h"
#include "RoomConnection.h"
#include "Door.h"
#include "Engine/Level.h"
#include "Engine/LevelStreamingDynamic.h"
#include "Utils/DungeonSaveUtils.h"
#include "ProceduralDungeonUtils.h"
#include "DungeonSettings.h"
UDungeonGraph::UDungeonGraph()
: Super()
, Octree(FVector::ZeroVector, HALF_WORLD_MAX)
{
}
void UDungeonGraph::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
FDoRepLifetimeParams Params;
Params.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS(UDungeonGraph, ReplicatedRooms, Params);
DOREPLIFETIME_WITH_PARAMS(UDungeonGraph, RoomConnections, Params);
}
bool UDungeonGraph::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
bool bWroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
for (URoom* Room : ReplicatedRooms)
{
check(Room);
bWroteSomething |= Room->ReplicateSubobject(Channel, Bunch, RepFlags);
}
for (URoomConnection* Conn : RoomConnections)
{
check(Conn);
bWroteSomething |= Conn->ReplicateSubobject(Channel, Bunch, RepFlags);
}
return bWroteSomething;
}
void UDungeonGraph::RegisterReplicableSubobjects(bool bRegister)
{
for (URoom* Room : ReplicatedRooms)
{
Room->RegisterAsReplicable(bRegister);
}
for (URoomConnection* Conn : RoomConnections)
{
Conn->RegisterAsReplicable(bRegister);
}
}
URoomConnection* UDungeonGraph::GetConnectionByIndex(int32 Index) const
{
if (!RoomConnections.IsValidIndex(Index))
{
DungeonLog_WarningSilent("Invalid index %d for RoomConnections.", Index);
return nullptr;
}
return RoomConnections[Index];
}
bool UDungeonGraph::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
{
SavedData = MakeUnique<FSaveData>();
if (!bIsLoading)
{
SavedData->Rooms = TArray<URoom*>(Rooms);
SavedData->Connections = TArray<URoomConnection*>(RoomConnections);
}
SerializeUObjectArray(Record, AR_FIELD_NAME("Rooms"), SavedData->Rooms, bIsLoading, this);
SerializeUObjectArray(Record, AR_FIELD_NAME("Connections"), SavedData->Connections, bIsLoading, this);
return true;
}
void UDungeonGraph::PostLoadDungeon_Implementation()
{
// Load has ended, we can safely reset the saved data.
SavedData.Reset();
}
void UDungeonGraph::AddRoom(URoom* Room)
{
check(IsValid(Room));
Rooms.Add(Room);
UpdateBounds(Room);
UpdateOctree(Room);
}
void UDungeonGraph::InitRooms()
{
// We split the for loops to ensure custom data are created for all rooms before initializing them
// First create empty connections for remaining unconnected doors
TArray<int32> EmptyConnections;
for (URoom* Room : Rooms)
{
check(IsValid(Room));
Room->GetAllEmptyConnections(EmptyConnections);
for (int32 DoorIndex : EmptyConnections)
{
Connect(Room, DoorIndex, nullptr, -1);
}
}
// Finally we can initialize them all
for (URoom* Room : Rooms)
{
// No need to check validity here
const URoomData* Data = Room->GetRoomData();
Data->InitializeRoom(Room, this);
}
}
bool UDungeonGraph::CanRoomFit(const URoom* Room) const
{
bool bCanFit = true;
for (int32 i = 0; i < Room->GetSubBoundsCount() && bCanFit; ++i)
{
FindElementsWithBoundsTest(Octree, Room->GetSubBounds(i), [&bCanFit, Room](const FDungeonOctreeElement& Element) {
bCanFit = false;
});
}
return bCanFit;
}
bool UDungeonGraph::TryConnectDoor(URoom* Room, int32 DoorIndex)
{
check(IsValid(Room));
// Check if already connected.
if (Room->IsConnected(DoorIndex))
return true;
// Get the room in front of the door if any.
EDoorDirection DoorDir = Room->GetDoorWorldOrientation(DoorIndex);
FIntVector AdjacentCell = Room->GetDoorWorldPosition(DoorIndex) + ToIntVector(DoorDir);
URoom* OtherRoom = GetRoomAt(AdjacentCell);
if (!IsValid(OtherRoom))
{
return false;
}
// Get the door index of the other room if any.
int OtherDoorIndex = OtherRoom->GetDoorIndexAt(AdjacentCell, ~DoorDir);
if (OtherDoorIndex < 0) // -1 if no door
{
return false;
}
// Check door compatibility.
const FDoorDef& ThisDoor = Room->GetRoomData()->Doors[DoorIndex];
const FDoorDef& OtherDoor = OtherRoom->GetRoomData()->Doors[OtherDoorIndex];
if (!FDoorDef::AreCompatible(ThisDoor, OtherDoor))
{
return false;
}
// Finally connect the doors.
Connect(Room, DoorIndex, OtherRoom, OtherDoorIndex);
return true;
}
bool UDungeonGraph::TryConnectToExistingDoors(URoom* Room)
{
bool HasConnection = false;
for (int i = 0; i < Room->GetRoomData()->GetNbDoor(); ++i)
{
HasConnection |= TryConnectDoor(Room, i);
}
return HasConnection;
}
TArray<URoom*> UDungeonGraph::GetAllRoomsOverlapping(const FBox& Box) const
{
TArray<URoom*> RoomsInBox;
FindElementsWithBoundsTest(Octree, Box, [&RoomsInBox](const FDungeonOctreeElement& Element) {
URoom* Room = Element.Room;
RoomsInBox.AddUnique(Room);
});
return RoomsInBox;
}
void UDungeonGraph::RetrieveRoomsFromLoadedData()
{
if (Rooms.Num() > 0)
{
DungeonLog_Error("Trying to retrieve loaded rooms while previous ones are not unloaded!");
return;
}
if (!SavedData.IsValid())
return;
Rooms = TArray<URoom*>(SavedData->Rooms);
RoomConnections = TArray<URoomConnection*>(SavedData->Connections);
IDungeonCustomSerialization::DispatchFixupReferences(this, this);
RebuildBounds();
RebuildOctree();
}
void UDungeonGraph::Connect(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB)
{
URoomConnection* NewConnection = URoomConnection::CreateConnection(RoomA, DoorA, RoomB, DoorB, this, RoomConnections.Num());
RoomConnections.Add(NewConnection);
DungeonLog_Debug("Connected %s (%d) to %s (%d)", *GetNameSafe(RoomA), DoorA, *GetNameSafe(RoomB), DoorB);
MARK_PROPERTY_DIRTY_FROM_NAME(UDungeonGraph, RoomConnections, this);
}
void UDungeonGraph::GetAllRoomsFromData(const URoomData* Data, TArray<URoom*>& OutRooms)
{
GetRoomsByPredicate(OutRooms, [Data](const URoom* Room) { return Room->GetRoomData() == Data; });
}
void UDungeonGraph::GetAllRoomsFromDataList(const TArray<URoomData*>& Data, TArray<URoom*>& OutRooms)
{
GetRoomsByPredicate(OutRooms, [&Data](const URoom* Room) { return Data.Contains(Room->GetRoomData()); });
}
const URoom* UDungeonGraph::GetFirstRoomFromData(const URoomData* Data)
{
return FindFirstRoomByPredicate([Data](const URoom* Room) { return Room->GetRoomData() == Data; });
}
void UDungeonGraph::GetAllRoomsWithCustomData(const TSubclassOf<URoomCustomData>& CustomData, TArray<URoom*>& OutRooms)
{
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) { return Room->HasCustomData(CustomData); });
}
void UDungeonGraph::GetAllRoomsWithAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& OutRooms)
{
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) {
for (auto Datum : CustomData)
{
if (!Room->HasCustomData(Datum))
return false;
}
return true;
});
}
void UDungeonGraph::GetAllRoomsWithAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& OutRooms)
{
GetRoomsByPredicate(OutRooms, [&CustomData](const URoom* Room) {
for (auto Datum : CustomData)
{
if (Room->HasCustomData(Datum))
return true;
}
return false;
});
}
URoom* UDungeonGraph::GetRandomRoom(const TArray<URoom*>& RoomList) const
{
if (!Generator.IsValid())
{
DungeonLog_Error("DungeonGraph has no Generator set.");
return nullptr;
}
if (RoomList.Num() <= 0)
return nullptr;
int32 rand = Generator->GetRandomStream().FRandRange(0, RoomList.Num() - 1);
return RoomList[rand];
}
bool UDungeonGraph::HasAlreadyRoomData(const URoomData* RoomData) const
{
return CountRoomData(RoomData) > 0;
}
bool UDungeonGraph::HasAlreadyOneRoomDataFrom(const TArray<URoomData*>& RoomDataList) const
{
return CountTotalRoomData(RoomDataList) > 0;
}
int UDungeonGraph::CountRoomData(const URoomData* RoomData) const
{
return CountRoomByPredicate([RoomData](const URoom* Room) { return Room->GetRoomData() == RoomData; });
}
int UDungeonGraph::CountTotalRoomData(const TArray<URoomData*>& RoomDataList) const
{
return CountRoomByPredicate([&RoomDataList](const URoom* Room) { return RoomDataList.Contains(Room->GetRoomData()); });
}
bool UDungeonGraph::HasAlreadyRoomType(const TSubclassOf<URoomData>& RoomType) const
{
return CountRoomType(RoomType) > 0;
}
bool UDungeonGraph::HasAlreadyOneRoomTypeFrom(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const
{
return CountTotalRoomType(RoomTypeList) > 0;
}
int UDungeonGraph::CountRoomType(const TSubclassOf<URoomData>& RoomType) const
{
return CountRoomByPredicate([&RoomType](const URoom* Room) { return Room->GetRoomData()->GetClass()->IsChildOf(RoomType); });
}
int UDungeonGraph::CountTotalRoomType(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const
{
return CountRoomByPredicate([&RoomTypeList](const URoom* Room) {
return RoomTypeList.ContainsByPredicate([Room](const TSubclassOf<URoomData> RoomType) {
return Room->GetRoomData()->GetClass()->IsChildOf(RoomType);
});
});
}
bool UDungeonGraph::HasValidPath(const URoom* From, const URoom* To, bool IgnoreLockedRooms) const
{
return FindPath(From, To, nullptr, IgnoreLockedRooms);
}
int32 UDungeonGraph::NumberOfRoomBetween(const URoom* A, const URoom* B, bool IgnoreLockedRooms) const
{
TArray<const URoom*> Path;
FindPath(A, B, &Path, IgnoreLockedRooms);
return Path.Num();
}
int32 UDungeonGraph::NumberOfRoomBetween_ReadOnly(TScriptInterface<IReadOnlyRoom> A, TScriptInterface<IReadOnlyRoom> B) const
{
// @TODO: That's not really safe, it should be better to make a FindPath using ReadOnlyRooms too.
const URoom* RoomA = Cast<URoom>(A.GetObject());
const URoom* RoomB = Cast<URoom>(B.GetObject());
return NumberOfRoomBetween(RoomA, RoomB);
}
bool UDungeonGraph::GetPathBetween(const URoom* A, const URoom* B, TArray<URoom*>& ResultPath, bool IgnoreLockedRooms) const
{
// @HACK: is it another alternative?
TArray<const URoom*>& Temp = reinterpret_cast<TArray<const URoom*>&>(ResultPath);
FindPath(A, B, &Temp, IgnoreLockedRooms);
return ResultPath.Num() > 0;
}
URoom* UDungeonGraph::GetRoomAt(FIntVector RoomCell) const
{
const FVector RoomUnit = UDungeonSettings::GetRoomUnit(Generator->GetSettings());
FVector Location = Dungeon::ToWorldLocation(RoomCell, RoomUnit);
FBox LocationBox(Location, Location + FVector::OneVector);
URoom* FoundRoom = nullptr;
FindElementsWithBoundsTest(Octree, LocationBox, [&FoundRoom](const FDungeonOctreeElement& Element) {
FoundRoom = Element.Room;
});
return FoundRoom;
}
FVector UDungeonGraph::GetDungeonBoundsCenter() const
{
FTransform Transform = Generator.IsValid() ? Generator->GetDungeonTransform() : FTransform::Identity;
return GetDungeonBounds(Transform).Center;
}
FVector UDungeonGraph::GetDungeonBoundsExtent() const
{
FTransform Transform = Generator.IsValid() ? Generator->GetDungeonTransform() : FTransform::Identity;
return GetDungeonBounds(Transform).Extent;
}
static bool RoomCandidatePredicate(const FRoomCandidate& A, const FRoomCandidate& B)
{
return A.Score > B.Score;
}
bool UDungeonGraph::FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms, const FScoreCallback& CustomScore) const
{
SortedRooms.Empty();
FDoorDef TargetDoor = FromDoor.GetOpposite();
for (URoomData* RoomData : RoomList)
{
if (!IsValid(RoomData))
continue;
FVoxelBounds DataBounds = RoomData->GetVoxelBounds();
// Try each possible door
for (int i = 0; i < RoomData->GetNbDoor(); ++i)
{
const FDoorDef& Door = RoomData->Doors[i];
// Filter out the door candidate if not compatible with the door
// we want to connect from.
if (!FDoorDef::AreCompatible(TargetDoor, Door))
continue;
// Compute new room placement
const EDoorDirection RoomDirection = TargetDoor.Direction - Door.Direction;
const FIntVector RoomLocation = TargetDoor.Position - Rotate(Door.Position, RoomDirection);
// Filter out the rooms that does not pass the constraints
if (!URoomData::DoesPassAllConstraints(this, RoomData, RoomLocation, RoomDirection))
continue;
FRoomCandidate Candidate;
Candidate.Data = RoomData;
Candidate.DoorIndex = i;
// Check if the new bounds placed at the target door can fit
const FVoxelBounds NewBounds = Rotate(DataBounds, RoomDirection) + RoomLocation;
if (!NewBounds.GetCompatibilityScore(Bounds, Candidate.Score, CustomScore))
continue;
SortedRooms.HeapPush(Candidate, ::RoomCandidatePredicate);
}
}
return SortedRooms.Num() > 0;
}
bool UDungeonGraph::FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms) const
{
return FilterAndSortRooms(RoomList, FromDoor, SortedRooms, FScoreCallback());
}
FBoxCenterAndExtent UDungeonGraph::GetDungeonBounds(const FTransform& Transform) const
{
const FVector RoomUnit = UDungeonSettings::GetRoomUnit(Generator.IsValid() ? Generator->SettingsOverrides : nullptr);
return Dungeon::ToWorld(Bounds.GetBounds(), RoomUnit, Transform);
}
FBoxMinAndMax UDungeonGraph::GetIntBounds() const
{
return Bounds.GetBounds();
}
URoom* UDungeonGraph::GetRoomByIndex(int64 Index) const
{
for (URoom* Room : Rooms)
{
if (Room->GetRoomID() == Index)
return Room;
}
return nullptr;
}
void UDungeonGraph::Clear()
{
// Call cleanup for each room
for (URoom* Room : Rooms)
{
check(IsValid(Room));
const URoomData* Data = Room->GetRoomData();
check(IsValid(Data));
Data->CleanupRoom(Room, this);
}
// Clear out data
Rooms.Empty();
RoomConnections.Empty();
RebuildBounds();
RebuildOctree();
}
int UDungeonGraph::CountRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const
{
int count = 0;
for (const URoom* Room : Rooms)
{
if (Predicate(Room))
count++;
}
return count;
}
void UDungeonGraph::GetRoomsByPredicate(TArray<URoom*>& OutRooms, TFunction<bool(const URoom*)> Predicate) const
{
OutRooms.Empty();
for (URoom* Room : Rooms)
{
check(IsValid(Room));
if (Predicate(Room))
OutRooms.Add(Room);
}
}
const URoom* UDungeonGraph::FindFirstRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const
{
for (URoom* Room : Rooms)
{
check(IsValid(Room));
if (Predicate(Room))
return Room;
}
return nullptr;
}
void UDungeonGraph::TraverseRooms(const TSet<URoom*>& InRooms, TSet<URoom*>* OutRooms, uint32 Distance, TFunction<void(URoom*, uint32)> Func)
{
TSet<URoom*> openList(InRooms);
TSet<URoom*> closedList, currentList;
const uint32 MaxDistance = Distance;
while (Distance > 0 && openList.Num() > 0)
{
for (URoom* openRoom : openList)
closedList.Add(openRoom);
Swap(currentList, openList);
openList.Empty();
for (URoom* currentRoom : currentList)
{
Func(currentRoom, MaxDistance - Distance);
for (int i = 0; i < currentRoom->GetConnectionCount(); ++i)
{
URoom* nextRoom = currentRoom->GetConnectedRoom(i).Get();
if (IsValid(nextRoom) && !closedList.Contains(nextRoom))
openList.Add(nextRoom);
}
}
--Distance;
}
if (OutRooms != nullptr)
Swap(*OutRooms, closedList);
}
// Do one cycle of BFS (dequeue one room from Queue, then check all its connections to add them in MarkedThis and filling ParentMap)
// Fills OutCommon if a connection has been found in MarkedOther
// Returns true if OutCommon had been filled
bool BFS_Cycle(TQueue<const URoom*>& Queue, TSet<const URoom*>& MarkedThis, const TSet<const URoom*>& MarkedOther, TMap<const URoom*, const URoom*>& ParentMap, const URoom*& OutCommon, bool IgnoreLocked)
{
const URoom* Current = nullptr;
const URoom* Next = nullptr;
Queue.Dequeue(Current);
for (const auto& Conn : Current->GetAllConnections())
{
if (!Conn.IsValid())
continue;
if (!IgnoreLocked && Conn->IsDoorLocked())
continue;
Next = Conn->GetOtherRoom(Current).Get();
if (!IsValid(Next))
continue;
if (!IgnoreLocked && Next->IsLocked())
continue;
if (MarkedThis.Contains(Next))
continue;
ParentMap.Add(Next, Current);
// Check intersection with other side
if (MarkedOther.Contains(Next))
{
OutCommon = Next;
break;
}
else
{
Queue.Enqueue(Next);
MarkedThis.Add(Next);
}
}
return OutCommon != nullptr;
}
void ReconstructPath(const URoom* Common, const TMap<const URoom*, const URoom*>& ParentsForward, const TMap<const URoom*, const URoom*>& ParentsReverse, TArray<const URoom*>& OutPath)
{
OutPath.Empty();
if (Common == nullptr)
return;
// Adds the first part of the path (From -> Common)
const URoom* const* Current = &Common;
while ((Current = ParentsForward.Find(*Current)) != nullptr)
{
OutPath.EmplaceAt(0, *Current);
}
// Common room between
OutPath.Add(Common);
// Adds the second part of the path (Common -> To)
Current = &Common;
while ((Current = ParentsReverse.Find(*Current)) != nullptr)
{
OutPath.Add(*Current);
}
}
// Uses Bidirectional BFS to find a path between A and B
bool UDungeonGraph::FindPath(const URoom* From, const URoom* To, TArray<const URoom*>* OutPath, bool IgnoreLocked)
{
if (OutPath)
OutPath->Empty();
if (!IsValid(From) || !IsValid(To))
return false;
// Always path between a room and itself
if (From == To)
{
if (OutPath)
OutPath->Add(From);
return true;
}
if (!IgnoreLocked && (From->IsLocked() || To->IsLocked()))
return false;
// Bidirectional BFS initialization
TMap<const URoom*, const URoom*> ParentsForward, ParentsReverse;
TSet<const URoom*> MarkedForward, MarkedReverse; // (visited rooms)
TQueue<const URoom*> QueueForward, QueueReverse; // (rooms to visit)
QueueForward.Enqueue(From);
QueueReverse.Enqueue(To);
MarkedForward.Add(From);
MarkedReverse.Add(To);
// Both are filled when during either cycle an intersection is found
const URoom* Common = nullptr;
// Bidirectional BFS
while (Common == nullptr && !QueueForward.IsEmpty() && !QueueReverse.IsEmpty())
{
// BFS from A
if (!BFS_Cycle(QueueForward, MarkedForward, MarkedReverse, ParentsForward, Common, IgnoreLocked))
{
// BFS from B if no common found
BFS_Cycle(QueueReverse, MarkedReverse, MarkedForward, ParentsReverse, Common, IgnoreLocked);
}
}
// Intersection has been found between MarkedForward and MarkedReverse
if (Common != nullptr && OutPath != nullptr)
{
ReconstructPath(Common, ParentsForward, ParentsReverse, *OutPath);
}
return Common != nullptr;
}
void CopyRooms(TArray<URoom*>& To, TArray<URoom*>& From)
{
for (URoom* Room : From)
{
if (Room->Instance)
DungeonLog_Debug("[%s] Loaded Level: %s", *GetNameSafe(Room), *GetNameSafe(Room->Instance->GetLoadedLevel()));
}
To = TArray<URoom*>(From);
}
void UDungeonGraph::SynchronizeRooms()
{
AActor* Owner = GetTypedOuter<AActor>();
if (!IsValid(Owner))
return;
if (Owner->HasAuthority())
{
Owner->FlushNetDormancy();
RegisterReplicableSubobjects(false);
CopyRooms(ReplicatedRooms, Rooms);
RegisterReplicableSubobjects(true);
MARK_PROPERTY_DIRTY_FROM_NAME(UDungeonGraph, ReplicatedRooms, this);
}
else
{
CopyRooms(Rooms, ReplicatedRooms);
RebuildBounds();
RebuildOctree();
DungeonLog_Debug("Synchronized Rooms from server (length: %d)", Rooms.Num());
for (const URoom* Room : Rooms)
{
DungeonLog_Debug(" - %s (Data: %s Valid: %d)", *GetNameSafe(Room), *GetNameSafe(Room->GetRoomData()), IsValid(Room->GetRoomData()));
}
}
bIsDirty = false;
}
bool UDungeonGraph::AreRoomsLoaded(int32& NbRoomLoaded) const
{
NbRoomLoaded = 0;
for (URoom* Room : Rooms)
{
if (Room->IsInstanceLoaded())
NbRoomLoaded++;
}
return NbRoomLoaded >= Rooms.Num();
}
bool UDungeonGraph::AreRoomsUnloaded(int32& NbRoomUnloaded) const
{
NbRoomUnloaded = 0;
for (URoom* Room : Rooms)
{
if (!IsValid(Room) || Room->IsInstanceUnloaded())
NbRoomUnloaded++;
}
return NbRoomUnloaded >= Rooms.Num();
}
bool UDungeonGraph::AreRoomsInitialized(int32& NbRoomInitialized) const
{
NbRoomInitialized = 0;
for (URoom* Room : Rooms)
{
if (Room->IsInstanceInitialized())
NbRoomInitialized++;
}
return NbRoomInitialized >= Rooms.Num();
}
bool UDungeonGraph::AreRoomsReady() const
{
for (URoom* Room : Rooms)
{
if (!(IsValid(Room) && Room->IsReady()))
return false;
}
return true;
}
void UDungeonGraph::SpawnAllDoors()
{
// Spawn doors only on server
// They will be replicated on the clients
if (!HasAuthority())
return;
checkf(Generator.IsValid(), TEXT("Spawning dungeon's doors is only available with a ADungeonGenerator outer."));
for (auto* RoomConnection : RoomConnections)
{
if (RoomConnection->IsDoorInstanced())
continue;
RoomConnection->InstantiateDoor(GetWorld(), Generator.Get(), Generator->UseGeneratorTransform());
}
}
void UDungeonGraph::LoadAllRooms()
{
// When a level is correct, load all rooms
for (URoom* Room : Rooms)
{
Room->Instantiate(GetWorld());
}
SpawnAllDoors();
}
void UDungeonGraph::UnloadAllRooms()
{
if (HasAuthority())
{
for (auto* RoomConnection : RoomConnections)
{
RoomConnection->DestroyDoor();
}
}
for (URoom* Room : Rooms)
{
check(Room);
Room->Destroy();
}
}
void UDungeonGraph::UpdateBounds(const URoom* Room)
{
check(IsValid(Room));
Bounds += Room->GetVoxelBounds();
}
void UDungeonGraph::RebuildBounds()
{
Bounds = FVoxelBounds();
for (const URoom* Room : Rooms)
{
UpdateBounds(Room);
}
}
void UDungeonGraph::UpdateOctree(URoom* Room)
{
check(IsValid(Room));
for (int i = 0; i < Room->GetSubBoundsCount(); ++i)
{
Octree.AddElement(FDungeonOctreeElement(Room, i));
}
}
void UDungeonGraph::RebuildOctree()
{
Octree.Destroy();
for (URoom* Room : Rooms)
{
UpdateOctree(Room);
}
}
void UDungeonGraph::OnRep_Rooms()
{
DungeonLog_Debug("Replicated Rooms Changed! (length: %d)", ReplicatedRooms.Num());
for (int i = 0; i < ReplicatedRooms.Num(); ++i)
{
// Trigger Room List Changed only when all received rooms are valid
if (!IsValid(ReplicatedRooms[i]))
return;
DungeonLog_Debug("Replicated Room [%d]: %s", i, *GetNameSafe(ReplicatedRooms[i]));
}
DungeonLog_Debug("Trigger Dungeon Reload!");
MarkDirty();
}
@@ -0,0 +1,16 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonOctree.h"
#include "Room.h"
FDungeonOctreeElement::FDungeonOctreeElement(URoom* Room, int32 BoxIndex)
{
check(IsValid(Room));
this->Room = Room;
Bounds = Room->GetSubBounds(BoxIndex);
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonSaveProxyArchive.h"
@@ -0,0 +1,20 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "DungeonSettings.h"
#include "ProceduralDungeonUtils.h"
UDungeonSettings::UDungeonSettings()
: Super()
{
RoomUnit = Dungeon::RoomUnit();
}
FVector UDungeonSettings::GetRoomUnit(const UDungeonSettings* Settings)
{
return IsValid(Settings) ? Settings->RoomUnit : Dungeon::RoomUnit();
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/DoorInterface.h"
@@ -0,0 +1,27 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/DungeonCustomSerialization.h"
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonUtils.h"
bool IDungeonCustomSerialization::DispatchFixupReferences(UObject* Obj, UObject* Context)
{
check(IsValid(Obj));
DungeonLog_Debug("[BEGIN] Dispatch 'Fixup References' function from object '%s'.", *GetNameSafe(Obj));
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [Context](UObject* O) {
auto* Custom = Cast<IDungeonCustomSerialization>(O);
if (nullptr != Custom)
{
Custom->FixupReferences(Context);
}
});
DungeonLog_Debug("[END] Dispatch 'Fixup References' function from object '%s'.", *GetNameSafe(Obj));
return true;
}
@@ -0,0 +1,40 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/DungeonSaveInterface.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
void IDungeonSaveInterface::DispatchPreSaveEvent(UObject* Obj)
{
check(IsValid(Obj));
DungeonLog_Debug("[BEGIN] Dispatch 'Dungeon Pre Save' events from object '%s'.", *GetNameSafe(Obj));
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [](UObject* O) {
if (IsValid(O) && O->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PreSaveDungeon(O);
}
});
DungeonLog_Debug("[END] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
}
void IDungeonSaveInterface::DispatchPostLoadEvent(UObject* Obj)
{
check(IsValid(Obj));
DungeonLog_Debug("[BEGIN] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
ObjectUtils::DispatchToObjectAndSubobjects(Obj, [](UObject* O) {
if (IsValid(O) && O->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PostLoadDungeon(O);
}
});
DungeonLog_Debug("[END] Dispatch 'Dungeon Post Load' events from object '%s'.", *GetNameSafe(Obj));
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/GeneratorProvider.h"
@@ -0,0 +1,16 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/RoomActorGuid.h"
#include "GameFramework/Actor.h"
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonUtils.h"
UObject* IRoomActorGuid::GetImplementer(AActor* Actor)
{
return ActorUtils::GetInterfaceImplementer<URoomActorGuid>(Actor);
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Interfaces/RoomContainer.h"
@@ -0,0 +1,139 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeon.h"
#include "Developer/Settings/Public/ISettingsModule.h"
#include "Developer/Settings/Public/ISettingsSection.h"
#include "ProceduralDungeonSettings.h"
#include "ProceduralDungeonLog.h"
#include "Misc/EngineVersionComparison.h"
#include "UObject/CoreRedirects.h"
#define LOCTEXT_NAMESPACE "FProceduralDungeonModule"
#if WITH_EDITOR && UE_VERSION_NEWER_THAN(5, 4, 0)
#define ACTOR_REPLACEMENT_FIX_HACK 1
#else
#define ACTOR_REPLACEMENT_FIX_HACK 0
#endif
// ----- Hack to fix Room references issues of RoomLevel actors in PIE for UE 5.4
#if ACTOR_REPLACEMENT_FIX_HACK
#include "RoomLevel.h"
#include "Room.h"
FDelegateHandle ObjectReplacedHandle;
void ObjectReplaced(const FCoreUObjectDelegates::FReplacementObjectMap& ReplacementMap)
{
for (const auto& Pair : ReplacementMap)
{
ARoomLevel* OldActor = Cast<ARoomLevel>(Pair.Key);
ARoomLevel* NewActor = Cast<ARoomLevel>(Pair.Value);
if (!OldActor || !NewActor)
continue;
if (OldActor->HasAllFlags(EObjectFlags::RF_ClassDefaultObject) || NewActor->HasAllFlags(EObjectFlags::RF_ClassDefaultObject))
continue;
URoom* RoomInstance = OldActor->GetRoom();
OldActor->Room = nullptr;
if (!IsValid(RoomInstance))
continue;
// Fixup Room reference not properly carried over during actor replacement process
NewActor->Init(RoomInstance);
DungeonLog_Debug("Fixed Room reference ('%s' -> '%s')", *GetNameSafe(OldActor), *GetNameSafe(NewActor));
}
}
#endif
// ----- End Hack
void FProceduralDungeonModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
RegisterSettings();
#if ACTOR_REPLACEMENT_FIX_HACK
ObjectReplacedHandle = FCoreUObjectDelegates::OnObjectsReinstanced.AddStatic(ObjectReplaced);
DungeonLog_Debug("Use Actor Replacement Hack");
#endif
TArray<FCoreRedirect> Redirects;
Redirects.Emplace(ECoreRedirectFlags::Type_Property, TEXT("/Script/ProceduralDungeon.Room.RoomData"), TEXT("/Script/ProceduralDungeon.Room.SoftRoomData"));
FCoreRedirects::AddRedirectList(Redirects, TEXT("ProceduralDungeon"));
}
void FProceduralDungeonModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
if (UObjectInitialized())
{
UnregisterSettings();
}
#if ACTOR_REPLACEMENT_FIX_HACK
FCoreUObjectDelegates::OnObjectsReinstanced.Remove(ObjectReplacedHandle);
#endif
}
void FProceduralDungeonModule::RegisterSettings()
{
// Registering some settings is just a matter of exposing the default UObject of
// your desired class, feel free to add here all those settings you want to expose
// to your LDs or artists.
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
// Register the settings
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Plugins", "Procedural Dungeon",
LOCTEXT("RuntimeGeneralSettingsName", "Procedural Dungeon"),
LOCTEXT("RuntimeGeneralSettingsDescription", "Configuration for the Procedural Dungeon plugin"),
GetMutableDefault<UProceduralDungeonSettings>()
);
// Register the save handler to your settings, you might want to use it to
// validate those or just act to settings changes.
if (SettingsSection.IsValid())
{
SettingsSection->OnModified().BindRaw(this, &FProceduralDungeonModule::HandleSettingsSaved);
}
}
}
void FProceduralDungeonModule::UnregisterSettings()
{
// Ensure to unregister all of your registered settings here, hot-reload would
// otherwise yield unexpected results.
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->UnregisterSettings("Project", "Plugins", "Procedural Dungeon");
}
}
// Callback for when the settings were saved.
bool FProceduralDungeonModule::HandleSettingsSaved()
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
bool ResaveSettings = false;
// You can put any validation code in here and resave the settings in case an invalid
// value has been entered
if (ResaveSettings)
{
Settings->SaveConfig();
}
return true;
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FProceduralDungeonModule, ProceduralDungeon)
@@ -0,0 +1,14 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeonCustomVersion.h"
#include "Serialization/CustomVersion.h"
const FGuid FProceduralDungeonCustomVersion::GUID(0x07E63959, 0x72E5DEE1, 0x7B00F72A, 0x1B442349);
// Register the custom version with core
FCustomVersionRegistration GRegisterDungeonCustomVersion(FProceduralDungeonCustomVersion::GUID, FProceduralDungeonCustomVersion::LatestVersion, TEXT("ProcDungeonVer"));
@@ -0,0 +1,31 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonSettings.h"
#include "Engine/Engine.h" // GEngine
DEFINE_LOG_CATEGORY(LogProceduralDungeon);
namespace
{
bool ShowLogsOnScreen(float& _duration)
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
_duration = Settings->PrintDebugDuration;
return Settings->OnScreenPrintDebug;
}
} //namespace
void LogOnScreen(const FString& Message, FColor Color, bool bForceOnScreen)
{
float Duration;
if (::ShowLogsOnScreen(Duration) || bForceOnScreen)
{
GEngine->AddOnScreenDebugMessage(-1, Duration, Color, Message);
}
}
@@ -0,0 +1,83 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeonSettings.h"
#include "HAL/IConsoleManager.h"
UProceduralDungeonSettings::UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Dungeon settings
RoomUnit = FVector(1000, 1000, 400);
DoorSize = FVector(40, 640, 400);
DoorOffset = 0.0f;
CanLoop = true;
MaxGenerationTry = 500;
MaxRoomPlacementTry = 10;
RoomLimit = 100;
// Occlusion settings
OcclusionCulling = true;
//LegacyOcclusion = true;
OcclusionDistance = 2;
OccludeDynamicActors = true;
// Debug settings
DrawDebug = true;
bDrawOnlyWhenEditingRooms = false;
ShowRoomOrigin = false;
bFlipDoorArrowSide = false;
DoorArrowLength = 300.0f;
DoorArrowHeadSize = 300.0f;
OnScreenPrintDebug = false;
PrintDebugDuration = 60.0f;
// Register console variables.
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion")
, OcclusionCulling
, TEXT("Enable/disable the plugin's occlusion culling system.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion.Distance")
, OcclusionDistance
, TEXT("Change the number of room shown by the plugin's occlusion culling system.\n")
TEXT("1 means only the player's room is visible. 0 or negative means no room visible at all. 2 or more will show the connected rooms to the player based on their number of connection.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Occlusion.DynamicActors")
, OccludeDynamicActors
, TEXT("Enable/disable the occlusion of actors with a RoomVisibility component attached on them.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Draw")
, DrawDebug
, TEXT("Enable/disable the debug drawings of the rooms and doors.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Draw.EditingOnly")
, bDrawOnlyWhenEditingRooms
, TEXT("Enable/disable the debug drawings to be only shown when editing a room level.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Log.OnScreen")
, OnScreenPrintDebug
, TEXT("Enable/disable the on-screen logging of the plugin.")
, EConsoleVariableFlags::ECVF_Cheat
);
IConsoleManager::Get().RegisterConsoleVariableRef(TEXT("pd.Debug.Log.Duration")
, PrintDebugDuration
, TEXT("Change the on-screen logging duration (in seconds) of the plugin.")
, EConsoleVariableFlags::ECVF_Cheat
);
}
@@ -0,0 +1,494 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeonTypes.h"
#include "DrawDebugHelpers.h"
#include "ProceduralDungeonUtils.h"
#include "DoorType.h"
bool operator!(const EDoorDirection& Direction)
{
return Direction == EDoorDirection::NbDirection;
}
EDoorDirection operator+(const EDoorDirection& A, const EDoorDirection& B)
{
check(!!A && !!B);
return static_cast<EDoorDirection>(static_cast<uint8>((static_cast<uint8>(A) + static_cast<uint8>(B))) % static_cast<uint8>(EDoorDirection::NbDirection));
}
EDoorDirection operator-(const EDoorDirection& A, const EDoorDirection& B)
{
check(!!A && !!B);
return static_cast<EDoorDirection>(static_cast<uint8>((static_cast<uint8>(A) - static_cast<uint8>(B))) % static_cast<uint8>(EDoorDirection::NbDirection));
}
EDoorDirection& operator+=(EDoorDirection& A, const EDoorDirection& B)
{
A = A + B;
return A;
}
EDoorDirection& operator-=(EDoorDirection& A, const EDoorDirection& B)
{
A = A - B;
return A;
}
EDoorDirection& operator++(EDoorDirection& Direction)
{
Direction = Direction + EDoorDirection::East;
return Direction;
}
EDoorDirection& operator--(EDoorDirection& Direction)
{
Direction = Direction - EDoorDirection::East;
return Direction;
}
EDoorDirection operator++(EDoorDirection& Direction, int)
{
EDoorDirection old {Direction};
++Direction;
return old;
}
EDoorDirection operator--(EDoorDirection& Direction, int)
{
EDoorDirection old {Direction};
--Direction;
return old;
}
EDoorDirection operator-(const EDoorDirection& Direction)
{
return EDoorDirection::North - Direction;
}
EDoorDirection operator~(const EDoorDirection& Direction)
{
return Direction + EDoorDirection::South;
}
FIntVector ToIntVector(const EDoorDirection& Direction)
{
FIntVector Dir = FIntVector::ZeroValue;
switch (Direction)
{
case EDoorDirection::North:
Dir.X = 1;
break;
case EDoorDirection::East:
Dir.Y = 1;
break;
case EDoorDirection::West:
Dir.Y = -1;
break;
case EDoorDirection::South:
Dir.X = -1;
break;
default:
checkNoEntry();
}
return Dir;
}
FVector ToVector(const EDoorDirection& Direction)
{
FVector Dir = FVector::ZeroVector;
switch (Direction)
{
case EDoorDirection::North:
Dir.X = 1.0f;
break;
case EDoorDirection::East:
Dir.Y = 1.0f;
break;
case EDoorDirection::West:
Dir.Y = -1.0f;
break;
case EDoorDirection::South:
Dir.X = -1.0f;
break;
default:
checkNoEntry();
}
return Dir;
}
FQuat ToQuaternion(const EDoorDirection& Direction)
{
check(!!Direction);
return FRotator(0.0f, ToAngle(Direction), 0.0f).Quaternion();
}
float ToAngle(const EDoorDirection& Direction)
{
check(!!Direction);
return 90.0f * static_cast<uint8>(Direction);
}
FIntVector Rotate(const FIntVector& Pos, const EDoorDirection& Rot)
{
FIntVector NewPos = Pos;
switch (Rot)
{
case EDoorDirection::North:
NewPos = Pos;
break;
case EDoorDirection::West:
NewPos.Y = -Pos.X;
NewPos.X = Pos.Y;
break;
case EDoorDirection::East:
NewPos.Y = Pos.X;
NewPos.X = -Pos.Y;
break;
case EDoorDirection::South:
NewPos.Y = -Pos.Y;
NewPos.X = -Pos.X;
break;
default:
checkNoEntry();
}
return NewPos;
}
FVector Rotate(const FVector& Pos, const EDoorDirection& Rot)
{
FVector NewPos = Pos;
switch (Rot)
{
case EDoorDirection::North:
NewPos = Pos;
break;
case EDoorDirection::West:
NewPos.Y = -Pos.X;
NewPos.X = Pos.Y;
break;
case EDoorDirection::East:
NewPos.Y = Pos.X;
NewPos.X = -Pos.Y;
break;
case EDoorDirection::South:
NewPos.Y = -Pos.Y;
NewPos.X = -Pos.X;
break;
default:
checkNoEntry();
}
return NewPos;
}
FIntVector Transform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return Rotate(Pos, Rotation) + Translation;
}
FIntVector InverseTransform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation)
{
return Rotate(Pos - Translation, -Rotation);
}
EDoorDirection Transform(const EDoorDirection& Direction, const EDoorDirection& Rotation)
{
return Direction + Rotation;
}
EDoorDirection InverseTransform(const EDoorDirection& Direction, const EDoorDirection& Rotation)
{
return Direction - Rotation;
}
// ############ FDoorDef ##############
const FDoorDef FDoorDef::Invalid(FIntVector::ZeroValue, EDoorDirection::NbDirection);
FDoorDef::FDoorDef(const FIntVector& InPosition, EDoorDirection InDirection, UDoorType* InType)
{
Position = InPosition;
Direction = InDirection;
Type = InType;
}
bool FDoorDef::IsValid() const
{
return Direction != EDoorDirection::NbDirection;
}
bool FDoorDef::operator==(const FDoorDef& Other) const
{
return Position == Other.Position && Direction == Other.Direction;
}
bool FDoorDef::AreCompatible(const FDoorDef& A, const FDoorDef& B)
{
return UDoorType::AreCompatible(A.Type, B.Type);
}
FVector FDoorDef::GetDoorSize() const
{
return UDoorType::GetSize(Type);
}
float FDoorDef::GetDoorOffset() const
{
return UDoorType::GetOffset(Type);
}
FColor FDoorDef::GetDoorColor() const
{
return UDoorType::GetColor(Type);
}
FString FDoorDef::GetTypeName() const
{
return ::IsValid(Type) ? Type->GetName() : TEXT("Default");
}
FString FDoorDef::ToString() const
{
FText DirectionName;
UEnum::GetDisplayValueAsText(Direction, DirectionName);
return FString::Printf(TEXT("(%d,%d,%d) [%s]"), Position.X, Position.Y, Position.Z, *DirectionName.ToString());
}
FDoorDef FDoorDef::GetOpposite() const
{
FDoorDef OppositeDoor(*this);
OppositeDoor.Position = Position + ToIntVector(Direction);
OppositeDoor.Direction = ~Direction;
return OppositeDoor;
}
FBoxCenterAndExtent FDoorDef::GetBounds(const FVector RoomUnit, bool bIncludeOffset) const
{
const FVector RotatedDoorSize = Rotate(GetDoorSize(), (!Direction) ? EDoorDirection::North : Direction).GetAbs();
const FVector WorldPosition = GetRealDoorPosition(*this, RoomUnit, bIncludeOffset) + FVector(0, 0, RotatedDoorSize.Z * 0.5f);
return FBoxCenterAndExtent(WorldPosition, 0.5f * RotatedDoorSize);
}
FVector FDoorDef::GetRealDoorPosition(const FDoorDef& DoorDef, const FVector RoomUnit, bool bIncludeOffset)
{
return GetRealDoorPosition(DoorDef.Position, DoorDef.Direction, RoomUnit, bIncludeOffset ? DoorDef.GetDoorOffset() : 0.0f);
}
FVector FDoorDef::GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot, const FVector RoomUnit, float DoorOffset)
{
const FVector CellPosition = FVector(DoorCell);
const FVector DirectionOffset = !DoorRot ? FVector::ZeroVector : (0.5f * ToVector(DoorRot));
const FVector HeightOffset = FVector(0, 0, DoorOffset);
return RoomUnit * (CellPosition + DirectionOffset + HeightOffset);
}
FQuat FDoorDef::GetRealDoorRotation(const FDoorDef& DoorDef, bool bFlipped)
{
return FRotator(0, 90 * static_cast<uint8>(DoorDef.Direction) + bFlipped * 180, 0).Quaternion();
}
FDoorDef FDoorDef::Transform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation)
{
FDoorDef NewDoor = DoorDef;
NewDoor.Position = ::Transform(DoorDef.Position, Translation, Rotation);
NewDoor.Direction = ::Transform(DoorDef.Direction, Rotation);
return NewDoor;
}
FDoorDef FDoorDef::InverseTransform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation)
{
FDoorDef NewDoor = DoorDef;
NewDoor.Position = ::InverseTransform(DoorDef.Position, Translation, Rotation);
NewDoor.Direction = ::InverseTransform(DoorDef.Direction, Rotation);
return NewDoor;
}
#if !UE_BUILD_SHIPPING
void FDoorDef::DrawDebug(const UWorld* World, const FDoorDef& DoorDef, const FVector RoomUnit, const FTransform& Transform, bool bIncludeOffset, bool bIsConnected)
{
DrawDebug(World, DoorDef.GetDoorColor(), DoorDef.GetDoorSize(), RoomUnit, DoorDef.Position, DoorDef.Direction, Transform, bIncludeOffset ? DoorDef.GetDoorOffset() : 0.0f, bIsConnected);
// Door debug draw using its bounds
//FBoxCenterAndExtent DoorBounds = DoorDef.GetBounds(bIncludeOffset);
//DrawDebugBox(World, Transform.TransformPosition(DoorBounds.Center), DoorBounds.Extent, Transform.GetRotation(), FColor::Cyan);
}
void FDoorDef::DrawDebug(const UWorld* World, const FColor& Color, const FVector& DoorSize, const FVector RoomUnit, const FIntVector& DoorCell, const EDoorDirection& DoorRot, const FTransform& Transform, float DoorOffset, bool bIsConnected)
{
#if ENABLE_DRAW_DEBUG
// @TODO: Use FDoorDef::GetBounds here? (should mabye remove this overload and use exclusively the one with FDoorDef?)
FQuat DoorRotation = Transform.GetRotation() * ToQuaternion(!DoorRot ? EDoorDirection::North : DoorRot);
FVector DoorPosition = Transform.TransformPosition(GetRealDoorPosition(DoorCell, DoorRot, RoomUnit, DoorOffset) + FVector(0, 0, DoorSize.Z * 0.5f));
// Door frame
DrawDebugBox(World, DoorPosition, DoorSize * 0.5f, DoorRotation, Color);
if (bIsConnected)
{
// Arrow (there is a room on the other side OR in the editor preview)
FVector ArrowVector = (Dungeon::FlipDoorArrow() ? -1.0f : 1.0f) * FVector(Dungeon::DoorArrowLength(), 0.0f, 0.0f);
DrawDebugDirectionalArrow(World, DoorPosition, DoorPosition + DoorRotation * ArrowVector, Dungeon::DoorArrowHeadSize(), Color);
}
else
{
// Cross (there is no room on the other side AND NOT in the editor preview)
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, Color);
DrawDebugLine(World, DoorPosition - HalfSizeConjugate, DoorPosition + HalfSizeConjugate, Color);
}
#endif // ENABLE_DRAW_DEBUG
}
#endif // !UE_BUILD_SHIPPING
// ############ FBoxMinAndMax ##############
const FBoxMinAndMax FBoxMinAndMax::Invalid {FIntVector::ZeroValue, FIntVector::ZeroValue};
FBoxMinAndMax::FBoxMinAndMax(const FIntVector& A, const FIntVector& B)
{
SetMinAndMax(A, B);
}
void FBoxMinAndMax::SetMinAndMax(const FIntVector& A, const FIntVector& B)
{
Min = IntVector::Min(A, B);
Max = IntVector::Max(A, B);
}
bool FBoxMinAndMax::IsValid() const
{
return Max.X > Min.X && Max.Y > Min.Y && Max.Z > Min.Z;
}
FIntVector FBoxMinAndMax::GetSize() const
{
return Max - Min;
}
FBoxCenterAndExtent FBoxMinAndMax::ToCenterAndExtent() const
{
FVector Center = 0.5f * FVector(Min + Max - FIntVector(1, 1, 0));
FVector Extent = 0.5f * FVector(Max - Min);
return FBoxCenterAndExtent(Center, Extent.GetAbs());
}
bool FBoxMinAndMax::IsInside(const FIntVector& Cell) const
{
return (Cell.X >= Min.X) && (Cell.X < Max.X)
&& (Cell.Y >= Min.Y) && (Cell.Y < Max.Y)
&& (Cell.Z >= Min.Z) && (Cell.Z < Max.Z);
}
bool FBoxMinAndMax::IsInside(const FBoxMinAndMax& Other) const
{
return (Other.Min.X >= Min.X) && (Other.Max.X <= Max.X)
&& (Other.Min.Y >= Min.Y) && (Other.Max.Y <= Max.Y)
&& (Other.Min.Z >= Min.Z) && (Other.Max.Z <= Max.Z);
}
void FBoxMinAndMax::Rotate(const EDoorDirection& Rot)
{
FIntVector Compensation = FIntVector::ZeroValue;
switch (Rot)
{
case EDoorDirection::East:
Compensation.X = 1;
break;
case EDoorDirection::West:
Compensation.Y = 1;
break;
case EDoorDirection::South:
Compensation.X = 1;
Compensation.Y = 1;
break;
default:
break;
}
const FIntVector A = ::Rotate(Min, Rot) + Compensation;
const FIntVector B = ::Rotate(Max, Rot) + Compensation;
Min = IntVector::Min(A, B);
Max = IntVector::Max(A, B);
}
void FBoxMinAndMax::Extend(const FBoxMinAndMax& Other)
{
if (Min != Max)
{
Min = IntVector::Min(Min, Other.Min);
Max = IntVector::Max(Max, Other.Max);
}
else
{
Min = Other.Min;
Max = Other.Max;
}
}
FString FBoxMinAndMax::ToString() const
{
return FString::Printf(TEXT("[(%d, %d, %d), (%d, %d, %d)]"), Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z);
}
FIntVector FBoxMinAndMax::GetClosestPoint(const FIntVector& Point) const
{
return IntVector::Min(Max, IntVector::Max(Min, Point));
}
bool FBoxMinAndMax::Overlap(const FBoxMinAndMax& A, const FBoxMinAndMax& B)
{
return (A.Max.X > B.Min.X && A.Min.X < B.Max.X)
&& (A.Max.Y > B.Min.Y && A.Min.Y < B.Max.Y)
&& (A.Max.Z > B.Min.Z && A.Min.Z < B.Max.Z);
}
FBoxMinAndMax& FBoxMinAndMax::operator+=(const FIntVector& X)
{
Min += X;
Max += X;
return *this;
}
FBoxMinAndMax& FBoxMinAndMax::operator-=(const FIntVector& X)
{
Min -= X;
Max -= X;
return *this;
}
FBoxMinAndMax FBoxMinAndMax::operator+(const FIntVector& X) const
{
FBoxMinAndMax NewBox(*this);
NewBox += X;
return NewBox;
}
FBoxMinAndMax FBoxMinAndMax::operator-(const FIntVector& X) const
{
FBoxMinAndMax NewBox(*this);
NewBox -= X;
return NewBox;
}
bool FBoxMinAndMax::operator==(const FBoxMinAndMax& Other) const
{
return (Min == Other.Min) && (Max == Other.Max);
}
bool FBoxMinAndMax::operator!=(const FBoxMinAndMax& Other) const
{
return !FBoxMinAndMax::operator==(Other);
}
FBoxMinAndMax Rotate(const FBoxMinAndMax& Box, const EDoorDirection& Rot)
{
FBoxMinAndMax NewBox(Box);
NewBox.Rotate(Rot);
return NewBox;
}
FRoomCandidate FRoomCandidate::Invalid = FRoomCandidate();
@@ -0,0 +1,321 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonSettings.h"
#include "Room.h"
#include "ProceduralDungeonLog.h"
#include "Math/GenericOctree.h" // FBoxCenterAndExtent
#include "ProceduralDungeonTypes.h" // FBoxMinAndMax
#include "Kismet/GameplayStatics.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "GameFramework/GameState.h"
#include "Components/PrimitiveComponent.h"
FIntVector IntVector::Min(const FIntVector& A, const FIntVector& B)
{
return FIntVector(FMath::Min(A.X, B.X), FMath::Min(A.Y, B.Y), FMath::Min(A.Z, B.Z));
}
FIntVector IntVector::Max(const FIntVector& A, const FIntVector& B)
{
return FIntVector(FMath::Max(A.X, B.X), FMath::Max(A.Y, B.Y), FMath::Max(A.Z, B.Z));
}
void IntVector::MinMax(const FIntVector& A, const FIntVector& B, FIntVector& OutMin, FIntVector& OutMax)
{
OutMin = Min(A, B);
OutMax = Max(A, B);
}
FVector Dungeon::ToWorldLocation(FIntVector RoomPoint, const FVector RoomUnit)
{
return RoomUnit * (FVector(RoomPoint) - FVector(0.5f, 0.5f, 0.0f));
}
FVector Dungeon::ToWorldVector(FIntVector RoomPoint, const FVector RoomUnit)
{
return RoomUnit * FVector(RoomPoint);
}
FBoxCenterAndExtent Dungeon::ToWorld(const FBoxMinAndMax& Box, const FVector RoomUnit, const FTransform& Transform)
{
return ToWorld(Box.ToCenterAndExtent(), RoomUnit, Transform);
}
FBoxCenterAndExtent Dungeon::ToWorld(const FBoxCenterAndExtent& Box, const FVector RoomUnit, const FTransform& Transform)
{
const FVector Center = Transform.TransformPositionNoScale(RoomUnit * Box.Center);
const FVector Extent = Transform.TransformVector(RoomUnit * Box.Extent).GetAbs();
return FBoxCenterAndExtent(Center, Extent);
}
FIntVector Dungeon::ToRoomLocation(FVector WorldPoint, const FVector RoomUnit)
{
const int X = FMath::RoundToInt(0.5f + (WorldPoint.X) / RoomUnit.X);
const int Y = FMath::RoundToInt(0.5f + (WorldPoint.Y) / RoomUnit.Y);
const int Z = FMath::RoundToInt((WorldPoint.Z) / RoomUnit.Z);
return FIntVector(X, Y, Z);
}
FIntVector Dungeon::ToRoomVector(FVector WorldVector, const FVector RoomUnit)
{
const int X = FMath::RoundToInt(WorldVector.X / RoomUnit.X);
const int Y = FMath::RoundToInt(WorldVector.Y / RoomUnit.Y);
const int Z = FMath::RoundToInt(WorldVector.Z / RoomUnit.Z);
return FIntVector(X, Y, Z);
}
FVector Dungeon::SnapPoint(FVector Point, const FVector RoomUnit)
{
return ToWorldLocation(ToRoomLocation(Point, RoomUnit), RoomUnit);
}
// =================== Plugin's Settings ========================
FVector Dungeon::RoomUnit()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->RoomUnit;
}
FVector Dungeon::DefaultDoorSize()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->DoorSize;
}
FColor Dungeon::DefaultDoorColor()
{
return FColor::Blue;
}
float Dungeon::DoorOffset()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->DoorOffset;
}
bool Dungeon::OcclusionCulling()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->OcclusionCulling;
}
bool Dungeon::UseLegacyOcclusion()
{
//const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
//return Settings->LegacyOcclusion;
return true;
}
uint32 Dungeon::OcclusionDistance()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->OcclusionDistance;
}
bool Dungeon::OccludeDynamicActors()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->OccludeDynamicActors;
}
bool Dungeon::DrawDebug()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->DrawDebug;
}
bool Dungeon::DrawOnlyWhenEditingRoom()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->bDrawOnlyWhenEditingRooms;
}
bool Dungeon::ShowRoomOrigin()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->ShowRoomOrigin;
}
bool Dungeon::FlipDoorArrow()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->bFlipDoorArrowSide;
}
float Dungeon::DoorArrowLength()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->DoorArrowLength;
}
float Dungeon::DoorArrowHeadSize()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->DoorArrowHeadSize;
}
bool Dungeon::CanLoop()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->CanLoop;
}
ECollisionChannel Dungeon::RoomObjectType()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->RoomObjectType;
}
uint32 Dungeon::MaxGenerationTryBeforeGivingUp()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->MaxGenerationTry;
}
uint32 Dungeon::MaxRoomPlacementTryBeforeGivingUp()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->MaxRoomPlacementTry;
}
int32 Dungeon::RoomLimit()
{
const UProceduralDungeonSettings* Settings = GetDefault<UProceduralDungeonSettings>();
return Settings->RoomLimit;
}
void Dungeon::EnableOcclusionCulling(bool Enable)
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
Settings->OcclusionCulling = Enable;
}
void Dungeon::SetOcclusionDistance(int32 Distance)
{
UProceduralDungeonSettings* Settings = GetMutableDefault<UProceduralDungeonSettings>();
Settings->OcclusionDistance = Distance;
}
uint64 Concat(uint32 A, uint32 B)
{
return (static_cast<uint64>(A) << 32) | static_cast<uint64>(B);
}
uint32 Random::Guid2Seed(FGuid Guid, int64 Salt)
{
//// CAUTION: This function must not be modified if not necessary!
//// Or else it will break behaviour compatibility with previous versions of the plugin!
// Using PCG-RXS-M-XS found in this paper: https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf (p. 45)
// Creating a 64bit states from the 128bit Guid and the salt.
const uint64 Part1 = Concat(Guid.A, Guid.B);
const uint64 Part2 = Concat(Guid.C, Guid.D);
uint64 State = Part1 ^ Part2 ^ Salt;
// Applying PCG-RXS-M-XS to create much more variations from the salt.
const uint8 Count = State >> 59; // Extracting the highest 5 bits for the random xorshift below (64-5=59)
State ^= State >> (5 + Count); // [RXS] Random xorshift (at least 5 to leave the highest 5 bits untouched)
State *= 12605985483714917081u; // [M] Multiplication with a really big odd number
State ^= State >> 43; // [XS] Xorshifting 1/3 of the top bits to the 1/3 of the lower bits
return static_cast<uint32>(State ^ (State >> 32)); // Folding the top half for the result on the bottom half to convert into a 32bit output.
}
void ObjectUtils::DispatchToObjectAndSubobjects(UObject* Obj, TFunction<void(UObject*)> Func, int32 Depth)
{
// Calls the function on the object itself.
DungeonLog_Debug("[%d] - Dispatch function to object '%s'.", Depth, *GetNameSafe(Obj));
Func(Obj);
// Get all direct subobjects of this object.
TArray<UObject*> Subobjects;
GetObjectsWithOuter(Obj, Subobjects, /*bIncludeNestedObjects = */ false);
++Depth;
// Recursively dispatch to all subobjects found.
for (UObject* Sub : Subobjects)
{
DispatchToObjectAndSubobjects(Sub, Func, Depth);
}
}
FBox ActorUtils::GetActorBoundingBoxForRooms(AActor* Actor, const FTransform& DungeonTransform)
{
if (!IsValid(Actor))
{
DungeonLog_Error("Invalid Actor provided.");
return FBox(ForceInit);
}
// Copied from AActor::GetComponentsBoundingBox but check also collision response with the room object type
FBox ActorBox(ForceInit);
Actor->ForEachComponent<UPrimitiveComponent>(/*bIncludeFromChildActors = */ false, [&](const UPrimitiveComponent* Component) {
if (Component->IsRegistered()
&& Component->IsCollisionEnabled()
&& Component->GetCollisionResponseToChannel(Dungeon::RoomObjectType()) != ECollisionResponse::ECR_Ignore)
{
ActorBox += Component->Bounds.GetBox();
}
});
ActorBox = ActorBox.InverseTransformBy(DungeonTransform);
return ActorBox;
}
APlayerController* ActorUtils::GetPlayerControllerFromPlayerId(const UObject* WorldContextObject, int32 PlayerId)
{
UWorld* World = WorldContextObject->GetWorld();
if (!IsValid(World))
return nullptr;
AGameStateBase* GameState = World->GetGameState();
if (!IsValid(GameState))
return nullptr;
const auto* StatePtr = GameState->PlayerArray.FindByPredicate([PlayerId](const APlayerState* State) { return State->GetPlayerId() == PlayerId; });
if (StatePtr == nullptr)
return nullptr;
const APlayerState* State = *StatePtr;
if (!IsValid(State))
return nullptr;
#if UE_VERSION_OLDER_THAN(5, 0, 0)
const APawn* Pawn = State->GetPawn();
if (!IsValid(Pawn))
return nullptr;
return Cast<APlayerController>(Pawn->GetController());
#else
return State->GetPlayerController();
#endif
}
UObject* ActorUtils::GetInterfaceImplementer(AActor* Actor, TSubclassOf<UInterface> InterfaceClass)
{
if (!IsValid(Actor) || InterfaceClass == nullptr)
return nullptr;
UClass* ActorClass = Actor->GetClass();
if (ActorClass && ActorClass->ImplementsInterface(InterfaceClass))
return Actor;
const auto Components = Actor->GetComponentsByInterface(InterfaceClass);
if (Components.Num() <= 0)
return nullptr;
if (Components.Num() > 1)
{
DungeonLog_WarningSilent("Multiple components have a %s interface. Only one used, remove the unnecessary ones to prevent any confusion!", *GetNameSafe(InterfaceClass));
}
return Components[0];
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2021 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "QueueOrStack.h"
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ReadOnlyRoom.h"
@@ -0,0 +1,125 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "ReplicableObject.h"
#include "Engine/ActorChannel.h"
#include "GameFramework/Actor.h"
#include "ProceduralDungeonLog.h"
#include "Utils/ReplicationUtils.h"
namespace
{
const TCHAR* GetWithPredicate(const TCHAR* Str, bool bPredicate)
{
return (bPredicate) ? Str : TEXT("");
}
} //namespace
bool UReplicableObject::ReplicateSubobject(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
// Ensure that nested objects are replicated BEFORE!
// thus any reference to them inside this object will be correct.
bool bWroteSomething = ReplicateSubobjects(Channel, Bunch, RepFlags);
bWroteSomething |= Channel->ReplicateSubobject(this, *Bunch, *RepFlags);
return bWroteSomething;
}
void UReplicableObject::RegisterAsReplicable(bool bRegister, FRegisterSubObjectParams Params)
{
#if UE_WITH_SUBOBJECT_LIST
AActor* Owner = GetTypedOuter<AActor>();
if (!IsValid(Owner))
{
ensureMsgf(false, TEXT("Trying to %sregister %s as replicable subobject but actor owner is invalid."), ::GetWithPredicate(TEXT("un"), !bRegister), *GetNameSafe(this));
return;
}
if (!Owner->HasAuthority())
return;
// Ignores if owner does not use registered subobject list
if (!Owner->IsUsingRegisteredSubObjectList())
return;
// Ignore if the object is already registered/unregistered
if (Owner->IsReplicatedSubObjectRegistered(this) == bRegister)
return;
DungeonLog_InfoSilent("%s Replicable Subobject: %s", (bRegister) ? TEXT("Register") : TEXT("Unregister"), *GetNameSafe(this));
if (bRegister)
Owner->AddReplicatedSubObject(this, Params.NetCondition);
else
{
switch (Params.UnregisterType)
{
case EUnregisterSubObjectType::Unregister:
Owner->RemoveReplicatedSubObject(this);
break;
#if UE_VERSION_NEWER_THAN(5, 2, 0)
case EUnregisterSubObjectType::Destroy:
Owner->DestroyReplicatedSubObjectOnRemotePeers(this);
break;
case EUnregisterSubObjectType::TearOff:
Owner->TearOffReplicatedSubObjectOnRemotePeers(this);
break;
#endif
default:
checkf(false, TEXT("Unimplemented case."));
break;
}
}
RegisterReplicableSubobjects(bRegister);
#endif
}
#if UE_WITH_IRIS
void UReplicableObject::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
{
// Build descriptors and allocate PropertyReplicationFragments for this object
UE::Net::FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
}
#endif // UE_WITH_IRIS
bool UReplicableObject::ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
return false;
}
UWorld* UReplicableObject::GetWorld() const
{
UObject* Outer = GetOuter();
if (!Outer)
return nullptr;
return Outer->GetWorld();
}
bool UReplicableObject::HasAuthority() const
{
AActor* Owner = GetTypedOuter<AActor>();
if (!Owner)
return false;
return Owner->HasAuthority();
}
FString UReplicableObject::GetAuthorityName() const
{
return HasAuthority() ? TEXT("Server") : TEXT("Client");
}
void UReplicableObject::WakeUpOwnerActor()
{
AActor* Owner = GetTypedOuter<AActor>();
if (!IsValid(Owner))
return;
if (!Owner->HasAuthority())
return;
Owner->FlushNetDormancy();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomConnection.h"
#include "Room.h"
#include "RoomData.h"
#include "Door.h"
#include "Engine/World.h"
#include "Utils/ReplicationUtils.h"
#include "ProceduralDungeonLog.h"
#include "Engine/Engine.h"
#include "Interfaces/RoomContainer.h"
#include "Interfaces/DoorInterface.h"
#include "Utils/DungeonSaveUtils.h"
#include "DungeonGeneratorBase.h"
#include "ProceduralDungeonCustomVersion.h"
#include "Components/DoorComponent.h"
void URoomConnection::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
FDoRepLifetimeParams Params;
Params.bIsPushBased = true;
// InitialOnly is not called on newly created subobjects after the InitialCond of actor owner has already been called!!!
//Params.Condition = COND_InitialOnly;
DOREPLIFETIME_WITH_PARAMS(URoomConnection, ID, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomA, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomADoorId, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomB, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, RoomBDoorId, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, DoorInstance, Params);
DOREPLIFETIME_WITH_PARAMS(URoomConnection, DoorState, Params);
}
bool URoomConnection::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
{
check(!SaveData.IsValid());
SaveData = MakeUnique<FSaveData>();
SaveData->Version = Record.GetUnderlyingArchive().CustomVer(FProceduralDungeonCustomVersion::GUID);
if (!bIsLoading)
{
TSoftObjectPtr<URoom> RoomAWeak(RoomA.Get());
TSoftObjectPtr<URoom> RoomBWeak(RoomB.Get());
SaveData->RoomAID = RoomA.IsValid() ? RoomA->GetRoomID() : -1;
SaveData->RoomBID = RoomB.IsValid() ? RoomB->GetRoomID() : -1;
// Serializing the door instance's properties only during the save here.
// The properties will be loaded back when the door is spawned.
if (DoorInstance.IsValid())
SerializeUObject(SaveData->DoorSavedData, DoorInstance.Get(), false);
}
Record.EnterField(AR_FIELD_NAME("RoomA")) << SaveData->RoomAID;
Record.EnterField(AR_FIELD_NAME("RoomB")) << SaveData->RoomBID;
Record.EnterField(AR_FIELD_NAME("DoorClass")) << DoorClass;
Record.EnterField(AR_FIELD_NAME("DoorProperties")) << SaveData->DoorSavedData;
if (bIsLoading)
{
DungeonLog_Debug("[%s] Loaded DoorClass: %s", *GetNameSafe(this), *GetNameSafe(DoorClass));
}
if (!bIsLoading)
{
// No need to keep the saved data after saving.
SaveData.Reset();
}
return true;
}
bool URoomConnection::FixupReferences(UObject* Context)
{
check(SaveData.IsValid());
auto* RoomContainer = Cast<IRoomContainer>(Context);
if (RoomContainer == nullptr)
{
DungeonLog_Error("[%s] Failed to fixup RoomConnection references: Context is not a RoomContainer.", *GetNameSafe(this));
return false;
}
RoomA = RoomContainer->GetRoomByIndex(SaveData->RoomAID);
RoomB = RoomContainer->GetRoomByIndex(SaveData->RoomBID);
DungeonLog_Debug("[%s] Fixed up RoomConnection references: RoomA=%s, RoomB=%s", *GetNameSafe(this), *GetNameSafe(RoomA.Get()), *GetNameSafe(RoomB.Get()));
return true;
}
void URoomConnection::PreSaveDungeon_Implementation()
{
if (DoorInstance.IsValid() && DoorInstance->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PreSaveDungeon(DoorInstance.Get());
}
}
void URoomConnection::PostLoadDungeon_Implementation()
{
SaveData.Reset();
if (DoorInstance.IsValid() && DoorInstance->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PostLoadDungeon(DoorInstance.Get());
}
}
int32 URoomConnection::GetID() const
{
return ID;
}
const TWeakObjectPtr<URoom> URoomConnection::GetRoomA() const
{
return RoomA;
}
const TWeakObjectPtr<URoom> URoomConnection::GetRoomB() const
{
return RoomB;
}
const URoom* URoomConnection::GetRoomA_BP() const
{
return RoomA.Get();
}
const URoom* URoomConnection::GetRoomB_BP() const
{
return RoomB.Get();
}
int32 URoomConnection::GetRoomADoorId() const
{
return RoomADoorId;
}
int32 URoomConnection::GetRoomBDoorId() const
{
return RoomBDoorId;
}
TWeakObjectPtr<URoom> URoomConnection::GetOtherRoom(const URoom* FromRoom) const
{
check(FromRoom == RoomA || FromRoom == RoomB);
return (FromRoom == RoomA) ? RoomB : RoomA;
}
int32 URoomConnection::GetOtherDoorId(const URoom* FromRoom) const
{
check(FromRoom == RoomA || FromRoom == RoomB);
return (FromRoom == RoomA) ? RoomBDoorId : RoomADoorId;
}
bool URoomConnection::IsDoorInstanced() const
{
return DoorInstance.IsValid();
}
AActor* URoomConnection::GetDoorInstance() const
{
return DoorInstance.Get();
}
FVector URoomConnection::GetDoorLocation(bool bIgnoreGeneratorTransform) const
{
FDoorDef DoorDef;
const AActor* Generator = nullptr;
const URoomData* Data = nullptr;
if (RoomA.IsValid())
{
DoorDef = RoomA->GetDoorDef(RoomADoorId);
Generator = RoomA->Generator();
Data = RoomA->GetRoomData();
}
else if (RoomB.IsValid())
{
DoorDef = RoomB->GetDoorDef(RoomBDoorId);
Generator = RoomB->Generator();
Data = RoomB->GetRoomData();
}
else
{
return FVector();
}
check(IsValid(Data));
FVector Location = FDoorDef::GetRealDoorPosition(DoorDef, Data->GetRoomUnit());
if (!bIgnoreGeneratorTransform && IsValid(Generator))
Location = Generator->GetTransform().TransformPositionNoScale(Location);
return Location;
}
FRotator URoomConnection::GetDoorRotation(bool bIgnoreGeneratorTransform) const
{
FDoorDef DoorDef;
const AActor* Generator = nullptr;
bool bFinalFlipped = bFlipped;
if (RoomA.IsValid())
{
DoorDef = RoomA->GetDoorDef(RoomADoorId);
Generator = RoomA->Generator();
}
else if (RoomB.IsValid())
{
DoorDef = RoomB->GetDoorDef(RoomBDoorId);
Generator = RoomB->Generator();
bFinalFlipped = !bFlipped;
}
else
{
return FRotator::ZeroRotator;
}
FQuat Rotation = FDoorDef::GetRealDoorRotation(DoorDef, bFinalFlipped);
if (!bIgnoreGeneratorTransform && IsValid(Generator))
Rotation = Generator->GetTransform().InverseTransformRotation(Rotation);
return Rotation.Rotator();
}
bool URoomConnection::IsDoorOpen() const
{
return DoorState.bIsOpen;
}
bool URoomConnection::IsDoorLocked() const
{
return DoorState.bIsLocked;
}
void URoomConnection::SetDoorOpen(bool bOpen)
{
DoorState.bIsOpen = bOpen;
MARK_PROPERTY_DIRTY_FROM_NAME(URoomConnection, DoorState, this);
}
void URoomConnection::SetDoorLocked(bool bLocked)
{
DoorState.bIsLocked = bLocked;
MARK_PROPERTY_DIRTY_FROM_NAME(URoomConnection, DoorState, this);
}
void URoomConnection::SetDoorClass(TSubclassOf<AActor> InDoorClass, bool bInFlipped)
{
DoorClass = InDoorClass;
bFlipped = bInFlipped;
}
AActor* URoomConnection::InstantiateDoor(UWorld* World, AActor* Owner, bool bUseOwnerTransform)
{
if (!IsValid(World))
{
DungeonLog_Error("Can't spanw door: Invalid World.");
return nullptr;
}
if (DoorInstance.IsValid())
{
DungeonLog_WarningSilent("Door already instanced.");
return DoorInstance.Get();
}
if (nullptr == DoorClass)
return nullptr;
// Get next room
const URoom* Room = RoomA.Get();
int32 DoorId = RoomADoorId;
bool bFinalFlipped = bFlipped;
if (!IsValid(Room))
{
Room = RoomB.Get();
DoorId = RoomBDoorId;
bFinalFlipped = !bFinalFlipped; // Flipped is inverted when using RoomB instead of RoomA
}
FDoorDef DoorDef = Room->GetDoorDef(DoorId);
FVector InstanceDoorPos = FDoorDef::GetRealDoorPosition(DoorDef, Room->GetRoomData()->GetRoomUnit());
FQuat InstanceDoorRot = FDoorDef::GetRealDoorRotation(DoorDef, bFinalFlipped);
if (bUseOwnerTransform && IsValid(Owner))
{
InstanceDoorPos = Owner->GetActorTransform().TransformPositionNoScale(InstanceDoorPos);
InstanceDoorRot = Owner->GetActorTransform().TransformRotation(InstanceDoorRot);
}
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = Owner;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* Door = GetWorld()->SpawnActor<AActor>(DoorClass, InstanceDoorPos, InstanceDoorRot.Rotator(), SpawnParams);
if (!IsValid(Door))
{
DungeonLog_Error("Failed to spawn Door, make sure you set door actor to always spawning.");
return nullptr;
}
UObject* Implementer = ActorUtils::GetInterfaceImplementer<UDoorInterface>(Door);
if (IsValid(Implementer))
IDoorInterface::Execute_SetRoomConnection(Implementer, this);
DoorInstance = Door;
if (SaveData.IsValid())
{
// Load door data back if we have some saved data.
SerializeUObject(SaveData->DoorSavedData, Door, true);
DungeonLog_InfoSilent("Loaded saved data for door '%s'", *GetNameSafe(Door));
if (SaveData->Version < FProceduralDungeonCustomVersion::DoorLogicRefactored)
{
if (ADoor* LegacyDoorActor = Cast<ADoor>(Door))
{
DoorState.bIsOpen = LegacyDoorActor->GetLegacyShouldBeOpen();
DoorState.bIsLocked = LegacyDoorActor->GetLegacyShouldBeLocked();
if (UDoorComponent* Component = LegacyDoorActor->FindComponentByClass<UDoorComponent>())
{
Component->SetAlwaysVisible(LegacyDoorActor->GetLegacyAlwaysVisible());
Component->SetAlwaysUnlocked(LegacyDoorActor->GetLegacyAlwaysUnlocked());
Component->SetDoorType(LegacyDoorActor->GetLegacyDoorType());
}
else
{
DungeonLog_WarningSilent("Legacy door actor '%s' does not have a DoorComponent, can't migrate its AlwaysVisible and AlwaysUnlocked properties.", *GetNameSafe(LegacyDoorActor));
}
DungeonLog_InfoSilent("Migrated from old door actor '%s': Open:%d | Locked:%d", *GetNameSafe(LegacyDoorActor), DoorState.bIsOpen, DoorState.bIsLocked);
}
}
}
return Door;
}
void URoomConnection::DestroyDoor()
{
if (!DoorInstance.IsValid())
return;
DoorInstance->Destroy();
DoorInstance.Reset();
}
void URoomConnection::OnRep_ID()
{
DungeonLog_Debug("[%s] RoomConnection '%s' ID replicated: %d", *GetAuthorityName(), *GetNameSafe(this), ID);
}
void URoomConnection::OnRep_RoomA()
{
DungeonLog_Debug("[%s] RoomConnection '%s' RoomA replicated: %s", *GetAuthorityName(), *GetNameSafe(this), *GetNameSafe(RoomA.Get()));
}
void URoomConnection::OnRep_RoomB()
{
DungeonLog_Debug("[%s] RoomConnection '%s' RoomB replicated: %s", *GetAuthorityName(), *GetNameSafe(this), *GetNameSafe(RoomB.Get()));
}
void URoomConnection::OnRep_DoorState()
{
DungeonLog_Debug("[%s] RoomConnection '%s' DoorState replicated: Open:%d | Locked:%d", *GetAuthorityName(), *GetNameSafe(this), DoorState.bIsOpen, DoorState.bIsLocked);
}
URoom* URoomConnection::GetOtherRoom(const URoomConnection* Conn, const URoom* FromRoom)
{
return (Conn != nullptr) ? Conn->GetOtherRoom(FromRoom).Get() : nullptr;
}
int32 URoomConnection::GetOtherDoorId(const URoomConnection* Conn, const URoom* FromRoom)
{
return (Conn != nullptr) ? Conn->GetOtherDoorId(FromRoom) : -1;
}
AActor* URoomConnection::GetDoorInstance(const URoomConnection* Conn)
{
return (Conn != nullptr) ? Conn->DoorInstance.Get() : nullptr;
}
UDoorType* URoomConnection::GetDoorType(const URoomConnection* Conn)
{
if (!IsValid(Conn))
{
return nullptr;
}
URoom* Room = Conn->RoomA.Get();
int32 DoorId = Conn->RoomADoorId;
if (!IsValid(Room))
{
Room = Conn->RoomB.Get();
DoorId = Conn->RoomBDoorId;
}
if (!IsValid(Room))
{
return nullptr;
}
return Room->GetRoomData()->Doors[DoorId].Type;
}
void URoomConnection::GetBothDoorTypes(const URoomConnection* Conn, UDoorType*& DoorTypeA, UDoorType*& DoorTypeB)
{
if (!IsValid(Conn))
{
return;
}
DoorTypeA = Conn->RoomA.IsExplicitlyNull() ? nullptr : Conn->RoomA->GetRoomData()->Doors[Conn->RoomADoorId].Type;
DoorTypeB = Conn->RoomB.IsExplicitlyNull() ? nullptr : Conn->RoomB->GetRoomData()->Doors[Conn->RoomBDoorId].Type;
}
URoomConnection* URoomConnection::CreateConnection(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB, UObject* Outer, int32 IdInOuter)
{
// At least one room and its door index must be valid.
const bool bIsAValid = IsValid(RoomA) && RoomA->IsDoorIndexValid(DoorA);
const bool bIsBValid = IsValid(RoomB) && RoomB->IsDoorIndexValid(DoorB);
check(bIsAValid || bIsBValid);
URoomConnection* NewConnection = NewObject<URoomConnection>(Outer);
check(NewConnection != nullptr);
NewConnection->ID = IdInOuter;
NewConnection->RoomA = RoomA;
NewConnection->RoomADoorId = DoorA;
NewConnection->RoomB = RoomB;
NewConnection->RoomBDoorId = DoorB;
if (bIsAValid)
{
RoomA->SetConnection(DoorA, NewConnection);
}
if (bIsBValid)
{
RoomB->SetConnection(DoorB, NewConnection);
}
return NewConnection;
}
@@ -0,0 +1,15 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomConstraints/RoomConstraint.h"
#include "ProceduralDungeonLog.h"
bool URoomConstraint::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
{
DungeonLog_WarningSilent("Constraint %s does not implements the URoomConstraint::Check function!", *GetNameSafe(this));
return true;
}
@@ -0,0 +1,30 @@
// Copyright Benoit Pelletier 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomConstraints/RoomConstraint_CountLimit.h"
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonTypes.h"
#include "RoomData.h"
#include "DungeonGraph.h"
bool URoomConstraint_CountLimit::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
{
if (!IsValid(RoomData))
{
DungeonLog_Error("Invalid RoomData passed to %s", *GetNameSafe(this));
return false;
}
if (!IsValid(Dungeon))
{
DungeonLog_Error("Invalid Dungeon passed to %s", *GetNameSafe(this));
return false;
}
const int32 ActualCount = Dungeon->CountRoomData(RoomData);
return ActualCount < MaxCount;
}
@@ -0,0 +1,15 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomConstraints/RoomConstraint_Direction.h"
#include "ProceduralDungeonLog.h"
#include "Room.h"
bool URoomConstraint_Direction::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
{
return AllowedDirections.Contains(Direction);
}
@@ -0,0 +1,25 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomConstraints/RoomConstraint_Location.h"
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonTypes.h"
#include "RoomData.h"
bool URoomConstraint_Location::Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const
{
if (!IsValid(RoomData))
{
DungeonLog_Error("Invalid RoomData passed to %s", *GetNameSafe(this));
return false;
}
const FBoxMinAndMax BoundingLimits = Limits.GetBox();
FBoxMinAndMax RoomBounds = RoomData->GetIntBounds();
RoomBounds.Rotate(Direction);
return BoundingLimits.IsInside(RoomBounds + Location);
}
@@ -0,0 +1,126 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomCustomData.h"
#include "Components/ActorComponent.h"
#include "Components/SceneComponent.h"
#include "RoomLevel.h"
#include "Utils/DungeonSaveUtils.h"
UActorComponent* CreateComponentOnInstance(AActor* ActorInstance, TSubclassOf<UActorComponent> ComponentClass, USceneComponent* OptionalParentForSceneComponent = nullptr)
{
if (!ActorInstance || !ComponentClass)
return nullptr;
// Don't create component if on a template actor (CDO or Archetype)
if (ActorInstance->IsTemplate())
return nullptr;
// For multiplayer games, create component only on server if component is replicating
const UActorComponent* ComponentCDO = ComponentClass->GetDefaultObject<UActorComponent>();
if (ComponentCDO->GetIsReplicated() && !ActorInstance->HasAuthority())
return nullptr;
UActorComponent* NewComp = NewObject<UActorComponent>(ActorInstance, ComponentClass);
// Handles USceneComponent attachment
if (USceneComponent* NewSceneComp = Cast<USceneComponent>(NewComp))
{
USceneComponent* ParentComponent = OptionalParentForSceneComponent;
if (!ParentComponent)
ParentComponent = ActorInstance->GetRootComponent();
if (ParentComponent)
{
// Parent component should always be owned by the passed in actor instance!
check(ParentComponent->GetOwner() != ActorInstance);
NewSceneComp->SetupAttachment(ParentComponent);
}
else
{
// Set component directly as root if no root component on the actor
ActorInstance->SetRootComponent(NewSceneComp);
}
}
NewComp->RegisterComponent();
return NewComp;
}
void URoomCustomData::CreateLevelComponent(ARoomLevel* LevelActor)
{
if (!LevelComponent)
return;
LevelComponentInstance = CreateComponentOnInstance(LevelActor, LevelComponent);
if (!LevelComponentInstance.IsValid())
{
DungeonLog_Error("Failed to create component '%s' on room level '%s'.", *GetNameSafe(LevelComponent), *GetNameSafe(LevelActor));
}
}
bool URoomCustomData::SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading)
{
// Nothing more to serialize if no component
if (nullptr == LevelComponent)
return true;
SavedData = MakeUnique<FSaveData>();
if (!bIsLoading)
{
// Serialize component data
if (LevelComponentInstance.IsValid())
{
SerializeUObject(SavedData->ComponentData, LevelComponentInstance.Get(), false);
}
}
Record.EnterField(AR_FIELD_NAME("ComponentData")) << SavedData->ComponentData;
if (!bIsLoading)
{
// No need to keep the data after saving
SavedData.Reset();
}
return true;
}
void URoomCustomData::PreSaveDungeon_Implementation()
{
if (!LevelComponentInstance.IsValid())
return;
if (LevelComponentInstance->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PreSaveDungeon(LevelComponentInstance.Get());
}
}
void URoomCustomData::PostLoadDungeon_Implementation()
{
if (!SavedData.IsValid())
return;
// Deserialize component data
if (LevelComponentInstance.IsValid())
{
SerializeUObject(SavedData->ComponentData, LevelComponentInstance.Get(), true);
}
else
{
DungeonLog_Error("Failed to deserialize component data for '%s' in room custom data '%s'", *GetNameSafe(LevelComponent), *GetNameSafe(this));
}
SavedData.Reset();
if (LevelComponentInstance.IsValid() && LevelComponentInstance->Implements<UDungeonSaveInterface>())
{
IDungeonSaveInterface::Execute_PostLoadDungeon(LevelComponentInstance.Get());
}
}
@@ -0,0 +1,377 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomData.h"
#include "RoomLevel.h"
#include "RoomCustomData.h"
#include "ProceduralDungeonTypes.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "ProceduralDungeonCustomVersion.h"
#include "DoorType.h"
#include "DungeonSettings.h"
#include "RoomConstraints/RoomConstraint.h"
#include "Serialization/CustomVersion.h"
#include "DrawDebugHelpers.h"
#if !USE_LEGACY_DATA_VALIDATION
#include "Misc/DataValidation.h"
#endif
URoomData::URoomData()
: Super()
{
BoundingBoxes.Add({FIntVector(0), FIntVector(1)});
}
void URoomData::Serialize(FArchive& Ar)
{
Super::Serialize(Ar);
Ar.UsingCustomVersion(FProceduralDungeonCustomVersion::GUID);
// If loading an old version, we need to handle the migration
if (Ar.IsLoading())
{
const int32 DungeonVersion = Ar.CustomVer(FProceduralDungeonCustomVersion::GUID);
if (DungeonVersion < FProceduralDungeonCustomVersion::RoomDataBoundingBoxesMigration)
{
DungeonLog_Warning("Migrating RoomData '%s' from legacy FirstPoint/SecondPoint to BoundingBoxes.", *GetName());
if (BoundingBoxes.Num() == 0)
BoundingBoxes.AddDefaulted();
BoundingBoxes[0].SetMinAndMax(FirstPoint, SecondPoint);
// Clear the legacy data after migration
FirstPoint = FIntVector(0);
SecondPoint = FIntVector(0);
}
}
}
const FDoorDef& URoomData::GetDoorDef(int32 DoorIndex) const
{
if (DoorIndex >= 0 && DoorIndex < Doors.Num())
return Doors[DoorIndex];
return FDoorDef::Invalid;
}
bool URoomData::HasCompatibleDoor(const FDoorDef& DoorData) const
{
for (int i = 0; i < Doors.Num(); ++i)
{
if (FDoorDef::AreCompatible(Doors[i], DoorData))
return true;
}
return false;
}
void URoomData::GetCompatibleDoors(const FDoorDef& DoorData, TArray<int>& CompatibleDoors) const
{
CompatibleDoors.Empty();
for (int i = 0; i < Doors.Num(); ++i)
{
if (FDoorDef::AreCompatible(Doors[i], DoorData))
CompatibleDoors.Add(i);
}
}
bool URoomData::HasDoorOfType(UDoorType* DoorType) const
{
for (const auto& Door : Doors)
{
if (Door.Type == DoorType)
return true;
}
return false;
}
bool URoomData::HasAnyDoorOfType(const TArray<UDoorType*>& DoorTypes) const
{
for (const auto& Door : Doors)
{
if (DoorTypes.Contains(Door.Type))
return true;
}
return false;
}
bool URoomData::HasAllDoorOfType(const TArray<UDoorType*>& DoorTypes) const
{
TSet<UDoorType*> AllDoorTypes(DoorTypes);
for (const auto& Door : Doors)
{
AllDoorTypes.Remove(Door.Type);
}
return AllDoorTypes.Num() <= 0;
}
bool URoomData::HasCustomData(TSubclassOf<URoomCustomData> CustomDataClass) const
{
return CustomData.Contains(CustomDataClass);
}
bool URoomData::HasAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const
{
for (const auto& CustomDataClass : CustomDataList)
{
if (HasCustomData(CustomDataClass))
return true;
}
return false;
}
bool URoomData::HasAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomDataList) const
{
for (const auto& CustomDataClass : CustomDataList)
{
if (!HasCustomData(CustomDataClass))
return false;
}
return true;
}
void URoomData::InitializeRoom_Implementation(URoom* Room, UDungeonGraph* Dungeon) const
{
}
void URoomData::CleanupRoom_Implementation(URoom* Room, UDungeonGraph* Dungeon) const
{
}
FVector URoomData::GetRoomUnit() const
{
return UDungeonSettings::GetRoomUnit(GetSettings());
}
bool URoomData::DoesPassAllConstraints(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction)
{
if (!IsValid(RoomData))
{
return false;
}
for (const URoomConstraint* Constraint : RoomData->Constraints)
{
if (!IsValid(Constraint))
{
DungeonLog_WarningSilent("Invalid constraint detected in RoomData %s", *GetNameSafe(RoomData));
continue;
}
if (!Constraint->Check(Dungeon, RoomData, Location, Direction))
return false;
}
return true;
}
FBoxCenterAndExtent URoomData::GetBounds(FTransform Transform) const
{
return Dungeon::ToWorld(GetIntBounds(), GetRoomUnit(), Transform);
}
FBoxCenterAndExtent URoomData::GetSubBounds(int32 Index, FTransform Transform) const
{
check(Index >= 0 && Index < BoundingBoxes.Num());
const FBoxMinAndMax& Box = BoundingBoxes[Index];
return Dungeon::ToWorld(Box, GetRoomUnit(), Transform);
}
FIntVector URoomData::GetSize() const
{
return GetIntBounds().GetSize();
}
int URoomData::GetVolume() const
{
const FVoxelBounds Bounds = GetVoxelBounds();
return Bounds.GetCellCount();
}
FBoxMinAndMax URoomData::GetIntBounds() const
{
return GetVoxelBounds().GetBounds();
}
FVoxelBounds URoomData::GetVoxelBounds() const
{
if (CachedVoxelBounds.IsValid())
return CachedVoxelBounds;
// For now, just convert the IntBounds into a VoxelBounds.
// When the VoxelBounds editor will be implemented, we will just have to return the serialized VoxelBounds.
for (const FBoxMinAndMax& Box : BoundingBoxes)
{
CachedVoxelBounds.AddBox(Box);
}
CachedVoxelBounds.ResetToWalls();
// Add the doors
for (int i = 0; i < Doors.Num(); ++i)
{
const FDoorDef& Door = Doors[i];
const FIntVector DoorPos = Door.Position;
const EDoorDirection DoorDir = Door.Direction;
CachedVoxelBounds.SetCellConnection(DoorPos, FVoxelBounds::EDirection(DoorDir), FVoxelBoundsConnection(Door.Type));
}
return CachedVoxelBounds;
}
bool URoomData::IsRoomInBounds(const FBoxMinAndMax& Bounds, int DoorIndex, const FDoorDef& DoorDungeonPos) const
{
const FIntVector BoundSize = Bounds.GetSize();
if (BoundSize.X == 0 || BoundSize.Y == 0 || BoundSize.Z == 0)
return false;
if (DoorIndex < 0 || DoorIndex >= Doors.Num())
return false;
const FDoorDef& Door = Doors[DoorIndex];
FBoxMinAndMax RoomBounds = GetIntBounds();
RoomBounds -= Door.Position;
RoomBounds.Rotate(DoorDungeonPos.Direction - Door.Direction);
RoomBounds += DoorDungeonPos.Position;
return Bounds.IsInside(RoomBounds);
}
#if !(UE_BUILD_SHIPPING) || WITH_EDITOR
bool URoomData::IsDoorValid(int DoorIndex) const
{
check(DoorIndex >= 0 && DoorIndex < Doors.Num());
bool bFacingNoBox = true;
bool bAtLeastInABox = false;
const FDoorDef& DoorDef = Doors[DoorIndex];
for (const auto& Box : BoundingBoxes)
{
bAtLeastInABox |= Box.IsInside(DoorDef.Position);
const FIntVector FacingCell = DoorDef.Position + ToIntVector(DoorDef.Direction);
bFacingNoBox &= !Box.IsInside(FacingCell);
}
return bAtLeastInABox && bFacingNoBox;
}
bool URoomData::IsDoorDuplicate(int DoorIndex) const
{
check(DoorIndex >= 0 && DoorIndex < Doors.Num());
for (int i = 0; i < Doors.Num(); ++i)
{
if (DoorIndex != i && Doors[i] == Doors[DoorIndex])
return true;
}
return false;
}
void URoomData::DrawDebug(const UWorld* World, const FTransform& Transform, const FColor& Color)
{
if (!IsValid(World))
return;
for (const FBoxMinAndMax& BoundingBox : BoundingBoxes)
{
const FBoxCenterAndExtent Box = Dungeon::ToWorld(BoundingBox, GetRoomUnit(), Transform);
DrawDebugBox(World, Box.Center, Box.Extent, FQuat::Identity, Color, false, -1.0f, SDPG_World, 2.0f);
}
}
#endif // !(UE_BUILD_SHIPPING) || WITH_EDITOR
#if WITH_EDITOR
#if USE_LEGACY_DATA_VALIDATION
#define VALIDATION_LOG_ERROR(Msg) ValidationErrors.Add(Msg)
EDataValidationResult URoomData::IsDataValid(TArray<FText>& ValidationErrors)
#else
#define VALIDATION_LOG_ERROR(Msg) Context.AddError(Msg)
EDataValidationResult URoomData::IsDataValid(FDataValidationContext& Context) const
#endif // USE_LEGACY_DATA_VALIDATION
{
#if USE_LEGACY_DATA_VALIDATION
EDataValidationResult Result = Super::IsDataValid(ValidationErrors);
#else
EDataValidationResult Result = Super::IsDataValid(Context);
#endif // USE_LEGACY_DATA_VALIDATION
if (!IsAsset() || Result == EDataValidationResult::Invalid)
return Result;
// Check the Level validity
if (Level.IsNull())
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has no level set. You have to set up a room level."), *GetName())));
Result = EDataValidationResult::Invalid;
}
if (BoundingBoxes.Num() <= 0)
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" should have at least one bounding box."), *GetName())));
Result = EDataValidationResult::Invalid;
}
else
{
// Check if all bounding boxes are valid
for (const FBoxMinAndMax& Box : BoundingBoxes)
{
if (!Box.IsValid())
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has an invalid bounding box: %s."), *GetName(), *Box.ToString())));
Result = EDataValidationResult::Invalid;
}
}
}
if (Doors.Num() <= 0)
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" should have at least one door."), *GetName())));
Result = EDataValidationResult::Invalid;
}
else
{
for (int i = 0; i < Doors.Num(); ++i)
{
// Check if all doors are valid
if (!IsDoorValid(i))
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has invalid door: %s."), *GetName(), *Doors[i].ToString())));
Result = EDataValidationResult::Invalid;
}
// Check if there are no duplicated doors
if (IsDoorDuplicate(i))
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has duplicated doors: %s."), *GetName(), *Doors[i].ToString())));
Result = EDataValidationResult::Invalid;
}
}
}
// Check if CustomData Set does not have null value
if (CustomData.Contains(nullptr))
{
VALIDATION_LOG_ERROR(FText::FromString(FString::Printf(TEXT("Room data \"%s\" has value None in CustomData."), *GetName())));
Result = EDataValidationResult::Invalid;
}
return Result;
}
#undef VALIDATION_LOG_ERROR
void URoomData::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
OnPropertiesChanged.Broadcast(this);
// Reset the cached VoxelBounds to trigger a new computation.
CachedVoxelBounds = FVoxelBounds();
}
#endif // WITH_EDITOR
@@ -0,0 +1,324 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomLevel.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "EngineUtils.h"
#include "Kismet/GameplayStatics.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/GameState.h"
#include "GameFramework/Pawn.h"
#include "ProceduralDungeonTypes.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "Room.h"
#include "RoomData.h"
#include "Door.h"
#include "DungeonGenerator.h"
#include "Components/BoxComponent.h"
#include "RoomVisibilityComponent.h"
#include "RoomVisitor.h"
#include "Utils/ReplicationUtils.h"
#if WITH_EDITOR
bool ARoomLevel::bIsDungeonEditorMode = false;
#endif
ARoomLevel::ARoomLevel(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = false;
SetNetUpdateFrequency(10);
bIsInit = false;
Room = nullptr;
DungeonTransform = FTransform::Identity;
// Create a root component to have a world position
SetRootComponent(CreateDefaultSubobject<USceneComponent>(FName("Root")));
}
// Use this for initialization
void ARoomLevel::Init(URoom* _Room)
{
check(IsValid(_Room));
Room = _Room;
bIsInit = false;
DungeonTransform = Room->Generator()->GetDungeonTransform();
// Update the room's bounding box for occlusion culling (also the red box drawn in debug)
UpdateBounds();
}
void ARoomLevel::BeginPlay()
{
Super::BeginPlay();
if (!IsValid(Room))
{
DungeonLog_Warning("RoomLevel was not spawned by a DungeonGenerator. It's fine for testing a room but occlusion will not work properly. Consider unchecking \"Occlude Dynamic Actors\" in the plugin's settings.");
return;
}
// Check if the data that spawned this level correspond to the data provided in blueprint
if (Data != Room->GetRoomData())
{
DungeonLog_Error("RoomLevel's Data does not match RoomData's Level [Data \"%s\" | Level \"%s\"]. Debug Draw will be incorrect.", *GetNameSafe(Room->GetRoomData()), *GetName());
}
// Create trigger box to track dynamic actors inside the room with IRoomVisitor
RoomTrigger = NewObject<UBoxComponent>(this, UBoxComponent::StaticClass(), FName("Room Trigger"));
RoomTrigger->RegisterComponent();
RoomTrigger->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
RoomTrigger->SetCanEverAffectNavigation(false);
RoomTrigger->SetGenerateOverlapEvents(true);
RoomTrigger->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
RoomTrigger->SetCollisionObjectType(Dungeon::RoomObjectType());
RoomTrigger->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Overlap);
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Ignore);
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Ignore);
RoomTrigger->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
static const uint8 FirstChannel = static_cast<uint8>(ECollisionChannel::ECC_GameTraceChannel1);
static const uint8 LastChannel = static_cast<uint8>(ECollisionChannel::ECC_GameTraceChannel18);
for (uint8 Channel = FirstChannel; Channel <= LastChannel; ++Channel)
{
ETraceTypeQuery TraceChannel = UEngineTypes::ConvertToTraceType(static_cast<ECollisionChannel>(Channel));
if (TraceChannel != ETraceTypeQuery::TraceTypeQuery_MAX)
{
RoomTrigger->SetCollisionResponseToChannel(static_cast<ECollisionChannel>(Channel), ECollisionResponse::ECR_Ignore);
}
}
RoomTrigger->OnComponentBeginOverlap.AddDynamic(this, &ARoomLevel::OnTriggerBeginOverlap);
RoomTrigger->OnComponentEndOverlap.AddDynamic(this, &ARoomLevel::OnTriggerEndOverlap);
// Update trigger box to have the room's bounds
FBoxCenterAndExtent LocalBounds = Room->GetLocalBounds();
RoomTrigger->SetRelativeLocationAndRotation(LocalBounds.Center, FQuat::Identity);
RoomTrigger->SetBoxExtent(LocalBounds.Extent, true);
SetActorsVisible(Room->IsVisible());
// Create dynamic components from the RoomCustomData
Room->CreateLevelComponents(this);
bIsInit = true;
}
void ARoomLevel::EndPlay(EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
Room = nullptr;
}
// Update is called once per frame
void ARoomLevel::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
#if ENABLE_DRAW_DEBUG
// @TODO: Place the debug draw in an editor module of the plugin?
const bool bIsEditingRoom = GetLevel() == GetWorld()->PersistentLevel;
bool bShouldDrawDebug = Dungeon::DrawDebug() && (!Dungeon::DrawOnlyWhenEditingRoom() || bIsEditingRoom);
#if WITH_EDITOR
// Force debug drawing when the editor is in DungeonEditor mode
bShouldDrawDebug |= bIsDungeonEditorMode;
#endif
if (IsValid(Data) && bShouldDrawDebug)
{
const bool bIsRoomValid = (Room != nullptr);
const bool bIsRoomDataValid = bIsRoomValid && (Data == Room->GetRoomData());
const FTransform& RoomTransform = (bIsRoomValid) ? Room->GetTransform() : FTransform::Identity;
const bool bIsRoomLocked = bIsRoomValid && Room->IsLocked();
UpdateBounds();
// Cache world
const UWorld* World = GetWorld();
// @TODO: is it still needed now?
// Pivot
if (Dungeon::ShowRoomOrigin())
DrawDebugSphere(World, DungeonTransform.TransformPositionNoScale(RoomTransform.GetLocation()), 100.0f, 4, FColor::Magenta);
// Room bounds
Data->DrawDebug(World, RoomTransform * DungeonTransform, IsPlayerInside() ? FColor::Green : FColor::Red);
if (bIsRoomLocked)
{
FBox Box = Bounds.GetBox();
const FVector& Min = Box.Min;
const FVector& Max = Box.Max;
#ifdef T
static_assert(false, "T macro is already defined! Please change its name to avoid potential conflicts");
#endif
#define T(POINT) DungeonTransform.TransformPositionNoScale(POINT)
DrawDebugLine(World, T(Min), T(Max), FColor::Red);
DrawDebugLine(World, T(FVector(Min.X, Min.Y, Max.Z)), T(FVector(Max.X, Max.Y, Min.Z)), FColor::Red);
DrawDebugLine(World, T(FVector(Min.X, Max.Y, Max.Z)), T(FVector(Max.X, Min.Y, Min.Z)), FColor::Red);
DrawDebugLine(World, T(FVector(Min.X, Max.Y, Min.Z)), T(FVector(Max.X, Min.Y, Max.Z)), FColor::Red);
#undef T
}
// Doors
for (int i = 0; i < Data->GetNbDoor(); i++)
{
const bool bIsConnected = !bIsRoomValid || (bIsRoomDataValid && Room->IsConnected(i));
const bool bIsDoorValid = Data->IsDoorValid(i) && !Data->IsDoorDuplicate(i);
FDoorDef::DrawDebug(World, Data->Doors[i], Data->GetRoomUnit(), RoomTransform * DungeonTransform, /*bIncludeOffset = */ true, bIsDoorValid && bIsConnected);
}
}
#endif // ENABLE_DRAW_DEBUG
}
bool ARoomLevel::IsPlayerInside(const APlayerController* PlayerController)
{
return IsValid(Room) ? Room->IsPlayerInside(PlayerController) : false;
}
bool ARoomLevel::IsVisible()
{
return IsValid(Room) ? Room->IsVisible() : true;
}
bool ARoomLevel::IsLocked()
{
return IsValid(Room) ? Room->IsLocked() : false;
}
void ARoomLevel::Lock(bool lock)
{
if (IsValid(Room))
Room->Lock(lock);
}
void ARoomLevel::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
TriggerActor(OtherActor, true);
}
void ARoomLevel::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
TriggerActor(OtherActor, false);
}
FVector ARoomLevel::GetBoundsCenter() const
{
return DungeonTransform.TransformPositionNoScale(Bounds.Center);
}
FVector ARoomLevel::GetBoundsExtent() const
{
return Bounds.Extent;
}
void ARoomLevel::UpdateBounds()
{
if (IsValid(Room))
{
Bounds = Room->GetBounds();
}
else if (IsValid(Data))
{
Bounds = Data->GetBounds();
}
}
void ARoomLevel::SetActorsVisible(bool Visible)
{
if (!Dungeon::OcclusionCulling())
{
// Force visibility when occlusion culling is disabled
Visible = true;
}
ULevel* Level = GetLevel();
if (IsValid(Level))
{
for (AActor* Actor : Level->Actors)
{
if (!IsValid(Actor))
continue;
// HACK: Don't manage replicated actors as their ActorHiddenInGame is replicated
// and will mess up the actor visibility on clients!
if (Actor->GetIsReplicated())
continue;
// Discard explicitly ignored actors.
// They can have a (Static) Room Visibility Component attached to have a custom occlusion management.
if (Actor->ActorHasTag(FName("Ignore Room Culling")))
continue;
Actor->SetActorHiddenInGame(!Visible);
}
}
// Notify the change (useful for RoomVisibilityComponent)
VisibilityChangedEvent.Broadcast(this, Visible);
}
void ARoomLevel::UpdateVisitor(UObject* Visitor, bool IsInside)
{
check(Visitor->Implements<URoomVisitor>());
if (IsInside && !Visitors.Contains(Visitor))
{
Visitors.Add(Visitor);
IRoomVisitor::Execute_OnRoomEnter(Visitor, this);
}
else if (!IsInside && Visitors.Contains(Visitor))
{
Visitors.Remove(Visitor);
IRoomVisitor::Execute_OnRoomExit(Visitor, this);
}
}
void ARoomLevel::TriggerActor(AActor* Actor, bool IsInTrigger)
{
if (!IsValid(Actor))
return;
// Call the interface on the actor itself
if (Actor->Implements<URoomVisitor>())
{
UpdateVisitor(Actor, IsInTrigger);
}
// Call the interface on its components too
TArray<UActorComponent*, FDefaultAllocator> VisitorComps = Actor->GetComponentsByInterface(URoomVisitor::StaticClass());
for (UActorComponent* VisitorComp : VisitorComps)
{
check(VisitorComp);
UpdateVisitor(VisitorComp, IsInTrigger);
}
if (IsInTrigger)
ActorEnterRoomEvent.Broadcast(this, Actor);
else
ActorExitRoomEvent.Broadcast(this, Actor);
}
void ARoomLevel::PostInitProperties()
{
Super::PostInitProperties();
UpdateBounds();
}
#if WITH_EDITOR
void ARoomLevel::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
UpdateBounds();
OnPropertiesChanged.Broadcast(this);
}
#endif
@@ -0,0 +1,28 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomVisibilityComponent.h"
#include "ProceduralDungeonUtils.h"
#include "ProceduralDungeonLog.h"
#include "RoomLevel.h"
URoomVisibilityComponent::URoomVisibilityComponent()
: Super()
{
}
void URoomVisibilityComponent::OnRoomEnter_Implementation(ARoomLevel* RoomLevel)
{
DungeonLog_Debug("[Visibility] '%s' Enters Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
RegisterVisibilityDelegate(RoomLevel, true);
}
void URoomVisibilityComponent::OnRoomExit_Implementation(ARoomLevel* RoomLevel)
{
DungeonLog_Debug("[Visibility] '%s' Exits Room: %s", *GetNameSafe(GetOwner()), *GetNameSafe(RoomLevel));
RegisterVisibilityDelegate(RoomLevel, false);
}
@@ -0,0 +1,8 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "RoomVisitor.h"
@@ -0,0 +1,264 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBoxMinAndMaxTest, "ProceduralDungeon.Types.BoxMinAndMax", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FBoxMinAndMaxTest::RunTest(const FString& Parameters)
{
// Constructor Test
{
FBoxMinAndMax Box0;
FBoxMinAndMax Box1(FIntVector(-1), FIntVector(1));
FBoxMinAndMax Box2(FIntVector(3, 2, 1), FIntVector(-1, -2, -3));
FBoxMinAndMax Box3(Box2);
FBoxMinAndMax Box4(FIntVector(-3, 2, -1), FIntVector(1, -2, 3));
FBoxMinAndMax Box5 = FBoxMinAndMax::Invalid;
TestEqual(TEXT("Default Constructor Min == 0"), Box0.GetMin(), FIntVector(0));
TestEqual(TEXT("Default Constructor Max == 1"), Box0.GetMax(), FIntVector(1));
TestEqual(TEXT("Constructor (-1,1) Min == -1"), Box1.GetMin(), FIntVector(-1));
TestEqual(TEXT("Constructor (-1,1) Max == 1"), Box1.GetMax(), FIntVector(1));
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box2.GetMin(), FIntVector(-1, -2, -3));
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box2.GetMax(), FIntVector(3, 2, 1));
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box3.GetMin(), FIntVector(-1, -2, -3));
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box3.GetMax(), FIntVector(3, 2, 1));
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Min == (-3,-2,-1)"), Box4.GetMin(), FIntVector(-3, -2, -1));
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Max == (1,2,3)"), Box4.GetMax(), FIntVector(1, 2, 3));
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMin(), FIntVector(0));
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMax(), FIntVector(0));
TestFalse(TEXT("Invalid Box is not Valid"), Box5.IsValid());
}
// Size Test
{
FBoxMinAndMax Box0(FIntVector(0), FIntVector(0));
FBoxMinAndMax Box1(FIntVector(0), FIntVector(1));
FBoxMinAndMax Box2(FIntVector(0), FIntVector(-1));
FBoxMinAndMax Box3(FIntVector(-1), FIntVector(0));
FBoxMinAndMax Box4(FIntVector(1), FIntVector(0));
FBoxMinAndMax Box5(FIntVector(0, 0, 0), FIntVector(3, 4, 5));
FBoxMinAndMax Box6(FIntVector(3, 4, 5), FIntVector(-5, -4, -3));
TestEqual(TEXT("Box(0, 0).GetSize() == (0,0,0)"), Box0.GetSize(), FIntVector(0));
TestEqual(TEXT("Box(0, 1).GetSize() == (1,1,1)"), Box1.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(0, -1).GetSize() == (1,1,1)"), Box2.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(-1, 0).GetSize() == (1,1,1)"), Box3.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(1, 0).GetSize() == (1,1,1)"), Box4.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(0, (3, 4, 5)).GetSize() == (3,4,5)"), Box5.GetSize(), FIntVector(3, 4, 5));
TestEqual(TEXT("Box((3, 4, 5), (-5, -4, -3)).GetSize() == (8,8,8)"), Box6.GetSize(), FIntVector(8));
}
// Overlap Test
{
FBoxMinAndMax Box1(FIntVector(-2, -3, -4), FIntVector(2, 3, 4));
FBoxMinAndMax Box2(FIntVector(3, 0, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box3(FIntVector(2, 3, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box4(FIntVector(2, 2, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box5(FIntVector(1, 2, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box6(FIntVector(-3, 0, 0), FIntVector(-5, -5, -5));
FBoxMinAndMax Box7(FIntVector(-2, -2, 0), FIntVector(-5, -5, -5));
FBoxMinAndMax Box8(FIntVector(-1, -2, 0), FIntVector(-5, -5, -5));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-3,-4), (2,3,4)) overlap"), FBoxMinAndMax::Overlap(Box1, Box1));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((3,0,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box2));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,3,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box3));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,2,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box4));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((1,2,0), (5,5,5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box5));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-3,0,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box6));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-2,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box7));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-1,-2,0), (-5,-5,-5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box8));
FBoxMinAndMax BoxA(FIntVector(0), FIntVector(1));
FBoxMinAndMax BoxB(FIntVector(-2), FIntVector(5));
TestTrue(TEXT("Box(0, 1) and Box(-2, 5) overlap"), FBoxMinAndMax::Overlap(BoxA, BoxB));
}
// Rotation Test
{
FBoxMinAndMax Box0(FIntVector(0), FIntVector(1));
FBoxMinAndMax RotBox0N = Rotate(Box0, EDoorDirection::North);
FBoxMinAndMax RotBox0E = Rotate(Box0, EDoorDirection::East);
FBoxMinAndMax RotBox0S = Rotate(Box0, EDoorDirection::South);
FBoxMinAndMax RotBox0W = Rotate(Box0, EDoorDirection::West);
TestEqual(TEXT("Rotate(Box(0,1), N).Min == 0"), RotBox0N.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), N).Max == 1"), RotBox0N.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), E).Min == 0"), RotBox0E.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), E).Max == 1"), RotBox0E.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), S).Min == 0"), RotBox0S.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), S).Max == 1"), RotBox0S.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), W).Min == 0"), RotBox0W.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), W).Max == 1"), RotBox0W.GetMax(), FIntVector(1));
FBoxMinAndMax Box1(FIntVector(-1, 0, -1), FIntVector(3, 1, 1));
FBoxMinAndMax RotBox1N = Rotate(Box1, EDoorDirection::North);
FBoxMinAndMax RotBox1E = Rotate(Box1, EDoorDirection::East);
FBoxMinAndMax RotBox1S = Rotate(Box1, EDoorDirection::South);
FBoxMinAndMax RotBox1W = Rotate(Box1, EDoorDirection::West);
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Min == (-1,0,-1)"), RotBox1N.GetMin(), FIntVector(-1, 0, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Max == (3,1,1)"), RotBox1N.GetMax(), FIntVector(3, 1, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Min == (0,-1,-1)"), RotBox1E.GetMin(), FIntVector(0, -1, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Max == (1,3,1)"), RotBox1E.GetMax(), FIntVector(1, 3, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Min == (-2,0,-1)"), RotBox1S.GetMin(), FIntVector(-2, 0, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Max == (2,1,1)"), RotBox1S.GetMax(), FIntVector(2, 1, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Min == (0,-2,-1)"), RotBox1W.GetMin(), FIntVector(0, -2, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Max == (1,2,1)"), RotBox1W.GetMax(), FIntVector(1, 2, 1));
}
// Extend Test
{
FBoxMinAndMax BoxToExtend;
FBoxMinAndMax Box0({0, 0, 0}, {1, 1, 1});
FBoxMinAndMax Box1 = Box0 + FIntVector(1, 2, 3); // offseted box
FBoxMinAndMax Box2({-1, -2, -3}, {4, 5, 6});
BoxToExtend.Extend(Box0);
TestEqual(TEXT("Extend Box Step 1"), BoxToExtend, Box0);
// Extend the box to contain the provided box
BoxToExtend.Extend(Box1);
TestEqual(TEXT("Extend Box Step 2"), BoxToExtend, FBoxMinAndMax({0, 0, 0}, {2, 3, 4}));
BoxToExtend.Extend(Box2);
TestEqual(TEXT("Extend Box Step 3"), BoxToExtend, Box2);
// The extended box should not change when using a box entirely contained in it.
BoxToExtend.Extend(Box0);
TestEqual(TEXT("Extend Box Step 4"), BoxToExtend, Box2);
}
// IsInside(FBoxMinAndMax) Test
{
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
FBoxMinAndMax Box(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Completely inside, no coincident face
TestTrue(*FString::Printf(TEXT("Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Positive X
Box += FIntVector(6, 0, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(1, 0, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[X+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(1, 0, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Positive Y
Box += FIntVector(0, 6, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 1, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[Y+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 1, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Positive Z
Box += FIntVector(0, 0, 6); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, 1); // Intersecting
TestFalse(*FString::Printf(TEXT("[Z+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, 1); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative X
Box += FIntVector(-3, 0, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(-1, 0, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[X-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(-1, 0, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative Y
Box += FIntVector(0, -4, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, -2, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[Y-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, -1, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative Z
Box += FIntVector(0, 0, -5); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, -3); // Intersecting
TestFalse(*FString::Printf(TEXT("[Z-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, -1); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
}
// IsInside(FIntVector) Test
{
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
FIntVector Cell {0};
// Completely inside, no coincident face
TestTrue(*FString::Printf(TEXT("Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive X
Cell = {6, 0, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {7, 0, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative X
Cell = {-4, 0, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {-5, 0, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive Y
Cell = {0, 7, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 8, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative Y
Cell = {0, -5, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, -6, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive Z
Cell = {0, 0, 8}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 0, 9}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative Z
Cell = {0, 0, -6}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 0, -7}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,31 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "VoxelBounds/VoxelBounds.h"
#include "CustomScoreCallbacks.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomScoreCallback : public UObject
{
GENERATED_BODY()
public:
UFUNCTION()
bool ZeroScore(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
{
Score = 0;
return true;
}
UFUNCTION()
bool NeverPass(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
{
return false;
}
};
@@ -0,0 +1,61 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Utils/CompatUtils.h"
#include "DungeonSaveClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class USaveTestObject : public UObject, public IDungeonCustomSerialization, public IDungeonSaveInterface
{
GENERATED_BODY()
public:
//~ Begin IDungeonCustomSerialization Interface
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("X") : TEXT("C");
Record.EnterField(AR_FIELD_NAME("NativeTest")) << TestSerializeObjectFunction;
return true;
}
//~ End IDungeonCustomSerialization Interface
//~ Begin IDungeonSaveInterface Interface
virtual void PreSaveDungeon_Implementation() override
{
OrderOfExecution += TEXT("A");
}
virtual void DungeonPreSerialize_Implementation(bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("W") : TEXT("B");
}
virtual void DungeonPostSerialize_Implementation(bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("Y") : TEXT("D");
}
virtual void PostLoadDungeon_Implementation() override
{
OrderOfExecution += TEXT("Z");
}
//~ End IDungeonSaveInterface Interface
public:
UPROPERTY(SaveGame)
int32 TestSaveGameFlag {0};
int32 TestSerializeObjectFunction {0};
UPROPERTY();
FString OrderOfExecution {};
};
@@ -0,0 +1,35 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "RoomConstraints/RoomConstraint.h"
#include "RoomConstraintChildClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UConstraintPass : public URoomConstraint
{
GENERATED_BODY()
public:
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
{
return true;
}
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UConstraintFail : public URoomConstraint
{
GENERATED_BODY()
public:
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
{
return false;
}
};
@@ -0,0 +1,29 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "RoomCustomData.h"
#include "RoomCustomDataChildClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataA : public URoomCustomData
{
GENERATED_BODY()
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataB : public URoomCustomData
{
GENERATED_BODY()
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataC : public URoomCustomData
{
GENERATED_BODY()
};
@@ -0,0 +1,91 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "DoorType.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDefTest, "ProceduralDungeon.Types.DoorDef", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDoorDefTest::RunTest(const FString& Parameters)
{
CREATE_DATA_ASSET(UDoorType, DoorTypeA);
CREATE_DATA_ASSET(UDoorType, DoorTypeB);
// Constructor Test
{
FDoorDef Door0;
FDoorDef Door1(FDoorDef::Invalid);
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef Door3(Door2);
TestTrue(TEXT("Default Constructor makes valid door"), Door0.IsValid());
TestFalse(TEXT("Copy constructor of invalid door mus makes an invalid door"), Door1.IsValid());
TestTrue(TEXT("Constructor (1,2,3) South DoorTypeA is valid"), Door2.IsValid());
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Position == (1,2,3)"), Door2.Position, {1, 2, 3});
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Direction == South"), Door2.Direction, EDoorDirection::South);
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Type == DoorTypeA"), Door2.Type, DoorTypeA.Get());
TestTrue(TEXT("Copy Constructor of valid door must be valid too"), Door3.IsValid());
TestEqual(TEXT("Copy Constructor must carry over position"), Door3.Position, Door2.Position);
TestEqual(TEXT("Copy Constructor must carry over direction"), Door3.Direction, Door2.Direction);
TestEqual(TEXT("Copy Constructor must carry over type"), Door3.Type, Door2.Type);
}
// Compatibility Test
{
FDoorDef Door0({0, 0, 0}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef Door1({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeB.Get());
FDoorDef Door3;
TestTrue(TEXT("Door0 is compatible with Door1"), FDoorDef::AreCompatible(Door0, Door1));
TestFalse(TEXT("Door0 is not compatible with Door2"), FDoorDef::AreCompatible(Door0, Door2));
TestFalse(TEXT("Door0 is not compatible with Door3"), FDoorDef::AreCompatible(Door0, Door3));
TestFalse(TEXT("Door1 is not compatible with Door2"), FDoorDef::AreCompatible(Door1, Door2));
TestFalse(TEXT("Door1 is not compatible with Door3"), FDoorDef::AreCompatible(Door1, Door3));
TestFalse(TEXT("Door2 is not compatible with Door3"), FDoorDef::AreCompatible(Door2, Door3));
}
// Opposite Test
{
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef Door1 = Door0.GetOpposite();
TestTrue(TEXT("Opposite door is valid"), Door1.IsValid());
TestEqual(TEXT("Opposite of North is South"), Door1.Direction, EDoorDirection::South);
TestEqual(TEXT("Opposite cell of (1,2,3)[North] is (2,2,3)"), Door1.Position, {2, 2, 3});
TestEqual(TEXT("Opposite type is the same"), Door1.Type, DoorTypeA.Get());
TestTrue(TEXT("Opposite door is compatible with original"), FDoorDef::AreCompatible(Door0, Door1));
}
// Transform Test
{
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef TransformedDoor0({-1, 3, 6}, EDoorDirection::East, DoorTypeA.Get());
FDoorDef TransformedDoor1({0, 0, 6}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef TransformedDoor2({3, 1, 6}, EDoorDirection::West, DoorTypeA.Get());
TestEqual(TEXT("Transformation {(1, 2, 3), East} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::East), TransformedDoor0);
TestEqual(TEXT("Transformation {(1, 2, 3), South} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::South), TransformedDoor1);
TestEqual(TEXT("Transformation {(1, 2, 3), West} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::West), TransformedDoor2);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), East} is correct"), FDoorDef::InverseTransform(TransformedDoor0, {1, 2, 3}, EDoorDirection::East), Door0);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), South} is correct"), FDoorDef::InverseTransform(TransformedDoor1, {1, 2, 3}, EDoorDirection::South), Door0);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), West} is correct"), FDoorDef::InverseTransform(TransformedDoor2, {1, 2, 3}, EDoorDirection::West), Door0);
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,109 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDirectionTest, "ProceduralDungeon.Types.DoorDirection", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDoorDirectionTest::RunTest(const FString& Parameters)
{
// Adding directions is correct
{
TestEqual(TEXT("North + North = North"), EDoorDirection::North + EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("North + East = East"), EDoorDirection::North + EDoorDirection::East, EDoorDirection::East);
TestEqual(TEXT("North + South = South"), EDoorDirection::North + EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("North + West = West"), EDoorDirection::North + EDoorDirection::West, EDoorDirection::West);
TestEqual(TEXT("East + North = East"), EDoorDirection::East + EDoorDirection::North, EDoorDirection::East);
TestEqual(TEXT("East + East = South"), EDoorDirection::East + EDoorDirection::East, EDoorDirection::South);
TestEqual(TEXT("East + South = West"), EDoorDirection::East + EDoorDirection::South, EDoorDirection::West);
TestEqual(TEXT("East + West = North"), EDoorDirection::East + EDoorDirection::West, EDoorDirection::North);
TestEqual(TEXT("South + North = South"), EDoorDirection::South + EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("South + East = West"), EDoorDirection::South + EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("South + South = North"), EDoorDirection::South + EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("South + West = East"), EDoorDirection::South + EDoorDirection::West, EDoorDirection::East);
TestEqual(TEXT("West + North = West"), EDoorDirection::West + EDoorDirection::North, EDoorDirection::West);
TestEqual(TEXT("West + East = North"), EDoorDirection::West + EDoorDirection::East, EDoorDirection::North);
TestEqual(TEXT("West + South = East"), EDoorDirection::West + EDoorDirection::South, EDoorDirection::East);
TestEqual(TEXT("West + West = South"), EDoorDirection::West + EDoorDirection::West, EDoorDirection::South);
}
// Subtracting directions is correct
{
TestEqual(TEXT("North - North = North"), EDoorDirection::North - EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("North - East = West"), EDoorDirection::North - EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("North - South = South"), EDoorDirection::North - EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("North - West = East"), EDoorDirection::North - EDoorDirection::West, EDoorDirection::East);
TestEqual(TEXT("East - North = East"), EDoorDirection::East - EDoorDirection::North, EDoorDirection::East);
TestEqual(TEXT("East - East = North"), EDoorDirection::East - EDoorDirection::East, EDoorDirection::North);
TestEqual(TEXT("East - South = West"), EDoorDirection::East - EDoorDirection::South, EDoorDirection::West);
TestEqual(TEXT("East - West = South"), EDoorDirection::East - EDoorDirection::West, EDoorDirection::South);
TestEqual(TEXT("South - North = South"), EDoorDirection::South - EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("South - East = East"), EDoorDirection::South - EDoorDirection::East, EDoorDirection::East);
TestEqual(TEXT("South - South = North"), EDoorDirection::South - EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("South - West = West"), EDoorDirection::South - EDoorDirection::West, EDoorDirection::West);
TestEqual(TEXT("West - North = West"), EDoorDirection::West - EDoorDirection::North, EDoorDirection::West);
TestEqual(TEXT("West - East = South"), EDoorDirection::West - EDoorDirection::East, EDoorDirection::South);
TestEqual(TEXT("West - South = East"), EDoorDirection::West - EDoorDirection::South, EDoorDirection::East);
TestEqual(TEXT("West - West = North"), EDoorDirection::West - EDoorDirection::West, EDoorDirection::North);
}
// Negating directions are correct
{
TestEqual(TEXT("-North = North"), -EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("-East = West"), -EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("-South = South"), -EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("-West = East"), -EDoorDirection::West, EDoorDirection::East);
}
// Opposite directions are correct
{
TestEqual(TEXT("~North = South"), ~EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("~East = West"), ~EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("~South = North"), ~EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("~West = East"), ~EDoorDirection::West, EDoorDirection::East);
}
// Incrementing/decrementing directions are correct
{
EDoorDirection direction {EDoorDirection::North};
TestEqual(TEXT("++North = East"), ++direction, EDoorDirection::East);
TestEqual(TEXT("++East = South"), ++direction, EDoorDirection::South);
TestEqual(TEXT("++South = West"), ++direction, EDoorDirection::West);
TestEqual(TEXT("++West = North"), ++direction, EDoorDirection::North);
direction = EDoorDirection::North;
TestEqual(TEXT("--North = West"), --direction, EDoorDirection::West);
TestEqual(TEXT("--West = South"), --direction, EDoorDirection::South);
TestEqual(TEXT("--South = East"), --direction, EDoorDirection::East);
TestEqual(TEXT("--East = North"), --direction, EDoorDirection::North);
}
// Boolean testing directions
{
TestFalse(TEXT("!North = false"), !EDoorDirection::North);
TestFalse(TEXT("!East = false"), !EDoorDirection::East);
TestFalse(TEXT("!South = false"), !EDoorDirection::South);
TestFalse(TEXT("!West = false"), !EDoorDirection::West);
TestTrue(TEXT("!NbDirection = true"), !EDoorDirection::NbDirection);
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,90 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "DoorType.h"
#include "UObject/Package.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorTypeTests, "ProceduralDungeon.Types.DoorType", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
bool FDoorTypeTests::RunTest(const FString& Parameters)
{
// Test DoorType Compatibility
{
CREATE_DATA_ASSET(UDoorType, TypeA);
CREATE_DATA_ASSET(UDoorType, TypeB);
CREATE_DATA_ASSET(UDoorType, TypeC);
/* Default Compatibility Table:
* A B C N
* A O X X X
* B X O X X
* C X X O X
* N X X X O
*/
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
TestFalse("TypeA should not be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
TestFalse("TypeB should not be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
TestTrue("TypeB should be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
TestTrue("TypeC should not be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
/* Test Compatibility Table:
* A B C N
* A O O X X
* B O X X X
* C X X O X
* N X X X O
*/
TypeA->SetCompatibility({});
TypeB->SetCompatibility({ TypeA.Get() });
TypeB->SetCompatibleWithItself(false);
TypeC->SetCompatibility({});
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
TestTrue("TypeA should be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
TestTrue("TypeB should be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
TestFalse("TypeB should not be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
TestTrue("TypeC should be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,384 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "DungeonGraph.h"
#include "Room.h"
#include "RoomData.h"
#include "TestUtils.h"
#include "./Classes/RoomConstraintChildClasses.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonGraphTest, "ProceduralDungeon.Types.DungeonGraph", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
#define INIT_TEST(Graph) \
TStrongObjectPtr<UDungeonGraph> Graph(NewObject<UDungeonGraph>(GetTransientPackage(), #Graph));
#define CLEAN_TEST() \
Graph->Clear();
// Utility to create and initialize a room
#define CREATE_ROOM(Name, RoomDataPtr) \
URoom* Name = NewObject<URoom>(); \
Name->Init(RoomDataPtr.Get(), nullptr, Graph->Count()); \
Graph->AddRoom(Name);
// Utility to create room data
#define CREATE_ROOM_DATA(Data) \
CREATE_DATA_ASSET(URoomData, Data); \
Data->Doors.Empty();
// Utility to create a non-empty path
#define DUMMY_PATH(Path) \
Path.Empty(); \
Path.Add(nullptr);
#pragma optimize("", off)
bool FDungeonGraphTest::RunTest(const FString& Parameters)
{
{
// Creating data assets
CREATE_ROOM_DATA(DA_A);
CREATE_ROOM_DATA(DA_B);
CREATE_ROOM_DATA(DA_C);
CREATE_ROOM_DATA(DA_D);
DA_A->Doors.Add({{0, 0, 0}, EDoorDirection::South});
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::East});
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::West});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::North});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::South});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::East});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::West});
DA_D->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 1, 2});
DA_D->Doors.Add({{0, 0, 0}, EDoorDirection::North});
DA_D->Doors.Add({{0, 0, 1}, EDoorDirection::North});
// Test pathfind
{
INIT_TEST(Graph);
// (Rooms are numbered from left to right and top to bottom starting at 0)
// A--C-C-!C-C-B
// | | |
// A-!B B--B-C
// first line
CREATE_ROOM(Room0, DA_A);
CREATE_ROOM(Room1, DA_C);
CREATE_ROOM(Room2, DA_C);
CREATE_ROOM(Room3, DA_C);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_B);
// second line
CREATE_ROOM(Room6, DA_A);
CREATE_ROOM(Room7, DA_B);
CREATE_ROOM(Room8, DA_B);
CREATE_ROOM(Room9, DA_B);
CREATE_ROOM(Room10, DA_C);
Room3->Lock(true);
Room7->Lock(true);
// first line
Graph->Connect(Room0, 0, Room1, 1);
Graph->Connect(Room1, 0, Room2, 1);
Graph->Connect(Room2, 0, Room3, 1);
Graph->Connect(Room3, 0, Room4, 1);
Graph->Connect(Room4, 0, Room5, 1);
// second line
Graph->Connect(Room6, 0, Room7, 0);
Graph->Connect(Room8, 0, Room9, 1);
Graph->Connect(Room9, 0, Room10, 0);
// transversal
Graph->Connect(Room1, 2, Room7, 1);
Graph->Connect(Room2, 2, Room8, 1);
Graph->Connect(Room4, 2, Room10, 1);
// Used to test path output for some scenarios
TArray<const URoom*> Path;
// Check graph state
TestEqual(TEXT("Graph should have 11 rooms"), 11, Graph->Count());
TestEqual(TEXT("Graph should have 2 rooms from data DA_A"), 2, Graph->CountRoomData(DA_A.Get()));
TestEqual(TEXT("Graph should have 4 rooms from data DA_B"), 4, Graph->CountRoomData(DA_B.Get()));
TestEqual(TEXT("Graph should have 5 rooms from data DA_C"), 5, Graph->CountRoomData(DA_C.Get()));
// Check path find with null rooms
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when both rooms are null"), UDungeonGraph::FindPath(nullptr, nullptr, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when first room is null"), UDungeonGraph::FindPath(nullptr, Room1, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when second room is null"), UDungeonGraph::FindPath(Room0, nullptr, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
// Check path found with same room
TestTrue(TEXT("Path should be found when both rooms are same"), UDungeonGraph::FindPath(Room0, Room0, &Path));
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
TestTrue(TEXT("Path should have Room0"), Path[0] == Room0);
TestTrue(TEXT("Path should be found when both rooms are same (even if locked)"), UDungeonGraph::FindPath(Room3, Room3, &Path));
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
TestTrue(TEXT("Path should have Room3"), Path[0] == Room3);
// Check no path when one or both rooms are locked
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room0 and Room3"), UDungeonGraph::FindPath(Room0, Room3, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room3 and Room0"), UDungeonGraph::FindPath(Room3, Room0, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room3 and Room7"), UDungeonGraph::FindPath(Room3, Room7, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
// output path has rooms in the correct order
TestTrue(TEXT("Path should be found between Room0 and Room5"), UDungeonGraph::FindPath(Room0, Room5, &Path));
TestEqual(TEXT("Path should have 8 rooms"), Path.Num(), 8);
TestTrue(TEXT("Path should not go through Room3"), !Path.Contains(Room3));
TestEqual(TEXT("Path's 1st room should be Room0"), Path[0], (const URoom*)Room0);
TestTrue(TEXT("Path's 2nd room should be Room1"), Path[1] == Room1);
TestTrue(TEXT("Path's 3rd room should be Room2"), Path[2] == Room2);
TestTrue(TEXT("Path's 4th room should be Room8"), Path[3] == Room8);
TestTrue(TEXT("Path's 5th room should be Room9"), Path[4] == Room9);
TestTrue(TEXT("Path's 6th room should be Room10"), Path[5] == Room10);
TestTrue(TEXT("Path's 7th room should be Room4"), Path[6] == Room4);
TestTrue(TEXT("Path's 8th room should be Room5"), Path[7] == Room5);
// Check pathfind through locked door (not first nor last)
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room0 and Room6"), UDungeonGraph::FindPath(Room0, Room6, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
TestTrue(TEXT("Path should be found between Room0 and Room6 (when locked rooms allowed)"), UDungeonGraph::FindPath(Room0, Room6, nullptr, /*IgnoreLocked = */ true));
CLEAN_TEST();
}
// Test Voxel Bounds Conversions
{
INIT_TEST(Graph);
// first floor second floor
// C - B - A C - C
// | |
// D D
CREATE_ROOM(Room0, DA_C);
CREATE_ROOM(Room1, DA_B);
CREATE_ROOM(Room2, DA_A);
CREATE_ROOM(Room3, DA_D);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_C);
Room1->Position = {0, 1, 0};
Room2->Position = {0, 2, 0};
Room2->Direction = EDoorDirection::East;
Room3->Position = {-1, 0, 0};
Room4->Position = {0, 0, 1};
Room5->Position = {0, 1, 1};
// Room positions are modified manually, so we need to explicitely rebuild bounds
Graph->RebuildBounds();
// Check Room0 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room0->GetVoxelBounds();
TestEqual(TEXT("Room0 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room1 voxel bounds conversion
{
FVoxelBounds ExceptedBounds;
ExceptedBounds.AddCell({0, 1, 0});
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room1->GetVoxelBounds();
TestEqual(TEXT("Room1 bounds should be as expected"), ConvertedBounds, ExceptedBounds);
}
// Check Room2 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 2, 0});
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room2->GetVoxelBounds();
TestEqual(TEXT("Room2 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room3 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({-1, 0, 0});
ExpectedBounds.AddCell({-1, 0, 1});
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room3->GetVoxelBounds();
TestEqual(TEXT("Room3 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room4 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 1});
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room4->GetVoxelBounds();
TestEqual(TEXT("Room4 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room5 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 1, 1});
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room5->GetVoxelBounds();
TestEqual(TEXT("Room5 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
}
// FilterAndSort Test
{
INIT_TEST(Graph);
// first floor second floor
// C - B - A C - C
// | |
// D D
CREATE_DATA_ASSET(UConstraintPass, Pass);
CREATE_DATA_ASSET(UConstraintFail, Fail);
CREATE_ROOM_DATA(DA_E);
DA_E->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
DA_E->Doors.Add({{0, 1, 0}, EDoorDirection::North});
DA_E->Constraints.Add(Pass.Get());
// Same as DA_E but constraint fail
CREATE_ROOM_DATA(DA_F);
DA_F->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
DA_F->Doors.Add({{0, 1, 0}, EDoorDirection::North});
DA_F->Constraints.Add(Fail.Get());
CREATE_ROOM(Room0, DA_C);
CREATE_ROOM(Room1, DA_B);
CREATE_ROOM(Room2, DA_A);
CREATE_ROOM(Room3, DA_D);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_C);
Room1->Position = {0, 1, 0};
Room2->Position = {0, 2, 0};
Room2->Direction = EDoorDirection::East;
Room3->Position = {-1, 0, 0};
Room4->Position = {0, 0, 1};
Room5->Position = {0, 1, 1};
// Room positions are modified manually, so we need to explicitely rebuild bounds
Graph->RebuildBounds();
const TArray<URoomData*> RoomList = {DA_A.Get(), DA_D.Get(), DA_E.Get(), DA_F.Get()};
TArray<FRoomCandidate> SortedRooms;
{
FDoorDef FromDoor = {{0, 0, 0}, EDoorDirection::North};
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
TestTrue(TEXT("There should be candidates"), bHasCandidates);
TestEqual(TEXT("There should be 4 candidates"), SortedRooms.Num(), 4);
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
TestEqual(TEXT("RoomData D Door index 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
TestEqual(TEXT("RoomData A should be the worst candidate"), SortedRooms[3].Data, DA_A.Get());
bool bContainsE = false;
bool bContainsF = false;
for (const FRoomCandidate& Candidate : SortedRooms)
{
bContainsE |= (Candidate.Data == DA_E.Get());
bContainsF |= (Candidate.Data == DA_F.Get());
}
// DA_E and DA_F are the same, but DA_F has a failing constraint
TestTrue(TEXT("RoomData E should be a valid candidate"), bContainsE);
TestFalse(TEXT("RoomData F should not be a valid candidate"), bContainsF);
}
{
FDoorDef FromDoor = {{0, 1, 1}, EDoorDirection::South};
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
TestTrue(TEXT("There should be candidates"), bHasCandidates);
TestEqual(TEXT("There should be 3 candidates"), SortedRooms.Num(), 3);
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
TestEqual(TEXT("RoomData D DoorIndex 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
TestEqual(TEXT("RoomData D should be the worst candidate"), SortedRooms[2].Data, DA_D.Get());
TestEqual(TEXT("RoomData D DoorIndex 1 should be the worst candidate"), SortedRooms[2].DoorIndex, 1);
}
CLEAN_TEST();
}
}
return true;
}
#pragma optimize("", on)
#undef INIT_TEST
#undef CLEAN_TEST
#undef CREATE_ROOM
#undef CREATE_ROOM_DATA
#undef DUMMY_PATH
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,50 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonUtils.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Utils/DungeonSaveUtils.h"
#include "Classes/DungeonSaveClasses.h"
#include "TestUtils.h"
#include "UObject/StrongObjectPtr.h"
#if WITH_DEV_AUTOMATION_TESTS
DEFINE_SPEC(FDungeonSaveSpecs, "ProceduralDungeon.SaveLoad", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
void FDungeonSaveSpecs::Define()
{
Describe(TEXT("Interfaces"), [this]()
{
It(TEXT("DungeonSaveInterface"), [this]()
{
TStrongObjectPtr<USaveTestObject> TestSave(NewObject<USaveTestObject>(GetTransientPackage()));
TStrongObjectPtr<USaveTestObject> TestLoad(NewObject<USaveTestObject>(GetTransientPackage()));
TestSave->TestSaveGameFlag = 5;
TestSave->TestSerializeObjectFunction = 8;
TArray<uint8> SavedData {};
SerializeUObject(SavedData, TestSave.Get(), false);
TestEqual(TEXT("should have correct order of execution"), TestSave->OrderOfExecution, FString(TEXT("BCD")));
TestTrue(TEXT("should have written data"), SavedData.Num() > 0);
SerializeUObject(SavedData, TestLoad.Get(), true);
TestEqual(TEXT("should have correct order of execution"), TestLoad->OrderOfExecution, FString(TEXT("WXY")));
TestEqual(TEXT("should have retrieved data from SaveGame flag"), TestLoad->TestSaveGameFlag, TestSave->TestSaveGameFlag);
TestEqual(TEXT("should have retrieved data from SerializedObject function"), TestLoad->TestSerializeObjectFunction, TestSave->TestSerializeObjectFunction);
});
});
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,167 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonUtils.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_WeightedMap, "ProceduralDungeon.Utils.WeightedMaps", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDungeonUtilsTest_WeightedMap::RunTest(const FString& Parameters)
{
// Built-in types test
{
TMap<int, int> WeightedMap = {
{1, 0}, // Weight with 0 should never be returned
{2, 1}, // The first non-zero weight should be return for index 0
{3, 2}, // Weights greater than 1 should be returned for as much indices
{4, 1} // The last one should be return when index == total weights minus one
}; // Out of bounds index should return default value
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 4);
TestEqual(TEXT("Weighted value at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), 0); // negative index should return default value
TestEqual(TEXT("Weighted value at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), 2);
TestEqual(TEXT("Weighted value at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), 3);
TestEqual(TEXT("Weighted value at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), 3);
TestEqual(TEXT("Weighted value at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), 4);
TestEqual(TEXT("Weighted value at 4"), Dungeon::GetWeightedAt(WeightedMap, 4), 0); // default int is 0
}
// Pointer test
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
TMap<int*, int> WeightedMap = {
{&a, 2},
{&b, 0}, // Weight with 0 in middle should be skipped
{&c, 1},
{&d, 0} // The last one should not be returned if weight is 0
};
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 3);
TestEqual(TEXT("Weighted pointer at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), (int*)nullptr);
TestEqual(TEXT("Weighted pointer at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), &a);
TestEqual(TEXT("Weighted pointer at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), &a);
TestEqual(TEXT("Weighted pointer at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), &c);
TestEqual(TEXT("Weighted pointer at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), (int*)nullptr);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_Guid2Seed, "ProceduralDungeon.Utils.Guid2Seed.Simple", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDungeonUtilsTest_Guid2Seed::RunTest(const FString& Parameters)
{
// Guid to Random Seed Tests
{
FGuid Guid1(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U);
FGuid Guid2(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U);
int64 Salt1 = 1;
int64 Salt2 = 2;
TestTrue(TEXT("Same Guid and Salt should return same Seed (1)"), Random::Guid2Seed(Guid1, Salt1) == Random::Guid2Seed(Guid1, Salt1));
TestTrue(TEXT("Same Guid and Salt should return same Seed (2)"), Random::Guid2Seed(Guid2, Salt2) == Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid1, Salt2));
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (2)"), Random::Guid2Seed(Guid2, Salt1) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt1));
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid and Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid and Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt1));
}
return true;
}
BEGIN_DEFINE_SPEC(FGuid2SeedStatisticalTests, "ProceduralDungeon.Utils.Guid2Seed.Stats", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
struct FTestParams
{
FGuid Guid;
int32 NbElements;
int32 NbSamples;
double CriticalValue;
};
using SampleArrayType = int32[];
static TUniquePtr<SampleArrayType> GenerateSamples(const FTestParams& Params)
{
TUniquePtr<SampleArrayType> Occurences(new int32[Params.NbElements] {0});
for (int32 i = 0; i < Params.NbSamples; ++i)
{
// Create a random stream with only the salt modified.
// This will simulate an actor in a room level generating its first random number
// in each of the room instances (where only the room ID changes)
FRandomStream RNG(Random::Guid2Seed(Params.Guid, i));
Occurences[RNG.RandRange(0, Params.NbElements - 1)]++;
}
return Occurences;
}
static bool PassChiSquaredTest(const FTestParams& Params, SampleArrayType Samples)
{
const double Expected = static_cast<double>(Params.NbSamples) / Params.NbElements;
double X2 = 0;
for (int i = 0; i < Params.NbElements; ++i)
{
const double Delta = Samples[i] - Expected;
X2 += (Delta * Delta) / Expected;
}
//UE_LOG(LogTemp, Warning, TEXT("Chi Squared result %d: %f / %f"), Params.NbElements, X2, Params.CriticalValue);
return X2 < Params.CriticalValue;
}
END_DEFINE_SPEC(FGuid2SeedStatisticalTests)
void FGuid2SeedStatisticalTests::Define()
{
Describe(TEXT("Chi Squared Test"), [this]()
{
FTestParams Params;
Params.NbSamples = 1'000'000'000;
TArray<FGuid> GuidsToTest = {
FGuid(),
FGuid(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U),
FGuid(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U),
};
for (const auto& Guid : GuidsToTest)
{
Params.Guid = Guid;
Describe(FString::Printf(TEXT("with Guid %s"), *Guid.ToString()), [this, Params]() mutable
{
// The test cases we want to check for each Guid.
// First value is the number of elements in the generated samples.
// Second value is the critical value for the Chi Squared test (with p-value of 5%)
TArray<TTuple<int32, double>> TestCases;
TestCases.Add(TTuple<int32, double>(10, 16.91898));
TestCases.Add(TTuple<int32, double>(100, 123.22522));
TestCases.Add(TTuple<int32, double>(1000, 1073.64265));
for (const auto& TestCase : TestCases)
{
Params.NbElements = TestCase.Key;
Params.CriticalValue = TestCase.Value;
It(FString::Printf(TEXT("with %d elements"), Params.NbElements), [this, Params]()
{
TUniquePtr<SampleArrayType> Samples = GenerateSamples(Params);
TestTrue(TEXT("Pass"), PassChiSquaredTest(Params, Samples.Get()));
});
}
});
}
});
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,448 @@
// Copyright Benoit Pelletier 2024 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "RoomData.h"
#include "DoorType.h"
#include "./Classes/RoomCustomDataChildClasses.h"
#include "./Classes/RoomConstraintChildClasses.h"
#include "UObject/Package.h"
#include "TestUtils.h"
#include "VoxelBounds/VoxelBounds.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FRoomDataTests, "ProceduralDungeon.Types.RoomData", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
// Utility to create room data
#define CREATE_ROOM_DATA(Data) \
CREATE_DATA_ASSET(URoomData, Data); \
Data->Doors.Empty();
#define ADD_DOOR(ROOM, DOOR_POS, DOOR_DIR, DOOR_TYPE) \
{ \
FDoorDef Door; \
Door.Position = DOOR_POS; \
Door.Direction = DOOR_DIR; \
Door.Type = DOOR_TYPE; \
ROOM->Doors.Add(Door); \
}
bool FRoomDataTests::RunTest(const FString& Parameters)
{
// Test IsRoomInBounds
{
// Creating this room data (X is the room origin, v is the door), thus we could test rotated bounds:
// 1 +---+---+---+
// | | X | |
// 0 +---+---+---+
// | | | |
// -1 +---+---+---+
// | | | |
// -2 +---+-v-+---+
// -1 0 1 2
CREATE_ROOM_DATA(RoomData);
RoomData->BoundingBoxes[0].SetMinAndMax(FIntVector(-2, -1, -1), FIntVector(1, 2, 2));
FDoorDef Door;
Door.Position = FIntVector(-2, 0, 0);
Door.Direction = EDoorDirection::South;
RoomData->Doors.Add(Door);
// If we want to limit the dungeon cells from -2 to 2,
// we need to create a box from -2 to 3 (see below).
// +---+---+---+---+---+
// |-2 |-1 | 0 | 1 | 2 |
// +---+---+---+---+---+
// -2 -1 0 1 2 3
FBoxMinAndMax DungeonBounds({-1000, -2, -1000}, {1000, 3, 1000});
FBoxMinAndMax RoomBoundsAtDoorLocation = RoomData->GetIntBounds() - Door.Position;
// Rotated to South
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::North);
TestEqual(TEXT("[S] Rotated Room Bounds: ((0, -1, -1), (3, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({0, -1, -1}, {3, 2, 2}));
Door.Direction = EDoorDirection::South;
Door.Position = {0, 0, 0};
TestTrue(TEXT("[S] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 1, 0};
TestTrue(TEXT("[S] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 2, 0};
TestFalse(TEXT("[S] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 4, 0};
TestFalse(TEXT("[S] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -1, 0};
TestTrue(TEXT("[S] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -2, 0};
TestFalse(TEXT("[S] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -4, 0};
TestFalse(TEXT("[S] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to East
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::West);
TestEqual(TEXT("[E] Rotated Room Bounds: ((-1, -2, -1), (2, 1, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, -2, -1}, {2, 1, 2}));
Door.Direction = EDoorDirection::East;
Door.Position = {0, 1, 0};
TestTrue(TEXT("[E] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 2, 0};
TestTrue(TEXT("[E] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 3, 0};
TestFalse(TEXT("[E] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 5, 0};
TestFalse(TEXT("[E] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, 0, 0};
TestTrue(TEXT("[E] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -1, 0};
TestFalse(TEXT("[E] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -3, 0};
TestFalse(TEXT("[E] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to West
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::East);
TestEqual(TEXT("[W] Rotated Room Bounds: ((-1, 0, -1), (2, 3, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, 0, -1}, {2, 3, 2}));
Door.Direction = EDoorDirection::West;
Door.Position = {0, -1, 0};
TestTrue(TEXT("[W] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 0, 0};
TestTrue(TEXT("[W] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 1, 0};
TestFalse(TEXT("[W] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 3, 0};
TestFalse(TEXT("[W] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -2, 0};
TestTrue(TEXT("[W] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -3, 0};
TestFalse(TEXT("[W] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -5, 0};
TestFalse(TEXT("[W] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to North
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::South);
TestEqual(TEXT("[N] Rotated Room Bounds: ((-2, -1, -1), (1, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({-2, -1, -1}, {1, 2, 2}));
Door.Direction = EDoorDirection::North;
Door.Position = {0, 0, 0};
TestTrue(TEXT("[N] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 1, 0};
TestTrue(TEXT("[N] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 2, 0};
TestFalse(TEXT("[N] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 4, 0};
TestFalse(TEXT("[N] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -1, 0};
TestTrue(TEXT("[N] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -2, 0};
TestFalse(TEXT("[N] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -4, 0};
TestFalse(TEXT("[N] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
}
// Test HasDoorOfType and variants
{
CREATE_ROOM_DATA(RoomData);
CREATE_DATA_ASSET(UDoorType, DoorA);
CREATE_DATA_ASSET(UDoorType, DoorB);
CREATE_DATA_ASSET(UDoorType, DoorC);
CREATE_DATA_ASSET(UDoorType, DoorD);
CREATE_DATA_ASSET(UDoorType, DoorE);
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::North, DoorA.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::South, DoorB.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::East, DoorC.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::West, DoorD.Get());
// HasDoorOfType
{
TestTrue("HasDoorOfType(DoorA)", RoomData->HasDoorOfType(DoorA.Get()));
TestTrue("HasDoorOfType(DoorB)", RoomData->HasDoorOfType(DoorB.Get()));
TestTrue("HasDoorOfType(DoorC)", RoomData->HasDoorOfType(DoorC.Get()));
TestTrue("HasDoorOfType(DoorD)", RoomData->HasDoorOfType(DoorD.Get()));
TestFalse("HasDoorOfType(DoorE)", RoomData->HasDoorOfType(DoorE.Get()));
TestFalse("HasDoorOfType(nullptr)", RoomData->HasDoorOfType(nullptr));
}
// HasAnyDoorOfType
{
TestTrue("HasAnyDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
TestTrue("HasAnyDoorOfType({DoorC, DoorD})", RoomData->HasAnyDoorOfType({DoorC.Get(), DoorD.Get()}));
TestFalse("HasAnyDoorOfType({DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorE.Get(), nullptr}));
TestFalse("HasAnyDoorOfType({})", RoomData->HasAnyDoorOfType({}));
}
// HasAllDoorOfType
{
TestFalse("HasAllDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
TestTrue("HasAllDoorOfType({DoorC, DoorD})", RoomData->HasAllDoorOfType({DoorC.Get(), DoorD.Get()}));
TestFalse("HasAllDoorOfType({DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorE.Get(), nullptr}));
TestTrue("HasAllDoorOfType({})", RoomData->HasAllDoorOfType({}));
}
}
// Test HasCustomData and variants
{
CREATE_ROOM_DATA(RoomData);
RoomData->CustomData.Add(UCustomDataA::StaticClass());
RoomData->CustomData.Add(UCustomDataB::StaticClass());
// HasCustomData
{
TestTrue("HasCustomData(CustomDataA)", RoomData->HasCustomData(UCustomDataA::StaticClass()));
TestTrue("HasCustomData(CustomDataB)", RoomData->HasCustomData(UCustomDataB::StaticClass()));
TestFalse("HasCustomData(CustomDataC)", RoomData->HasCustomData(UCustomDataC::StaticClass()));
TestFalse("HasCustomData(nullptr)", RoomData->HasCustomData(nullptr));
}
// HasAnyCustomData
{
TestTrue("HasAnyCustomData({CustomDataA, CustomDataB})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
TestTrue("HasAnyCustomData({CustomDataA, CustomDataC})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
TestFalse("HasAnyCustomData({nullptr, CustomDataC})", RoomData->HasAnyCustomData({nullptr, UCustomDataC::StaticClass()}));
TestFalse("HasAnyCustomData({})", RoomData->HasAnyCustomData({}));
}
// HasAllCustomData
{
TestTrue("HasAllCustomData({CustomDataA, CustomDataB})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
TestFalse("HasAllCustomData({CustomDataA, CustomDataC})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
TestFalse("HasAllCustomData({nullptr, CustomDataC})", RoomData->HasAllCustomData({nullptr, UCustomDataC::StaticClass()}));
TestTrue("HasAllCustomData({})", RoomData->HasAllCustomData({}));
}
}
// Test Size and Volume
{
// Should have Size=(1,1,1) and Volume=1
CREATE_ROOM_DATA(RoomDataA);
RoomDataA->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 1, 0), FIntVector(1, 0, 1));
// Should have Size=(2,1,1) and Volume=2
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, 0, 1));
// Should have Size=(2,2,1) and Volume=4
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, -1, 1));
// Should have Size=(2,2,2) and Volume=8
CREATE_ROOM_DATA(RoomDataD);
RoomDataD->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, -1), FIntVector(1, -1, 1));
// GetSize
{
TestEqual("RoomDataA->GetSize() == {1,1,1}", RoomDataA->GetSize(), FIntVector {1, 1, 1});
TestEqual("RoomDataB->GetSize() == {2,1,1}", RoomDataB->GetSize(), FIntVector {2, 1, 1});
TestEqual("RoomDataC->GetSize() == {2,2,1}", RoomDataC->GetSize(), FIntVector {2, 2, 1});
TestEqual("RoomDataD->GetSize() == {2,2,2}", RoomDataD->GetSize(), FIntVector {2, 2, 2});
}
// GetVolume
{
TestEqual("RoomDataA->GetVolume() == 1", RoomDataA->GetVolume(), 1);
TestEqual("RoomDataB->GetVolume() == 2", RoomDataB->GetVolume(), 2);
TestEqual("RoomDataC->GetVolume() == 4", RoomDataC->GetVolume(), 4);
TestEqual("RoomDataD->GetVolume() == 8", RoomDataD->GetVolume(), 8);
}
}
// Test GetVoxelBounds
{
CREATE_ROOM_DATA(RoomDataA);
RoomDataA->Doors.Add({{0, 0, 0}, EDoorDirection::North});
// Should have one cell at (0,0,0) with a door at (0,0,0)[North]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataA->GetVoxelBounds();
TestEqual("RoomDataA->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 0, 0), FIntVector(2, 1, 1));
RoomDataB->Doors.Add({{0, 0, 0}, EDoorDirection::West});
RoomDataB->Doors.Add({{1, 0, 0}, EDoorDirection::North});
// Should have 3 cells at (-1,0,0), (0,0,0), (1,0,0) with doors at (0,0,0)[West] and (1,0,0)[North]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({-1, 0, 0});
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.AddCell({1, 0, 0});
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataB->GetVoxelBounds();
TestEqual("RoomDataB->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 0, -1), FIntVector(1, 1, 2));
RoomDataC->Doors.Add({{0, 0, 0}, EDoorDirection::North});
RoomDataC->Doors.Add({{0, 0, 1}, EDoorDirection::South});
// Should have 3 cells at (0,0,-1), (0,0,0), (0,0,1) with doors at (0,0,0)[North] and (0,0,1)[South]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, -1});
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.AddCell({0, 0, 1});
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataC->GetVoxelBounds();
TestEqual("RoomDataC->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
}
// Test Room Constraints
{
#define CHECK_CONSTRAINTS(DATA) URoomData::DoesPassAllConstraints(nullptr, DATA, FIntVector::ZeroValue, EDoorDirection::North)
TestFalse("null data should fail.", CHECK_CONSTRAINTS(nullptr));
CREATE_DATA_ASSET(UConstraintPass, Pass);
CREATE_DATA_ASSET(UConstraintFail, Fail);
CREATE_ROOM_DATA(RoomDataA);
TestTrue("No constraint should pass.", CHECK_CONSTRAINTS(RoomDataA.Get()));
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->Constraints.Add(Pass.Get());
TestTrue("One passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataB.Get()));
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->Constraints.Add(Fail.Get());
TestFalse("One failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataC.Get()));
CREATE_ROOM_DATA(RoomDataD);
RoomDataD->Constraints.Add(Pass.Get());
RoomDataD->Constraints.Add(Pass.Get());
RoomDataD->Constraints.Add(Pass.Get());
TestTrue("All passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataD.Get()));
CREATE_ROOM_DATA(RoomDataE);
RoomDataE->Constraints.Add(Fail.Get());
RoomDataE->Constraints.Add(Pass.Get());
RoomDataE->Constraints.Add(Pass.Get());
TestFalse("First failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataE.Get()));
CREATE_ROOM_DATA(RoomDataF);
RoomDataF->Constraints.Add(Pass.Get());
RoomDataF->Constraints.Add(Fail.Get());
RoomDataF->Constraints.Add(Pass.Get());
TestFalse("Second failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataF.Get()));
CREATE_ROOM_DATA(RoomDataG);
RoomDataG->Constraints.Add(Pass.Get());
RoomDataG->Constraints.Add(Pass.Get());
RoomDataG->Constraints.Add(Fail.Get());
TestFalse("Third failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataG.Get()));
#undef CHECK_CONSTRAINTS
}
return true;
}
#undef CREATE_DATA_ASSET
#undef CREATE_ROOM_DATA
#undef ADD_DOOR
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,23 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "UObject/UObjectGlobals.h"
#include "UObject/Package.h"
#include "Misc/EngineVersionComparison.h"
#include "UObject/StrongObjectPtr.h"
#if UE_VERSION_OLDER_THAN(5, 5, 0)
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags::ApplicationContextMask
#else
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags_ApplicationContextMask
#endif
// Utility to create a data asset
#define CREATE_DATA_ASSET(VAR_TYPE, VAR_NAME) \
TStrongObjectPtr<VAR_TYPE> VAR_NAME(NewObject<VAR_TYPE>(GetTransientPackage(), #VAR_NAME))
@@ -0,0 +1,522 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "VoxelBounds/VoxelBounds.h"
#include "Tests/Classes/CustomScoreCallbacks.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FVoxelBoundsTest, "ProceduralDungeon.Types.VoxelBounds", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
#define SET_CONNECTION(BOUNDS, CELL, DIR, TYPE) \
BOUNDS.SetCellConnection(FIntVector CELL, FVoxelBounds::EDirection::DIR, FVoxelBoundsConnection(EVoxelBoundsConnectionType::TYPE));
bool FVoxelBoundsTest::RunTest(const FString& Parameters)
{
// Extend Test
{
FVoxelBounds BoundsA;
FVoxelBounds BoundsB;
FVoxelBounds BoundsC;
BoundsA.AddCell(FIntVector(0, -1, 0));
BoundsA.AddCell(FIntVector(1, 1, 2));
BoundsB.AddCell(FIntVector(1, 1, 1));
TestEqual(TEXT("BoundsA == ({0, -1, 0}, {2, 2, 3})"), BoundsA.GetBounds(), FBoxMinAndMax({0, -1, 0}, {2, 2, 3}));
TestEqual(TEXT("BoundsB == ({1, 1, 1}, {2, 2, 2})"), BoundsB.GetBounds(), FBoxMinAndMax({1, 1, 1}, {2, 2, 2}));
TestEqual(TEXT("BoundsC == ({0, 0, 0}, {0, 0, 0})"), BoundsC.GetBounds(), FBoxMinAndMax::Invalid);
}
// Comparison Test
{
FVoxelBounds BoundsA;
FVoxelBounds BoundsB;
FVoxelBounds BoundsC;
FVoxelBounds BoundsD;
FVoxelBounds BoundsE;
BoundsA.AddCell(FIntVector(0, 0, 0));
BoundsA.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Wall);
SET_CONNECTION(BoundsA, (1, 1, 1), South, Door);
// Setting the cell connections in a different way is intentional here
auto& CellA = BoundsB.AddCell(FIntVector(0, 0, 0));
auto& CellB = BoundsB.AddCell(FIntVector(1, 1, 1));
CellA[static_cast<uint8>(FVoxelBounds::EDirection::North)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall);
CellB[static_cast<uint8>(FVoxelBounds::EDirection::South)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door);
BoundsC.AddCell(FIntVector(0, 0, 0));
BoundsC.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(BoundsC, (0, 0, 0), North, Wall);
TestEqual(TEXT("BoundsA == BoundsB"), BoundsA, BoundsB);
TestNotEqual(TEXT("BoundsA != BoundsC"), BoundsA, BoundsC);
TestTrue(TEXT("BoundsD is not valid"), !BoundsD.IsValid());
TestNotEqual(TEXT("BoundsA != BoundsD"), BoundsA, BoundsD);
}
// Transform Test
{
FVoxelBounds Bounds;
Bounds.AddCell(FIntVector(0, 0, 0));
Bounds.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(Bounds, (0, 0, 0), North, Wall);
SET_CONNECTION(Bounds, (1, 1, 1), South, Door);
// Offset by 0
{
TestEqual(TEXT("Bounds + (0,0,0) == OffsetBounds"), Bounds + FIntVector {0, 0, 0}, Bounds);
}
// Simple addition
{
FVoxelBounds OffsetBounds;
OffsetBounds.AddCell(FIntVector(1, -1, 1));
OffsetBounds.AddCell(FIntVector(2, 0, 2));
SET_CONNECTION(OffsetBounds, (1, -1, 1), North, Wall);
SET_CONNECTION(OffsetBounds, (2, 0, 2), South, Door);
TestEqual(TEXT("Bounds + (1,-1,1) == OffsetBounds"), Bounds + FIntVector {1, -1, 1}, OffsetBounds);
}
// Simple subtraction
{
FVoxelBounds OffsetBounds;
OffsetBounds.AddCell(FIntVector(-1, 1, -1));
OffsetBounds.AddCell(FIntVector(0, 2, 0));
SET_CONNECTION(OffsetBounds, (-1, 1, -1), North, Wall);
SET_CONNECTION(OffsetBounds, (0, 2, 0), South, Door);
TestEqual(TEXT("Bounds - (1,-1,1) == OffsetBounds"), Bounds - FIntVector {1, -1, 1}, OffsetBounds);
}
// North rotation
{
TestEqual(TEXT("Bounds.Rotate(North) == Bounds"), Rotate(Bounds, EDoorDirection::North), Bounds);
}
// East rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(-1, 1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), East, Wall);
SET_CONNECTION(RotatedBounds, (-1, 1, 1), West, Door);
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::East);
TestEqual(TEXT("Bounds.Rotate(East) == RotatedBounds"), NewBounds, RotatedBounds);
}
// South rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(-1, -1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), South, Wall);
SET_CONNECTION(RotatedBounds, (-1, -1, 1), North, Door);
TestEqual(TEXT("Bounds.Rotate(South) == RotatedBounds"), Rotate(Bounds, EDoorDirection::South), RotatedBounds);
}
// West rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(1, -1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), West, Wall);
SET_CONNECTION(RotatedBounds, (1, -1, 1), East, Door);
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::West);
TestEqual(TEXT("Bounds.Rotate(West) == RotatedBounds"), NewBounds, RotatedBounds);
}
}
// Concatenation Test
{
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
FVoxelBounds BoundsC = BoundsA + BoundsB;
FVoxelBounds BoundsD = BoundsB + BoundsA;
TestEqual(TEXT("BoundsC == BoundsD"), BoundsC, BoundsD);
TestEqual(TEXT("BoundsC.GetBounds() == BoundsD.GetBounds()"), BoundsC.GetBounds(), BoundsD.GetBounds());
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell(FIntVector(0, 0, 0));
ExpectedBounds.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(ExpectedBounds, (0, 0, 0), East, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), South, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), West, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Down, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), East, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), West, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Down, Wall);
TestEqual(TEXT("BoundsC == ExpectedBounds"), BoundsC, ExpectedBounds);
}
// Subtraction test
{
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
FVoxelBounds BoundsC;
BoundsC.AddCell(FIntVector(0, 0, 0));
BoundsC.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsC, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsC, (0, 0, 0), Down, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), Down, Wall);
FVoxelBounds ExpectedBoundsCMinusA;
ExpectedBoundsCMinusA.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), East, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), South, Door);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), West, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Down, Wall);
FVoxelBounds ExpectedBoundsCMinusB;
ExpectedBoundsCMinusB.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), East, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), South, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), West, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsCMinusA = BoundsC - BoundsA;
FVoxelBounds BoundsCMinusB = BoundsC - BoundsB;
TestEqual(TEXT("BoundsC - BoundsA == ExpectedBoundsCMinusA"), BoundsCMinusA, ExpectedBoundsCMinusA);
TestEqual(TEXT("BoundsC - BoundsB == ExpectedBoundsCMinusB"), BoundsCMinusB, ExpectedBoundsCMinusB);
// Non commutative
FVoxelBounds BoundsAMinusC = BoundsA - BoundsC;
FVoxelBounds BoundsBMinusC = BoundsB - BoundsC;
TestFalse(TEXT("BoundsA - BoundsC is invalid"), BoundsAMinusC.IsValid());
TestFalse(TEXT("BoundsB - BoundsC is invalid"), BoundsBMinusC.IsValid());
}
// DoesFitOutside Test
{
// Bounds of 2x2x2
// +---+-o-+ + +---+
// |100 110| |111|
// +-o-+ + + +-o-+
// |010|
// + +---+ + + +
FVoxelBounds DungeonBounds;
DungeonBounds.AddCell(FIntVector(1, 0, 0));
DungeonBounds.AddCell(FIntVector(0, 1, 0));
DungeonBounds.AddCell(FIntVector(1, 1, 0));
DungeonBounds.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(DungeonBounds, (1, 0, 0), North, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), South, Door);
SET_CONNECTION(DungeonBounds, (1, 0, 0), West, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), Up, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), West, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), East, Door);
SET_CONNECTION(DungeonBounds, (0, 1, 0), South, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), Up, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 0), North, Door);
SET_CONNECTION(DungeonBounds, (1, 1, 0), East, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), North, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), East, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), South, Door);
SET_CONNECTION(DungeonBounds, (1, 1, 1), West, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), Up, Wall);
// Bounds that fit in the available space
{
// Bounds that fits perfectly in the available space
// + + +
//
// +-o-+ +
// |000|
// +---+ +
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
// Like BoundsA but with a door facing a wall
// + + +
//
// +-o-+ +
// |000o
// +---+ +
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsB, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Down, Wall);
// Bounds bigger that fits perfectly in the available space
// +-o-+ +
// |101|
// + + +
// |001o
// +---+ +
FVoxelBounds BoundsC;
BoundsC.AddCell(FIntVector(0, 0, 1));
BoundsC.AddCell(FIntVector(1, 0, 1));
SET_CONNECTION(BoundsC, (0, 0, 1), East, Door);
SET_CONNECTION(BoundsC, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), North, Door);
SET_CONNECTION(BoundsC, (1, 0, 1), West, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), East, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), Up, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), Down, Wall);
// Like BoundsC but with a door facing a wall
// +---+ +
// |101o
// + + +
// |001o
// +---+ +
FVoxelBounds BoundsD;
BoundsD.AddCell(FIntVector(0, 0, 1));
BoundsD.AddCell(FIntVector(1, 0, 1));
SET_CONNECTION(BoundsD, (0, 0, 1), East, Door);
SET_CONNECTION(BoundsD, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), North, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), West, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), East, Door);
SET_CONNECTION(BoundsD, (1, 0, 1), Up, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), Down, Wall);
// Bounds outside the dungeon bounds
// + +---+ + +-o-+
// |210| |211|
// + +-o-+ + +---+
FVoxelBounds BoundsE;
BoundsE.AddCell(FIntVector(2, 1, 0));
BoundsE.AddCell(FIntVector(2, 1, 1));
SET_CONNECTION(BoundsE, (2, 1, 0), North, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), East, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), South, Door);
SET_CONNECTION(BoundsE, (2, 1, 0), West, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), Down, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), North, Door);
SET_CONNECTION(BoundsE, (2, 1, 1), East, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), South, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), West, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), Up, Wall);
// Like BoundsE but with a door facing a wall
// + +---+ + +---+
// |210| |211|
// + +-o-+ + +-o-+
FVoxelBounds BoundsF;
BoundsF.AddCell(FIntVector(2, 1, 0));
BoundsF.AddCell(FIntVector(2, 1, 1));
SET_CONNECTION(BoundsF, (2, 1, 0), North, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), East, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), South, Door);
SET_CONNECTION(BoundsF, (2, 1, 0), West, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), Down, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), North, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), East, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), South, Door);
SET_CONNECTION(BoundsF, (2, 1, 1), West, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), Up, Wall);
// Bounds bigger with a connected door
// + + +
//
// +-o-+-o-+
// |001 011|
// +---+---+
FVoxelBounds BoundsG;
BoundsG.AddCell(FIntVector(0, 0, 1));
BoundsG.AddCell(FIntVector(0, 1, 1));
SET_CONNECTION(BoundsG, (0, 0, 1), North, Door);
SET_CONNECTION(BoundsG, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), North, Door);
SET_CONNECTION(BoundsG, (0, 1, 1), South, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), East, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), Up, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), Down, Wall);
int32 _ScoreA, _ScoreB, _ScoreC, _ScoreD, _ScoreE, _ScoreF, _ScoreG;
TestTrue(TEXT("BoundsA fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, _ScoreA));
TestTrue(TEXT("BoundsB fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, _ScoreB));
TestTrue(TEXT("BoundsC fit in DungeonBounds"), BoundsC.GetCompatibilityScore(DungeonBounds, _ScoreC));
TestTrue(TEXT("BoundsD fit in DungeonBounds"), BoundsD.GetCompatibilityScore(DungeonBounds, _ScoreD));
TestTrue(TEXT("BoundsE fit in DungeonBounds"), BoundsE.GetCompatibilityScore(DungeonBounds, _ScoreE));
TestTrue(TEXT("BoundsF fit in DungeonBounds"), BoundsF.GetCompatibilityScore(DungeonBounds, _ScoreF));
TestTrue(TEXT("BoundsG fit in DungeonBounds"), BoundsG.GetCompatibilityScore(DungeonBounds, _ScoreG));
// Order of fitting: E(G) > A > C(F) > B > D
TestTrue(TEXT("BoundsE fit better than BoundsA"), _ScoreE > _ScoreA);
TestTrue(TEXT("BoundsA fit better than BoundsC"), _ScoreA > _ScoreC);
TestTrue(TEXT("BoundsC fit better than BoundsB"), _ScoreC > _ScoreB);
TestTrue(TEXT("BoundsB fit better than BoundsD"), _ScoreB > _ScoreD);
TestTrue(TEXT("BoundsG fit equally as BoundsE"), _ScoreG == _ScoreE);
TestTrue(TEXT("BoundsC fit equally as BoundsF"), _ScoreC == _ScoreF);
}
// Bounds that do not fit in the available space
{
// Bounds that does not fit in the available space
// +---+ +
// |100|
// +-o-+ +
//
// + + +
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsA, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), Down, Wall);
int32 ScoreA;
TestFalse(TEXT("BoundsA does not fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA));
// Bigger bounds that does not fit in the available space
// +---+ +
// |100|
// + + +
// |000|
// +-o-+ +
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(0, 0, 0));
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (0, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
int32 ScoreB;
TestFalse(TEXT("BoundsB does not fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, ScoreB));
}
// Custom Score Test
{
UCustomScoreCallback* CustomCallbacks = NewObject<UCustomScoreCallback>();
FScoreCallback ZeroScore;
ZeroScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::ZeroScore);
FScoreCallback NeverPassScore;
NeverPassScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::NeverPass);
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
int32 ScoreA;
TestTrue(TEXT("BoundsA does fit in DungeonBounds with ZeroScore"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA, ZeroScore));
TestEqual(TEXT("BoundsA ZeroScore should have score of 0"), ScoreA, 0);
int32 ScoreB;
TestFalse(TEXT("BoundsA does not fit in DungeonBounds ith NeverPass"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreB, NeverPassScore));
TestNotEqual(TEXT("BoundsA NeverPass should have score different from ZeroScore"), ScoreA, ScoreB);
}
}
return true;
}
#undef SET_CONNECTION
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,55 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "TriggerDoor.h"
#include "Components/BoxComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/Character.h"
#include "Room.h"
#include "RoomLevel.h"
ATriggerDoor::ATriggerDoor()
{
BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxComponent"));
if (IsValid(RootComponent))
BoxComponent->SetupAttachment(RootComponent);
}
void ATriggerDoor::BeginPlay()
{
Super::BeginPlay();
check(BoxComponent);
BoxComponent->OnComponentBeginOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerEnter);
BoxComponent->OnComponentEndOverlap.AddUniqueDynamic(this, &ATriggerDoor::OnTriggerExit);
}
void ATriggerDoor::OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (!ActorList.Contains(OtherActor) && IsValidActor(OtherActor, OtherComp))
{
ActorList.Add(OtherActor);
UpdateOpenState();
}
}
void ATriggerDoor::OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (ActorList.Contains(OtherActor))
{
ActorList.Remove(OtherActor);
UpdateOpenState();
}
}
bool ATriggerDoor::IsValidActor_Implementation(AActor* Actor, UPrimitiveComponent* Component)
{
ACharacter* Character = Cast<ACharacter>(Actor);
UCapsuleComponent* Capsule = Cast<UCapsuleComponent>(Component);
return IsValid(Character) && IsValid(Capsule) && Capsule == Character->GetCapsuleComponent();
}
@@ -0,0 +1,97 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "TriggerType.h"
#include "TimerManager.h"
#include "ProceduralDungeonTypes.h"
#include "Engine/World.h"
// Sets default values for this component's properties
UTriggerType::UTriggerType()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = false;
TickDuration = 0.5f;
ActivationDelay = 0.0;
}
// Called when the game starts
void UTriggerType::BeginPlay()
{
Super::BeginPlay();
if (GetNetMode() != ENetMode::NM_Client)
{
OnComponentBeginOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerEnter);
OnComponentEndOverlap.AddUniqueDynamic(this, &UTriggerType::OnTriggerExit);
GetWorld()->GetTimerManager().SetTimer(TickTimer, this, &UTriggerType::TriggerTick, TickDuration, true);
}
}
void UTriggerType::OnTriggerEnter(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (ActorType == nullptr || (OtherActor != nullptr && OtherActor->IsA(ActorType)))
{
if (!ActorList.Contains(OtherActor))
{
ActorList.Add(OtherActor);
OnActorEnter.Broadcast(OtherActor);
if (ActorList.Num() >= requiredActorCountToActivate)
{
if (ActivationDelay > 0)
{
GetWorld()->GetTimerManager().SetTimer(ActivationTimer, this, &UTriggerType::TriggerActivate, ActivationDelay, false);
}
else
{
TriggerActivate();
}
}
}
}
}
void UTriggerType::OnTriggerExit(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
if (ActorType == nullptr || (OtherActor != nullptr && OtherActor->IsA(ActorType)))
{
if (ActorList.Contains(OtherActor))
{
ActorList.Remove(OtherActor);
OnActorExit.Broadcast(OtherActor);
GetWorld()->GetTimerManager().ClearTimer(ActivationTimer);
TriggerDeactivate();
}
}
}
void UTriggerType::TriggerTick()
{
OnTriggerTick.Broadcast(ActorList);
}
void UTriggerType::TriggerActivate()
{
if (!bIsActivated)
{
bIsActivated = true;
OnActivation.Broadcast(ActorList);
}
}
void UTriggerType::TriggerDeactivate()
{
if (bIsActivated)
{
bIsActivated = false;
OnDeactivation.Broadcast(ActorList);
}
}
@@ -0,0 +1,131 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "Utils/DungeonSaveUtils.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "Serialization/StructuredArchive.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Serialization/Formatters/JsonArchiveInputFormatter.h"
#include "Serialization/Formatters/JsonArchiveOutputFormatter.h"
#include "UObject/Class.h"
#if UE_VERSION_NEWER_THAN(5, 0, 0)
#include "Serialization/StructuredArchiveSlotBase.h"
#endif
TUniquePtr<FArchiveFormatterType> CreateArchiveFormatterFromArchive(FArchive& Ar, bool bTextFormat)
{
#if !UE_BUILD_SHIPPING && WITH_TEXT_ARCHIVE_SUPPORT
if (bTextFormat)
{
if (Ar.IsSaving())
return MakeUnique<FJsonArchiveOutputFormatter>(Ar);
else
return MakeUnique<FJsonArchiveInputFormatter>(Ar);
}
else
#endif
{
return MakeUnique<FBinaryArchiveFormatter>(Ar);
}
}
bool SerializeUObject(FStructuredArchive::FRecord& Record, UObject* Obj, bool bIsLoading)
{
check(IsValid(Obj));
const bool bImplementsDungeonSave = Obj->Implements<UDungeonSaveInterface>();
// Allow modification of saved variables just before serializing into the saved dungeon.
if (bImplementsDungeonSave)
IDungeonSaveInterface::Execute_DungeonPreSerialize(Obj, bIsLoading);
bool bSuccess = true;
Obj->SerializeScriptProperties(Record.EnterField(AR_FIELD_NAME("Properties")));
if (auto* SaveableObj = Cast<IDungeonCustomSerialization>(Obj))
{
bSuccess &= SaveableObj->SerializeObject(Record, bIsLoading);
}
// Allow some setup code right after the deserialization of its properties from the saved dungeon.
if (bImplementsDungeonSave)
IDungeonSaveInterface::Execute_DungeonPostSerialize(Obj, bIsLoading);
return bSuccess;
}
bool SerializeUObject(FArchive& Ar, UObject* Obj, bool bIsLoading, bool bTextFormat)
{
TUniquePtr<FArchiveFormatterType> Formatter = CreateArchiveFormatterFromArchive(Ar, bTextFormat);
FStructuredArchive StructuredArchive(*Formatter);
//FStructuredArchiveFromArchive StructuredArchive(Ar);
FStructuredArchive::FRecord Record = StructuredArchive.Open().EnterRecord();
SerializeUObject(Record, Obj, bIsLoading);
return true;
}
bool SerializeUObject(TArray<uint8>& Data, UObject* Obj, bool bIsLoading, bool bTextFormat)
{
TSharedPtr<FArchive> Ar {nullptr};
if (bIsLoading)
{
Ar = MakeShared<FMemoryReader>(Data);
}
else
{
Ar = MakeShared<FMemoryWriter>(Data);
}
check(Ar != nullptr);
FObjectAndNameAsStringProxyArchive Archive(*Ar, true);
Archive.ArIsSaveGame = true;
SerializeUObject(Archive, Obj, bIsLoading, bTextFormat);
return true;
}
void SerializeUClass(FStructuredArchiveSlot Slot, UClass*& Class)
{
SerializeUObjectRef(Slot, Class);
}
void SerializeUObjectRef(FStructuredArchiveSlot Slot, UObject*& Object)
{
auto ObjPath = FSoftObjectPath(Object);
Slot << ObjPath;
if (Slot.GetArchiveState().IsLoading())
{
// Resolve potential redirectors before trying to resolve the object
ObjPath.FixupCoreRedirects();
Object = ObjPath.ResolveObject();
if (nullptr == Object && !ObjPath.IsNull())
{
Object = ObjPath.TryLoad();
}
if (nullptr == Object)
{
DungeonLog_Error("Failed to load class from path: %s", *ObjPath.ToString());
}
}
}
bool IsLoading(const FStructuredArchiveSlotBase& Slot)
{
return Slot.GetArchiveState().IsLoading();
}
bool IsSaving(const FStructuredArchiveSlotBase& Slot)
{
return Slot.GetArchiveState().IsSaving();
}
@@ -0,0 +1,344 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "VoxelBounds/VoxelBounds.h"
const FIntVector FVoxelBounds::Directions[] = {
{1, 0, 0}, // North
{0, 1, 0}, // East
{-1, 0, 0}, // South
{0, -1, 0}, // West
{0, 0, 1}, // Up
{0, 0, -1} // Down
};
bool FVoxelBoundsConnection::operator==(const FVoxelBoundsConnection& Other) const
{
if (Type != Other.Type)
return false;
if (EVoxelBoundsConnectionType::Door == Type)
return DoorType == Other.DoorType;
return true;
}
int32 FVoxelBoundsConnection::GetCompatibilityScore(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B)
{
// No connection is always compatible with any other (that measn it's inside the bounds)
if (A.Type == EVoxelBoundsConnectionType::None || B.Type == EVoxelBoundsConnectionType::None)
return 0;
// When types are mismatching, it's not compatible
if (A.Type != B.Type)
{
// Penalty when a door is not aligned with another door
if (EVoxelBoundsConnectionType::Door == A.Type || EVoxelBoundsConnectionType::Door == B.Type)
return -10;
return 0;
}
if (EVoxelBoundsConnectionType::Door == A.Type)
{
// High score when doors are aligned and matching together
if (A.DoorType == B.DoorType)
return 10;
// Penalty when doors are aligned but not matching together
return -10;
}
return 0;
}
FVoxelBounds::EDirection FVoxelBounds::Opposite(EDirection Direction)
{
static const EDirection OppositeDirections[] = {
EDirection::South,
EDirection::West,
EDirection::North,
EDirection::East,
EDirection::Down,
EDirection::Up
};
const uint8 Index = static_cast<uint8>(Direction);
if (Index < static_cast<uint8>(EDirection::NbDirection))
{
return OppositeDirections[Index];
}
return EDirection::NbDirection;
}
TArray<FVoxelBoundsConnection>& FVoxelBounds::AddCell(FIntVector Cell)
{
auto& Connections = Cells.Add(Cell);
Connections.SetNum(static_cast<uint8>(EDirection::NbDirection));
Bounds.Extend(FBoxMinAndMax(Cell, Cell + FIntVector(1)));
return Connections;
}
void FVoxelBounds::AddBox(const FBoxMinAndMax& Box)
{
Bounds.Extend(Box);
Cells.Reserve(Cells.Num() + Box.GetSize().X * Box.GetSize().Y * Box.GetSize().Z);
for (int32 X = Box.GetMin().X; X < Box.GetMax().X; ++X)
{
for (int32 Y = Box.GetMin().Y; Y < Box.GetMax().Y; ++Y)
{
for (int32 Z = Box.GetMin().Z; Z < Box.GetMax().Z; ++Z)
{
auto& Connections = Cells.Add(FIntVector(X, Y, Z));
Connections.SetNum(static_cast<uint8>(EDirection::NbDirection));
}
}
}
}
const FVoxelBoundsConnection* FVoxelBounds::GetCellConnection(FIntVector Cell, EDirection Direction) const
{
auto* CellConnections = Cells.Find(Cell);
if (nullptr == CellConnections)
return nullptr;
return &(*CellConnections)[static_cast<uint8>(Direction)];
}
bool FVoxelBounds::SetCellConnection(FIntVector Cell, EDirection Direction, const FVoxelBoundsConnection& Connection)
{
auto* CellConnections = Cells.Find(Cell);
if (nullptr == CellConnections)
return false;
(*CellConnections)[static_cast<uint8>(Direction)] = Connection;
return true;
}
void FVoxelBounds::ResetToWalls()
{
static const FVoxelBoundsConnection NoneConnection(EVoxelBoundsConnectionType::None);
static const FVoxelBoundsConnection WallConnection(EVoxelBoundsConnectionType::Wall);
for (auto& Cell : Cells)
{
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
{
const FIntVector OtherCell = Cell.Key + Directions[i];
const auto* FoundOtherCell = Cells.Find(OtherCell);
Cell.Value[i] = (FoundOtherCell) ? NoneConnection : WallConnection;
}
}
}
bool FVoxelBounds::GetCompatibilityScore(const FVoxelBounds& Other, int32& Score, const FScoreCallback& CustomScore) const
{
// Each cell add 1 to the score, so the bigger volume the higher score.
Score = Cells.Num();
bool bAreOverlapping = FBoxMinAndMax::Overlap(Bounds, Other.Bounds);
// @TODO: for now, treating a coincident face as overlapping
// There is room for further optimizations here later
bAreOverlapping |= Bounds.GetMin().X == Other.Bounds.GetMax().X;
bAreOverlapping |= Bounds.GetMax().X == Other.Bounds.GetMin().X;
bAreOverlapping |= Bounds.GetMin().Y == Other.Bounds.GetMax().Y;
bAreOverlapping |= Bounds.GetMax().Y == Other.Bounds.GetMin().Y;
bAreOverlapping |= Bounds.GetMin().Z == Other.Bounds.GetMax().Z;
bAreOverlapping |= Bounds.GetMax().Z == Other.Bounds.GetMin().Z;
// When not overlapping, the score is equal to the number of cell
// and it does always fit outside too.
if (!bAreOverlapping)
{
return true;
}
for (const auto& Cell : Cells)
{
// When a cell is defined in both bounds, it does not fit outside
if (Other.Cells.Contains(Cell.Key))
{
Score = -1;
return false;
}
// Case when this cell is not defined in the other bounds
// We set a score depending on the connection compatibility
// @TODO: top and bottom are not yet relevant, but will be when doors on top/bottom will be implemented
for (uint8 i = 0; i < static_cast<uint8>(EDoorDirection::NbDirection); ++i)
{
// Get Neighbor cell
const FIntVector Neighbor = Cell.Key + Directions[i];
auto* NeighConns = Other.Cells.Find(Neighbor);
if (nullptr == NeighConns)
continue;
const auto& Connection = Cell.Value[i];
const auto& OtherConnection = (*NeighConns)[static_cast<uint8>(Opposite(static_cast<EDirection>(i)))];
if (CustomScore.IsBound())
{
if (!CustomScore.Execute(Connection, OtherConnection, Score))
return false;
}
else
{
Score += FVoxelBoundsConnection::GetCompatibilityScore(Connection, OtherConnection);
}
}
}
return true;
}
void FVoxelBounds::operator+=(const FIntVector& Offset)
{
*this = *this + Offset;
}
void FVoxelBounds::operator-=(const FIntVector& Offset)
{
*this = *this - Offset;
}
FVoxelBounds operator+(const FVoxelBounds& Bounds, const FIntVector& Offset)
{
FVoxelBounds NewBounds;
for (const auto& Cell : Bounds.Cells)
{
NewBounds.Cells.Add(Cell.Key + Offset, Cell.Value);
}
NewBounds.Bounds = Bounds.Bounds + Offset;
return NewBounds;
}
FVoxelBounds operator-(const FVoxelBounds& Bounds, const FIntVector& Offset)
{
return Bounds + (FIntVector::ZeroValue - Offset);
}
void FVoxelBounds::operator+=(const FVoxelBounds& Other)
{
for (const auto& Cell : Other.Cells)
{
// Ignore incoming cells that are already defined
// @TODO: how to manage different connections?
if (Cells.Contains(Cell.Key))
continue;
auto& Connections = AddCell(Cell.Key);
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
{
// Get neighbor cell
const FIntVector Neighbor = Cell.Key + Directions[i];
if (auto* NeighConns = Cells.Find(Neighbor))
{
// If neighbor is defined, we clear the neigbor's connection
// Also, we don't copy the connection of other bounds
const uint8 OppositeDir = static_cast<uint8>(Opposite(static_cast<EDirection>(i)));
(*NeighConns)[OppositeDir] = FVoxelBoundsConnection();
}
else
{
// Just copy connection if no neighbors
Connections[i] = Cell.Value[i];
}
}
}
}
void FVoxelBounds::operator-=(const FVoxelBounds& Other)
{
for (const auto& Cell : Other.Cells)
{
if (!Cells.Remove(Cell.Key))
continue;
for (uint8 i = 0; i < static_cast<uint8>(EDirection::NbDirection); ++i)
{
// Get neighbor cell
const FIntVector Neighbor = Cell.Key + Directions[i];
if (auto* NeighConns = Cells.Find(Neighbor))
{
// If neighbor is defined, we copy this connection into it
const uint8 OppositeDir = static_cast<uint8>(Opposite(static_cast<EDirection>(i)));
(*NeighConns)[OppositeDir] = Cell.Value[i];
}
}
}
}
FVoxelBounds operator+(const FVoxelBounds& A, const FVoxelBounds& B)
{
FVoxelBounds Result = A;
Result += B;
return Result;
}
FVoxelBounds operator-(const FVoxelBounds& A, const FVoxelBounds& B)
{
FVoxelBounds Result = A;
Result -= B;
return Result;
}
bool FVoxelBounds::operator==(const FVoxelBounds& Other) const
{
if (Cells.Num() != Other.Cells.Num())
return false;
for (const auto& Cell : Cells)
{
const auto* OtherConnections = Other.Cells.Find(Cell.Key);
if (!OtherConnections)
return false;
if (Cell.Value.Num() != OtherConnections->Num())
return false;
for (uint8 i = 0; i < Cell.Value.Num(); ++i)
{
if (Cell.Value[i] != (*OtherConnections)[i])
return false;
}
}
return true;
}
bool FVoxelBounds::Overlap(const FVoxelBounds& A, const FVoxelBounds& B)
{
if (!FBoxMinAndMax::Overlap(A.Bounds, B.Bounds))
return false;
// @TODO: Maybe it will be more performant to use a hierarchical partitioning
// especially when using really small RoomUnits (like (1,1,1))
for (const auto& Cell : A.Cells)
{
if (B.Cells.Contains(Cell.Key))
return true;
}
return false;
}
FVoxelBounds Rotate(const FVoxelBounds& Bounds, const EDoorDirection& Rot)
{
FVoxelBounds NewBounds;
for (const auto& Cell : Bounds.Cells)
{
const FIntVector NewCell = Rotate(Cell.Key, Rot);
auto& NewConnections = NewBounds.AddCell(NewCell);
// @TODO: will need to update that when doors on top/bottom will be implemented
for (uint8 i = 0; i < static_cast<uint8>(EDoorDirection::NbDirection); ++i)
{
NewConnections[static_cast<uint8>(static_cast<EDoorDirection>(i) + Rot)] = Cell.Value[i];
}
// @TODO: Currently, no rotation are applied on top/bottom connections
// but they will be when doors on top/bottom will be implemented
NewConnections[static_cast<uint8>(FVoxelBounds::EDirection::Up)] = Cell.Value[static_cast<uint8>(FVoxelBounds::EDirection::Up)];
NewConnections[static_cast<uint8>(FVoxelBounds::EDirection::Down)] = Cell.Value[static_cast<uint8>(FVoxelBounds::EDirection::Down)];
}
return NewBounds;
}
@@ -0,0 +1,33 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
using UnrealBuildTool;
public class ProceduralDungeon : ModuleRules
{
public ProceduralDungeon(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
// Uncomment that to detect when there are missing includes in cpp files
//bUseUnity = false;
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"NavigationSystem",
#if UE_5_2_OR_LATER
"IrisCore",
#endif
});
PrivateDependencyModuleNames.AddRange(new string[] {
"Engine",
"CoreUObject",
"NetCore",
});
}
}
@@ -0,0 +1,75 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "ProceduralDungeonTypes.h"
#include "BoundsParams.generated.h"
// Holds the settings for the dungeon limits.
// These values are expressed in Room cells, and are based on the origin of the first room (0,0,0).
// For example, if the first room is only 1 room cell (`FirstPoint = (0,0,0)`, `SecondPoint = (1,1,1)`), then this is the cell (0,0,0).
// If you set a `MinY=2` et `MaxY=2`, then on the Y axis the dungeon can go from the cell -2 to cell 2,
// Making an effective range of 5 cells, centered on the first room.
USTRUCT(BlueprintType, meta = (ShortToolTip = "Holds the settings for the dungeon limits."))
struct FBoundsParams
{
GENERATED_BODY()
public:
// Enables the X limit in positive axis (north from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMaxX {false};
// The X positive limit (north) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxX", UIMin = 0, ClampMin = 0))
int32 MaxX {0};
// Enables the X limit in negative axis (south from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMinX {false};
// The X negative limit (south) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinX", UIMin = 0, ClampMin = 0))
int32 MinX {0};
// Enables the Y limit in positive axis (east from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMaxY {false};
// The Y positive limit (east) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxY", UIMin = 0, ClampMin = 0))
int32 MaxY {0};
// Enables the Y limit in negative axis (west from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMinY {false};
// The Y negative limit (west) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinY", UIMin = 0, ClampMin = 0))
int32 MinY {0};
// Enables the Z limit in positive axis (up from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMaxZ {false};
// The Z positive limit (up) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMaxZ", UIMin = 0, ClampMin = 0))
int32 MaxZ {0};
// Enables the Z limit in negative axis (down from the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (InlineEditConditionToggle))
bool bLimitMinZ {false};
// The Z negative limit (down) of the dungeon in room units (starting from the origin of the first room).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation|Bounds Limits", meta = (EditCondition = "bLimitMinZ", UIMin = 0, ClampMin = 0))
int32 MinZ {0};
public:
FBoxMinAndMax GetBox() const;
};
@@ -0,0 +1,38 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "DeterministicRandomComponent.generated.h"
// A simple component that adds a RandomStream to any actor placed in a room level.
// It will uses the actor's guid (provided by a IRoomActorGuid interface) and the owning room's ID
// to generate an initial seed unique for this actor but deterministic with the dungeon's seed.
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
class PROCEDURALDUNGEON_API UDeterministicRandomComponent : public UActorComponent
{
GENERATED_BODY()
public:
UDeterministicRandomComponent();
protected:
//~ Begin UActorComponent Interface
virtual void OnRegister() override;
//~ End UActorComponent Interface
static int32 GenerateDeterministicSeed(AActor* Actor);
// This is the C++ accessor.
const FRandomStream& GetRandom() const { return Random; }
private:
UPROPERTY(BlueprintReadOnly, Category = "Deterministic Random", meta = (AllowPrivateAccess = true))
FRandomStream Random;
};
@@ -0,0 +1,105 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Interfaces/DoorInterface.h"
#include "ProceduralDungeonTypes.h"
#include "DoorComponent.generated.h"
class URoom;
class UDoorType;
class UDoorComponent;
class URoomConnection;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDoorComponentLockedDelegate, UDoorComponent*, Component, bool, IsLocked);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FDoorComponentOpenedDelegate, UDoorComponent*, Component, bool, IsOpened);
// Component that manages open/close of a door, as well as a locking state.
// Multiplayer and GameSave ready
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
class PROCEDURALDUNGEON_API UDoorComponent : public UActorComponent, public IDoorInterface
{
GENERATED_BODY()
public:
UDoorComponent();
public:
//~ Begin AActor interface
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
//~ End AActor interface
//~ Begin IDoorInterface interface
virtual const UDoorType* GetDoorType_Implementation() const override { return Type; }
virtual void SetRoomConnection_Implementation(URoomConnection* RoomConnection) override;
//~ End IDoorInterface interface
public:
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Locked"))
FORCEINLINE bool IsLocked() const { return bLocked; }
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Open"))
FORCEINLINE bool IsOpen() const { return bIsOpen; }
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door")
void Open(bool bOpen);
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door")
void Lock(bool bLock);
bool ShouldBeOpen() const;
bool ShouldBeLocked() const;
URoom* GetRoomA() const;
URoom* GetRoomB() const;
URoomConnection* GetRoomConnection() const { return RoomConnection; }
bool IsAlwaysVisible() const { return bAlwaysVisible; }
bool IsAlwaysUnlocked() const { return bAlwaysUnlocked; }
void SetAlwaysVisible(bool bInAlwaysVisible) { bAlwaysVisible = bInAlwaysVisible; }
void SetAlwaysUnlocked(bool bInAlwaysUnlocked) { bAlwaysUnlocked = bInAlwaysUnlocked; }
void SetDoorType(UDoorType* DoorType) { Type = DoorType; }
public:
UPROPERTY(BlueprintAssignable, Category = "Door")
FDoorComponentOpenedDelegate OnDoorOpened;
UPROPERTY(BlueprintAssignable, Category = "Door")
FDoorComponentLockedDelegate OnDoorLocked;
protected:
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Locked"))
void OnDoorLock_BP(bool bIsLocked);
virtual void OnDoorLock(bool bIsLocked) {}
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Open"))
void OnDoorOpen_BP(bool bIsOpened);
virtual void OnDoorOpen(bool bIsOpened) {}
bool OwnerHasAuthority() const;
protected:
bool bLocked {false};
bool bIsOpen {false};
UPROPERTY(BlueprintReadOnly, Replicated, Category = "Door")
URoomConnection* RoomConnection {nullptr};
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door", meta = (DisplayName = "Always Visible"))
bool bAlwaysVisible {false};
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Door", meta = (DisplayName = "Always Unlocked"))
bool bAlwaysUnlocked {false};
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Door", meta = (DisplayName = "Door Type"))
UDoorType* Type {nullptr};
};
@@ -0,0 +1,54 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "RoomVisitor.h"
#include "RoomObserverComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomObserverEvent, ARoomLevel*, RoomLevel, AActor*, Actor);
// Room Observer that auto-(un)bind itself when it enters/exits a dungeon room.
// Could observe (be bound) multiple rooms at once if the actor overlaps multiple room.
// This component **does** track its own Room, thus the actor can move between rooms (use StaticRoomObserverComponent instead if this behavior is not needed).
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
class PROCEDURALDUNGEON_API URoomObserverComponent : public UActorComponent, public IRoomVisitor
{
GENERATED_BODY()
public:
URoomObserverComponent();
FRoomObserverEvent& OnActorEnterRoomEvent() { return ActorEnterRoomEvent; }
FRoomObserverEvent& OnActorExitRoomEvent() { return ActorExitRoomEvent; }
protected:
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Enter Room"))
FRoomObserverEvent ActorEnterRoomEvent;
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Exit Room"))
FRoomObserverEvent ActorExitRoomEvent;
private:
//~ BEGIN IRoomVisitor
virtual void OnRoomEnter_Implementation(ARoomLevel* RoomLevel) override;
virtual void OnRoomExit_Implementation(ARoomLevel* RoomLevel) override;
//~ END IRoomVisitor
void BindToLevel(ARoomLevel* RoomLevel, bool Bind);
UFUNCTION()
void OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Actor);
UFUNCTION()
void OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Actor);
private:
TSet<ARoomLevel*> BoundLevels;
};
@@ -0,0 +1,67 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Interfaces/RoomActorGuid.h"
#include "SimpleGuidComponent.generated.h"
#define GUID_DEBUG_ENABLED 0
// A simple guid component that will retrieve the Editor's ActorGuid
// to save/load it in packaged games.
//
// :::warning
//
// This component will work only on placed actor, not actors spawned during runtime!!!
//
// :::
UCLASS(BlueprintType, Blueprintable, ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
class PROCEDURALDUNGEON_API USimpleGuidComponent : public UActorComponent, public IRoomActorGuid
{
GENERATED_BODY()
public:
USimpleGuidComponent();
protected:
//~ Begin UActorComponent Interface
virtual void OnRegister() override;
//~ End UActorComponent Interface
//~ Begin IRoomActorGuid Interface
virtual FGuid GetGuid_Implementation() const override;
virtual bool ShouldSaveActor_Implementation() const override;
//~ End IRoomActorGuid Interface
// Unfortunately I can't place them in the #if block below. UE will complain about it.
virtual void Serialize(FArchive& Ar) override;
virtual void Serialize(FStructuredArchive::FRecord Record) override;
#if GUID_DEBUG_ENABLED // Enable some logs to debug lifecycle of the component.
//~ Begin UObject Interface
virtual void PostInitProperties() override;
virtual void PreSave(FObjectPreSaveContext SaveContext);
virtual void PostLoad() override;
//~ End UObject Interface
//~ Begin UActorComponent Interface
virtual void OnComponentCreated() override;
virtual void InitializeComponent() override;
virtual void BeginPlay() override;
//~ End UActorComponent Interface
#endif
public:
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, NonPIEDuplicateTransient, TextExportTransient, Category = "Guid")
FGuid Guid;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Dungeon Save")
bool bSaveActorInDungeon {true};
};
@@ -0,0 +1,52 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "StaticRoomObserverComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FStaticRoomObserverEvent, ARoomLevel*, RoomLevel, AActor*, Actor);
// Room Observer that auto-(un)bind itself at BeginPlay and EndPlay.
// This component will bind to the level it belongs to. So it needs to be placed directly in the Room map.
// This component does **not** track its own Room, thus the actor should not move between rooms (use RoomObserverComponent instead).
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent))
class PROCEDURALDUNGEON_API UStaticRoomObserverComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStaticRoomObserverComponent();
FStaticRoomObserverEvent& OnActorEnterRoomEvent() { return ActorEnterRoomEvent; }
FStaticRoomObserverEvent& OnActorExitRoomEvent() { return ActorExitRoomEvent; }
protected:
virtual void BeginPlay() override;
virtual void EndPlay(EEndPlayReason::Type Reason) override;
protected:
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Enter Room"))
FStaticRoomObserverEvent ActorEnterRoomEvent;
UPROPERTY(BlueprintAssignable, Category = "Room Observer", meta = (DisplayName = "On Actor Exit Room"))
FStaticRoomObserverEvent ActorExitRoomEvent;
private:
void BindToLevel(bool Bind);
UFUNCTION()
void OnActorEnterRoom(ARoomLevel* RoomLevel, AActor* Visitor);
UFUNCTION()
void OnActorExitRoom(ARoomLevel* RoomLevel, AActor* Visitor);
private:
bool bBound {false};
};
@@ -0,0 +1,67 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ProceduralDungeonTypes.h"
#include "StaticRoomVisibilityComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomVisibilityEvent, AActor*, Actor, bool, IsInVisibleRoom);
// Component to manage the visibility of an actor in the dungeon.
// Use this one if the actor remains in the same room.
// If the actor is able to move room, use URoomVisibilityComponent instead.
UCLASS(ClassGroup = "ProceduralDungeon", meta = (BlueprintSpawnableComponent, DisplayName = "Room Visibility (Static)"))
class PROCEDURALDUNGEON_API UStaticRoomVisibilityComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStaticRoomVisibilityComponent();
virtual void BeginPlay() override;
virtual void EndPlay(EEndPlayReason::Type Reason) override;
void SetVisible(UObject* Owner, bool Visible);
void ResetVisible(UObject* Owner); // Same as SetVisible(Owner, false)
// Returns true if the actor is in a visible room.
// Always returns true when "Occlude Dynamic Actors" is unchecked in the plugin's settings
// Useful with "Custom" visibility.
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon", meta = (CompactNodeTitle = "Is In Visible Room", DisplayName = "Is In Visible Room"))
bool IsVisible();
UFUNCTION(BlueprintSetter)
void SetVisibilityMode(EVisibilityMode Mode);
UFUNCTION(BlueprintGetter)
FORCEINLINE EVisibilityMode GetVisibilityMode() const { return VisibilityMode; }
public:
// Called when the visibility from rooms changed (either by a room visibility change or by this actor moving between rooms).
// Useful to update a "Custom" visibility.
UPROPERTY(BlueprintAssignable, Category = "Procedural Dungeon")
FRoomVisibilityEvent OnRoomVisibilityChanged;
protected:
ARoomLevel* GetOwnerRoomLevel() const;
void UpdateVisibility();
void RegisterVisibilityDelegate(ARoomLevel* RoomLevel, bool Register);
UFUNCTION()
void RoomVisibilityChanged(class ARoomLevel* RoomLevel, bool IsVisible);
private:
void CleanEnablers();
TSet<TWeakObjectPtr<UObject>> VisibilityEnablers {};
UPROPERTY(EditAnywhere, BlueprintGetter = GetVisibilityMode, BlueprintSetter = SetVisibilityMode, Category = "Procedural Dungeon", meta = (AllowPrivateAccess = true))
EVisibilityMode VisibilityMode {EVisibilityMode::Default};
};
@@ -0,0 +1,138 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Door.generated.h"
class UDoorType;
class UDoorComponent;
// Base class for all door actors in the dungeon.
// Use this class even if you want to create a wall to place instead of a door (when the door is not connected to another room for example).
UCLASS(Blueprintable, ClassGroup = "Procedural Dungeon")
class PROCEDURALDUNGEON_API ADoor : public AActor
{
GENERATED_BODY()
public:
ADoor();
//~ Begin AActor interface
virtual void PostInitializeComponents() override;
//~ End AActor interface
public:
UDoorComponent* GetDoorComponent() const { return DoorComponent; }
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Locked", DeprecatedFunction, DeprecationMessage = "Use DoorComponent->IsLocked instead."))
bool IsLocked() const;
UFUNCTION(BlueprintPure, Category = "Door", meta = (CompactNodeTitle = "Is Open", DeprecatedFunction, DeprecationMessage = "Use DoorComponent->IsOpen instead."))
bool IsOpen() const;
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door", meta = (DeprecatedFunction, DeprecationMessage = "Use DoorComponent->Open instead."))
void Open(bool open);
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Door", meta = (DeprecatedFunction, DeprecationMessage = "Use DoorComponent->Lock instead."))
void Lock(bool lock);
bool ShouldBeOpened() const;
bool ShouldBeLocked() const;
UFUNCTION(BlueprintGetter)
const UDoorType* GetDoorType() const;
UFUNCTION(BlueprintGetter)
URoom* GetRoomA() const;
UFUNCTION(BlueprintGetter)
URoom* GetRoomB() const;
// Used only to migrate from old save games. Must not be used anywhere else than the URoomConnection.
bool GetLegacyShouldBeLocked() const { return bShouldBeLocked; }
bool GetLegacyShouldBeOpen() const { return bShouldBeOpen; }
bool GetLegacyAlwaysVisible() const { return bAlwaysVisible; }
bool GetLegacyAlwaysUnlocked() const { return bAlwaysUnlocked; }
UDoorType* GetLegacyDoorType() const { return Type; }
protected:
UFUNCTION()
virtual void OnDoorLock() {}
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Locked", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorLocked(true) instead."))
void OnDoorLock_BP();
UFUNCTION()
virtual void OnDoorUnlock() {}
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Unlocked", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorLocked(false) instead."))
void OnDoorUnlock_BP();
UFUNCTION()
virtual void OnDoorOpen() {}
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Open", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorOpened(true) instead."))
void OnDoorOpen_BP();
UFUNCTION()
virtual void OnDoorClose() {}
UFUNCTION(BlueprintImplementableEvent, Category = "Door", meta = (DisplayName = "On Close", DeprecatedFunction, DeprecationMessage = "Bind to DoorComponent->OnDoorOpened(false) instead."))
void OnDoorClose_BP();
UFUNCTION()
void DispatchDoorLock(UDoorComponent* Component, bool IsLocked);
UFUNCTION()
void DispatchDoorOpen(UDoorComponent* Component, bool IsOpened);
protected:
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
UPROPERTY(BlueprintGetter = GetAlwaysVisible, BlueprintSetter = SetAlwaysVisible, SaveGame, Category = "Door", meta = (DisplayName = "Always Visible", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->AlwaysVisible instead."))
bool bAlwaysVisible {false};
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
UPROPERTY(BlueprintGetter = GetAlwaysUnlocked, BlueprintSetter = SetAlwaysUnlocked, SaveGame, Category = "Door", meta = (DisplayName = "Always Unlocked", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->AlwaysUnlocked instead."))
bool bAlwaysUnlocked {false};
// DEPRECATED: Ghost property for retro-compatibility with older plugin versions.
UPROPERTY(BlueprintGetter = GetDoorType, Category = "Door", meta = (DisplayName = "Door Type", DeprecatedProperty, DeprecationMessage = "Use DoorComponent->DoorType instead."))
UDoorType* Type {nullptr};
UPROPERTY(EditAnywhere, Category = "Door")
USceneComponent* DefaultSceneComponent {nullptr};
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Door")
UDoorComponent* DoorComponent {nullptr};
private:
UFUNCTION(BlueprintGetter)
bool GetAlwaysVisible() const;
UFUNCTION(BlueprintGetter)
bool GetAlwaysUnlocked() const;
UFUNCTION(BlueprintSetter)
void SetAlwaysVisible(bool bInAlwaysVisible);
UFUNCTION(BlueprintSetter)
void SetAlwaysUnlocked(bool bInAlwaysUnlocked);
private:
// Ghost properties for retro-compatibility. Redirect to the DoorComponent->RoomA/B internally.
UPROPERTY(BlueprintGetter = GetRoomA, Category = "Door", meta = (AllowPrivateAccess = true, DeprecatedProperty, DeprecationMessage = "Use DoorComponent->RoomConnection->GetRoomA instead."))
URoom* RoomA {nullptr};
UPROPERTY(BlueprintGetter = GetRoomB, Category = "Door", meta = (AllowPrivateAccess = true, DeprecatedProperty, DeprecationMessage = "Use DoorComponent->RoomConnection->GetRoomB instead."))
URoom* RoomB {nullptr};
// DEPRECATED: Ghost property for retro-compatibility with older save games.
UPROPERTY(SaveGame, meta = (AllowPrivateAccess = true))
bool bShouldBeLocked {false};
// DEPRECATED: Ghost property for retro-compatibility with older save games.
UPROPERTY(SaveGame, meta = (AllowPrivateAccess = true))
bool bShouldBeOpen {false};
};
@@ -0,0 +1,72 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "DoorType.generated.h"
// Data asset to define a door type.
// A door type is used to define the size, offset, and color of a door bounds.
// Doors with different types are not compatible with each others.
UCLASS(BlueprintType)
class PROCEDURALDUNGEON_API UDoorType : public UDataAsset
{
GENERATED_BODY()
public:
UDoorType();
// Returns the door size from the door type asset,
// or the default door size in plugin's settings if no door type defined.
static FVector GetSize(const UDoorType* DoorType);
// Returns the door offset from the door type asset,
// or the default door offset in plugin's settings if no door type defined.
static float GetOffset(const UDoorType* DoorType);
// Returns the door color from the door type asset,
// or the default door color in plugin's settings if no door type defined.
static FColor GetColor(const UDoorType* DoorType);
// Returns true if one of the door type is explicitely set to be compatible with the other.
static bool AreCompatible(const UDoorType* A, const UDoorType* B);
#if WITH_DEV_AUTOMATION_TESTS
// For unit tests only, to set the compatibility of a door type.
void SetCompatibility(const TArray<UDoorType*>& InCompatibility) { Compatibility = InCompatibility; }
void SetCompatibleWithItself(bool bInCompatibleWithItself) { bCompatibleWithItself = bInCompatibleWithItself; }
#endif
protected:
// Size of the door bounds, only used by the debug draw as a visual hint for designers and artists.
UPROPERTY(EditInstanceOnly, Category = "Door Type", meta = (ClampMin = 0))
FVector Size;
// The offset of the door bounds from the room's base (in percentage of the room unit Z).
UPROPERTY(EditInstanceOnly, Category = "Door Type", meta = (ClampMin = 0, ClampMax = 1, UIMin = 0, UIMax = 1))
float Offset;
// The color used to draw the door bounds in the editor.
UPROPERTY(EditInstanceOnly, Category = "Door Type")
FColor Color;
#if WITH_EDITORONLY_DATA
// Just a description, used nowhere.
UPROPERTY(EditInstanceOnly, Category = "Door Type")
FText Description;
#endif
// Can this door type be connected with itself?
UPROPERTY(EditInstanceOnly, Category = "Door Type")
bool bCompatibleWithItself;
// Which door types are compatible with this one
UPROPERTY(EditInstanceOnly, Category = "Door Type")
TArray<UDoorType*> Compatibility;
};
@@ -0,0 +1,168 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "ProceduralDungeonTypes.h"
#include "Engine/DataTable.h"
#include "DungeonBlueprintLibrary.generated.h"
class URoom;
class URoomCustomData;
class ARoomLevel;
UCLASS()
class PROCEDURALDUNGEON_API UDungeonBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon")
static bool IsDoorOfType(const TSubclassOf<class AActor> DoorClass, const class UDoorType* DoorType);
UFUNCTION(BlueprintPure, Category = "Utilities", meta = (DisplayName = "Equal (Data Table Row Handle)", CompactNodeTitle = "=="))
static bool CompareDataTableRows(const FDataTableRowHandle& A, const FDataTableRowHandle& B);
static const ARoomLevel* GetLevelScript(const AActor* Target);
// Returns the room instance the actor is in.
// If the actor is spawned at runtime or the owning level is not a room level, returns null.
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target"))
static URoom* GetOwningRoom(const AActor* Target);
// Returns the first RoomCustomData of the provided type in the owning room.
// If no owning room or no custom data of this type, returns null.
UFUNCTION(BlueprintCallable, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target", ExpandBoolAsExecs = "ReturnValue", DeterminesOutputType = "CustomDataClass", DynamicOutputParam = "CustomData"))
static bool GetOwningRoomCustomData(const AActor* Target, TSubclassOf<URoomCustomData> CustomDataClass, URoomCustomData*& CustomData);
UFUNCTION(BlueprintPure, Category = "Utilities|Procedural Dungeon", meta = (DefaultToSelf = "Target"))
static const URoomData* GetLevelRoomData(const AActor* Target);
UFUNCTION(BlueprintPure, Category = "DoorDef", meta = (DisplayName = "Opposite", CompactNodeTitle = "Opposite"))
static FDoorDef DoorDef_GetOpposite(const FDoorDef& DoorDef);
// ===== DoorDirection Math Utility Functions =====
// True if the value is set (either North, South, East, West)
// False otherwise
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Is Valid", ExpandBoolAsExecs = "ReturnValue"))
static bool DoorDirection_Valid(const EDoorDirection& A) { return !!A; }
// Addition (A + B)
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Direction + Direction", CompactNodeTitle = "+", AutoCreateRefTerm = "A,B"))
static EDoorDirection DoorDirection_Add(const EDoorDirection& A, const EDoorDirection& B) { return A + B; }
// Subtraction (A - B)
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Direction - Direction", CompactNodeTitle = "-", AutoCreateRefTerm = "A,B"))
static EDoorDirection DoorDirection_Sub(const EDoorDirection& A, const EDoorDirection& B) { return A - B; }
// Increment the direction and set it
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Increment Door Direction", CompactNodeTitle = "++"))
static EDoorDirection& DoorDirection_Increment(UPARAM(ref) EDoorDirection& A) { return ++A; }
// Decrement the direction and set it
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Decrement Door Direction", CompactNodeTitle = "--"))
static EDoorDirection& DoorDirection_Decrement(UPARAM(ref) EDoorDirection& A) { return --A; }
// Negate the direction and set it (same as North - Direction)
UFUNCTION(BlueprintCallable, Category = "Math|Door Direction", meta = (DisplayName = "Negate Door Direction", CompactNodeTitle = "-"))
static EDoorDirection& DoorDirection_Negate(UPARAM(ref) EDoorDirection& A) { A = -A; return A; }
// Transforms North into South and East into West (and vice versa)
UFUNCTION(BlueprintPure, Category = "Math|Door Direction", meta = (DisplayName = "Opposite", CompactNodeTitle = "Opposite", AutoCreateRefTerm = "A"))
static EDoorDirection DoorDirection_Opposite(const EDoorDirection& A) { return ~A; }
// Convert a DoorDirection enum value into a unit IntVector pointing in that direction.
UFUNCTION(BlueprintPure, Category = "Conversion|Door Direction", meta = (BlueprintAutocast, DisplayName = "To Int Vector", AutoCreateRefTerm = "A"))
static FIntVector DoorDirection_ToIntVector(const EDoorDirection& A) { return ToIntVector(A); }
// Convert a DoorDirection enum value into a unit IntVector pointing in that direction.
UFUNCTION(BlueprintPure, Category = "Conversion|Door Direction", meta = (BlueprintAutocast, DisplayName = "To Angle", AutoCreateRefTerm = "A"))
static float DoorDirection_ToAngle(const EDoorDirection& A) { return ToAngle(A); }
// ===== Dungeon Math Transform =====
// Returns the neighbor at the provided direction.
// Same as Vector + ToIntVector(Direction)
UFUNCTION(BlueprintPure, Category = "Math|Transform", meta = (DisplayName = "Next (Int Vector)"))
static FIntVector IntVector_Next(const FIntVector& Vector, const EDoorDirection& Direction);
UFUNCTION(BlueprintPure, Category = "Math|Transform", meta = (DisplayName = "Rotate (Int Vector)"))
static FIntVector IntVector_Rotate(const FIntVector& Vector, const EDoorDirection& Direction);
// Transform a cell position from local coordinates into the dungeon coordinates
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Transform Position (Dungeon)", AutoCreateRefTerm = "Rotation"))
static FIntVector Dungeon_TransformPosition(const FIntVector& LocalPos, const FIntVector& Translation, const EDoorDirection& Rotation);
// Inverse transform a cell position from the dungeon coordinates into a local coordinates
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Inverse Transform Position (Dungeon)", AutoCreateRefTerm = "Rotation"))
static FIntVector Dungeon_InverseTransformPosition(const FIntVector& DungeonPos, const FIntVector& Translation, const EDoorDirection& Rotation);
// Transform a DoorDef structure from local coordinates into the dungeon coordinates
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Transform DoorDef (Dungeon)", AutoCreateRefTerm = "Rotation"))
static FDoorDef Dungeon_TransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation);
// Inverse transform a DoorDef structure from the dungeon coordinates into a local coordinates
UFUNCTION(BlueprintPure, Category = "Math|Dungeon", meta = (DisplayName = "Inverse Transform DoorDef (Dungeon)", AutoCreateRefTerm = "Rotation"))
static FDoorDef Dungeon_InverseTransformDoorDef(const FDoorDef& DoorDef, const FIntVector& Translation, const EDoorDirection& Rotation);
// ===== Int Vector Operators =====
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Add (Int Vector)", CompactNodeTitle = "+", CallableWithoutWorldContext, CommutativeAssociativeBinaryOperator))
static FIntVector IntVector_Add(const FIntVector& A, const FIntVector& B);
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Subtract (Int Vector)", CompactNodeTitle = "-", CallableWithoutWorldContext, CommutativeAssociativeBinaryOperator))
static FIntVector IntVector_Subtract(const FIntVector& A, const FIntVector& B);
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Equal (Int Vector)", Keywords = "==", CompactNodeTitle = "==", CallableWithoutWorldContext))
static bool IntVector_Equal(const FIntVector& A, const FIntVector& B);
UFUNCTION(BlueprintPure, Category = "Utilities|Operators", meta = (DisplayName = "Not Equal (Int Vector)", Keywords = "!=", CompactNodeTitle = "!=", CallableWithoutWorldContext))
static bool IntVector_NotEqual(const FIntVector& A, const FIntVector& B);
// ===== Plugin Settings Accessors =====
// Returns the room unit size in unreal units
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Default Room Unit", DeprecatedFunction, DeprecationMessage = "Use the GetRoomUnit from the DungeonSettings class instead."))
static FVector Settings_RoomUnit();
// Returns the default door type's size
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Default Door Size"))
static FVector Settings_DefaultDoorSize();
// Returns the room offset as a percentage of the height of a room unit
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Door Offset"))
static float Settings_DoorOffset();
// Returns true if the plugin's occlusion system is enabled
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Is Occlusion Culling Enabled"))
static bool Settings_OcclusionCulling();
// Enable/disable the plugin's occlusion system
UFUNCTION(BlueprintCallable, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Enable Occlusion Culling"))
static void Settings_SetOcclusionCulling(bool Enable);
// Returns the number of visible room from the player's room (1 mean only the player room is visible)
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Get Occlusion Culling Distance"))
static int32 Settings_OcclusionDistance();
// Set the number of visible rooms from the player's room (1 mean only the player room is visible)
UFUNCTION(BlueprintCallable, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Set Occlusion Culling Distance"))
static void Settings_SetOcclusionDistance(int32 Distance);
// Returns true if actors with a RoomVisibility component should have their visibility toggled with the rooms
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings", meta = (DisplayName = "Should Dynamic Actors Be Occluded"))
static bool Settings_OccludeDynamicActors();
// ===== Gameplay Utility Functions =====
// Set player to spectate
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Player", meta = (DefaultToSelf = "Controller"))
static void Spectate(APlayerController* Controller, bool DestroyPawn = true);
};
@@ -0,0 +1,153 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "DungeonGeneratorBase.h"
#include "BoundsParams.h"
#include "QueueOrStack.h"
#include "DungeonGenerator.generated.h"
class IReadOnlyRoom;
// This is the main actor of the plugin. The dungeon generator is responsible to generate dungeons and replicate them over the network.
UCLASS(Blueprintable, ClassGroup = "Procedural Dungeon", HideCategories = "GenerationAlgorithm")
class PROCEDURALDUNGEON_API ADungeonGenerator : public ADungeonGeneratorBase
{
GENERATED_BODY()
public:
ADungeonGenerator();
protected:
//~ Begin ADungeonGeneratorBase Interface
virtual bool CreateDungeon_Implementation() override;
//~ End ADungeonGeneratorBase Interface
public:
// ===== Methods that should be overriden in blueprint =====
// Return the RoomData you want as root of the dungeon generation
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose First Room"))
URoomData* ChooseFirstRoomData();
/** Return the RoomData that will be connected to the Current Room
* @param CurrentRoom [DEPRECATED] Use CurrentRoomInstance->GetRoomData instead.
* @param CurrentRoomInstance The room instance to which the generator will connect the next room. This interface allows access only to some data.
* @param DoorData The door of the CurrentRoom on which the next room will be connected (its location in room units, its orientation and its type).
* @param DoorIndex The index of the door used on the next room to connect to the CurrentRoom.
* Use -1 for a random (compatible) door, or the door index from the RoomData door array (0 is the first door).
* WARNING: If the RandomDoor boolean of the RoomData is checked, then it will be considered -1 whatever you set here.
* @return The room data asset used to instantiate the new room instance (must not be null)
*/
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Next Room", ReturnDisplayName = "Room Data", AutoCreateRefTerm = "DoorIndex"))
URoomData* ChooseNextRoomData(const URoomData* CurrentRoom, const TScriptInterface<IReadOnlyRoom>& CurrentRoomInstance, const FDoorDef& DoorData, int& DoorIndex);
// Condition to validate a dungeon Generation
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Is Valid Dungeon"))
bool IsValidDungeon();
// Condition to continue or stop adding room to the dungeon
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Continue To Add Room"))
bool ContinueToAddRoom();
// ===== Utility functions you can use in blueprint =====
// Return true if a specific RoomData is already in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
bool HasAlreadyRoomData(URoomData* RoomData);
// Return true if at least one of the RoomData from the list provided is already in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
bool HasAlreadyOneRoomDataFrom(TArray<URoomData*> RoomDataList);
// Return the number of a specific RoomData in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
int CountRoomData(URoomData* RoomData);
// Return the total number of RoomData in the dungeon from the list provided
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
int CountTotalRoomData(TArray<URoomData*> RoomDataList);
// Return true if a specific RoomData type is already in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
bool HasAlreadyRoomType(TSubclassOf<URoomData> RoomType);
// Return true if at least one of the RoomData type from the list provided is already in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
bool HasAlreadyOneRoomTypeFrom(TArray<TSubclassOf<URoomData>> RoomTypeList);
// Return the number of a specific RoomData type in the dungeon
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
int CountRoomType(TSubclassOf<URoomData> RoomType);
// Return the total number of RoomData type in the dungeon from the list provided
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
int CountTotalRoomType(TArray<TSubclassOf<URoomData>> RoomTypeList);
// Returns the current number of room in the generated dungeon.
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (DisplayName = "Room Count", CompactNodeTitle = "Room Count", DeprecatedFunction, DeprecationMessage = "Use the same function from the Rooms variable."))
int GetNbRoom();
// Must be called in "Choose Next Room" function to be used.
// Tell explicitely the generator we don't want to place a room for a specific door.
// No error will be thrown when returning a null room data and no further room placement tries occur for this door (skip directly to the next door).
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
void DiscardRoom() { bDiscardRoom = true; }
private:
// Adds some new rooms linked to ParentRoom into Rooms list output
// AddedRooms contains only the new rooms added to Rooms list
// Returns true if the dungeon should keep adding new rooms
bool AddNewRooms(URoom& ParentRoom, TArray<URoom*>& AddedRooms);
public:
// In which order the dungeon generate rooms.
// Depth First: Dungeon will use the last generated room to place the next one. Resulting in a more linear dungeon.
// Breadth First: Dungeon will generate a room at each door of the current one before going to the next room. Resulting in a more spread dungeon.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
EGenerationType GenerationType;
// If ticked, newly placed room will check if any door is aligned with another room, and if so will connect them.
// If unticked, only the doors between CurrentRoom and NextRoom (in the function ChooseNextRoom) will be connected.
// (will only have effect if the deprecated CanLoop in the plugin settings is ticked too, until it is removed in a future version)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
bool bCanLoop {true};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation")
FBoundsParams DungeonLimits;
// If true, returning null in ChooseNextRoom is the same as calling DiscardRoom.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation", AdvancedDisplay)
bool bAutoDiscardRoomIfNull = false;
// Number of room as parent room to process per tick.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Procedural Generation", AdvancedDisplay)
int RoomBatchSize {10};
// Flag to explicitely tell we don't want to place a room.
bool bDiscardRoom = false;
private:
enum class EState : uint8
{
Idle,
Initializing,
AddingRooms,
Finalizing,
Completed
};
EState CurrentState {EState::Idle};
// Holds rooms pending to have new rooms added to them.
TQueueOrStack<URoom*> PendingRooms;
int CurrentTriesLeft {0};
};
@@ -0,0 +1,435 @@
// Copyright Benoit Pelletier 2019 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Math/RandomStream.h"
#include "ProceduralDungeonTypes.h"
#include "CollisionQueryParams.h"
#include "UObject/ScriptInterface.h"
#include "Serialization/Archive.h"
#include "DungeonGeneratorBase.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGenerationEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomEvent, const URoomData*, Room, const TScriptInterface<IReadOnlyRoom>&, RoomInstance);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FRoomDoorEvent, const URoomData*, Room, const FDoorDef&, Door);
class URoom;
class UDoorType;
class UDungeonGraph;
UENUM()
enum class EGenerationResult : uint8
{
None,
Error,
Success
};
UENUM(meta = (Bitflags))
enum class EGeneratorFlags
{
None = 0,
Generating = 1 << 0,
LoadSavedDungeon = 1 << 1,
All = 0b11 // add new 1 for each new flags
};
ENUM_CLASS_FLAGS(EGeneratorFlags);
// Holds the data for saving a dungeon state
USTRUCT(BlueprintType)
struct FDungeonSaveData
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly, Category = "GUID")
FGuid GeneratorId;
UPROPERTY()
TArray<uint8> Data {};
friend FArchive& operator<<(FArchive& Ar, FDungeonSaveData& Data);
friend void operator<<(FStructuredArchiveSlot Slot, FDungeonSaveData& Data);
};
// This is the main actor of the plugin. The dungeon generator is responsible to generate dungeons and replicate them over the network.
// This base class is abstract. You need to override the `CreateDungeon` function to write your own generation algorithm.
UCLASS(Abstract, Blueprintable, BlueprintType, ClassGroup = "Procedural Dungeon")
class PROCEDURALDUNGEON_API ADungeonGeneratorBase : public AActor
{
GENERATED_BODY()
public:
ADungeonGeneratorBase();
protected:
//~ Begin AActor Interface
virtual void PostInitializeComponents() override;
virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override;
virtual void Tick(float DeltaTime) override;
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override;
virtual void PostActorCreated() override;
//~ End AActor Interface
void SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading);
public:
// Update the seed and call the generation on all clients
// Do nothing when called on clients
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Dungeon Generator")
void Generate();
// Unload the current dungeon
// Do nothing when called on clients
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "Dungeon Generator")
void Unload();
// Create a saved data from the current dungeon state
UFUNCTION(BlueprintPure = false, Category = "Dungeon Generator")
void SaveDungeon(FDungeonSaveData& SaveData);
// Load a dungeon from a previously saved data
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
void LoadDungeon(const FDungeonSaveData& SaveData);
void SerializeDungeon(FArchive& Archive);
// ===== Methods that should be overriden in blueprint =====
// Return the door which will be spawned between Current Room and Next Room
// @param CurrentRoom The first of both rooms to have been generated. By default the door will face this room. [DEPRECATED] Use `CurrentRoomInstance->GetRoomData` instead.
// @param CurrentRoomInstance The room instance of one side of the door. By default the door will face this room.
// @param NextRoom The second of both rooms to have been generated. Set Flipped to true to make the door facing this room. [DEPRECATED] Use `NextRoomInstance->GetRoomData` instead.
// @param NextRoomInstance The room instance of the other side of the door. Set Flipped to true to make the door facing this room.
// @param DoorType The door type set by both room data. Use IsDoorOfType function to compare a door actor class with this.
// @param Flipped Tells which room the door is facing between CurrentRoom (false) and NextRoom (true).
// @return The door actor class to spawn between CurrentRoom and NextRoom.
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Choose Door"))
TSubclassOf<AActor> ChooseDoor(const URoomData* CurrentRoom, const URoom* CurrentRoomInstance, const URoomData* NextRoom, const URoom* NextRoomInstance, const UDoorType* DoorType, const UDoorType* OtherDoorType, bool& Flipped);
// ===== Optional functions to override =====
// Initialize the room instances during the generation step
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator")
void InitializeDungeon(const UDungeonGraph* Rooms);
// Returns which pawn is used for the room culling system.
// This pawn will also affect the PlayerInside variable of the rooms.
// By default returns GetPlayerController(0)->GetPawnOrSpectator().
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator")
APawn* GetVisibilityPawn(APlayerController* PlayerController);
// ===== Optional events =====
// Called once before anything else when generating a new dungeon.
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Pre Generation"))
void OnPreGeneration();
// Called once after all the dungeon generation (even if failed).
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Post Generation"))
void OnPostGeneration();
// Called before trying to generate a new dungeon and each time IsValidDungeon return false.
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Init"))
void OnGenerationInit();
// Called when a dungeon has been successfully generated (IsValidDungeon returned true).
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Success"))
void OnGenerationSuccess();
// Called when all dungeon generation tries are exhausted (IsValidDungeon always return false).
// No dungeon had been generated.
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Generation Failed"))
void OnGenerationFailed();
// Called each time a room is added in the dungeon (but not spawned yet).
// Those rooms can be destroyed without loading them if the generation try is not valid.
// @param NewRoom The room data successfully added to the dungeon [DEPRECATED: will be removed in future version, use RoomInstance->GetRoomData instead]
// @param RoomInstance The room successfully added to the dungeon
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "On Room Added"))
void OnRoomAdded(const URoomData* NewRoom, const TScriptInterface<IReadOnlyRoom>& RoomInstance);
// Called each time no room could have been placed at a door (all room placement tries have been exhausted).
UFUNCTION(BlueprintNativeEvent, Category = "Dungeon Generator", meta = (DisplayName = "Failed To Add Room"))
void OnFailedToAddRoom(const URoomData* FromRoom, const FDoorDef& FromDoor);
// ===== Utility functions you can use in blueprint =====
// Return a random RoomData from the array provided
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
URoomData* GetRandomRoomData(TArray<URoomData*> RoomDataArray);
// Return a random RoomData from the weighted map provided.
// For example: you have RoomA with weight 1 and RoomB with weight 2,
// then RoomA has proba of 1/3 and RoomB 2/3 to be returned.
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
URoomData* GetRandomRoomDataWeighted(const TMap<URoomData*, int>& RoomDataWeightedMap);
// Returns a random RoomCandidate from the array provided
// When the scores are used as weights, zero and negative scores are discarded automatically
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph", meta = (AdvancedDisplay = "bUseScoresAsWeights"))
const FRoomCandidate& GetRandomRoomCandidate(const TArray<FRoomCandidate>& RoomCandidates, bool bUseScoresAsWeights = true) const;
// Returns an array of room data with at least one compatible door with the door data provided.
// @param bSuccess True if at least one compatible room data was found.
// @param CompatibleRooms Filled with all compatible room data found.
// @param RoomDataArray The list of room data to check for compatibility.
// @param DoorData The door used to check if a room is compatible.
UFUNCTION(BlueprintPure, Category = "Dungeon Generator")
void GetCompatibleRoomData(bool& bSuccess, TArray<URoomData*>& CompatibleRooms, const TArray<URoomData*>& RoomDataArray, const FDoorDef& DoorData);
// Access to the random stream of the procedural dungeon. You should always use this for the procedural generation.
// @return The random stream used by the dungeon generator.
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (DeprecatedFunction, DeprecationMessage = "This one is buggy, use the `Random Stream` variable getter instead."))
const FRandomStream& GetRandomStream() const { return Random; }
// Returns the current generation progress.
UFUNCTION(BlueprintPure, Category = "Dungeon Generator")
float GetProgress() const;
// @TODO: remove this function and use Graph->GetRoomByIndex() instead.
URoom* GetRoomByIndex(int64 Index) const;
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator", meta = (WorldContext = "WorldContextObject"))
static void SaveAllDungeons(const UObject* WorldContextObject, TArray<FDungeonSaveData>& SavedData);
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator", meta = (WorldContext = "WorldContextObject"))
static void LoadAllDungeons(const UObject* WorldContextObject, const TArray<FDungeonSaveData>& SavedData);
// ===== Events =====
// Called once before anything else when generating a new dungeon.
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FGenerationEvent OnPreGenerationEvent;
// Called once after all the dungeon generation (even if failed).
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FGenerationEvent OnPostGenerationEvent;
// Called before trying to generate a new dungeon and each time IsValidDungeon return false.
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FGenerationEvent OnGenerationInitEvent;
// Called when a dungeon has been successfully generated (IsValidDungeon returned true).
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FGenerationEvent OnGenerationSuccessEvent;
// Called when all dungeon generation tries are exhausted (IsValidDungeon always return false).
// No dungeon had been generated.
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FGenerationEvent OnGenerationFailedEvent;
// Called each time a room is added in the dungeon (but not spawned yet).
// Those rooms can be destroyed without loading them if the generation try is not valid.
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FRoomEvent OnRoomAddedEvent;
// Called each time no room could have been placed at a door (all room placement tries have been exhausted).
UPROPERTY(BlueprintAssignable, Category = "Dungeon Generator")
FRoomDoorEvent OnFailedToAddRoomEvent;
protected:
// Create virtually the dungeon (no load nor initialization of room levels)
UFUNCTION(BlueprintNativeEvent, Category = "GenerationAlgorithm")
bool CreateDungeon();
// ===== Functions for dungeon creation =====
// Clear current graph and call GenerationInit event.
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
void StartNewDungeon();
// Initialize room instances after all rooms have been placed and connected (call InitializeDungeon).
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
void FinalizeDungeon();
// Create and initialize a new room instance using the room data provided.
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
URoom* CreateRoomInstance(URoomData* RoomData);
// Set the position and rotation of a room instance and return true if there is nothing colliding with it.
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", HidePin = "World"))
bool TryPlaceRoom(URoom* const& Room, int DoorIndex, const FDoorDef& TargetDoor, const UWorld* World = nullptr) const;
// Set the position and rotation of a room instance and return true if there is nothing colliding with it.
UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", HidePin = "World"))
bool TryPlaceRoomAtLocation(URoom* const& Room, FIntVector Location, EDoorDirection Rotation, const UWorld* World = nullptr) const;
// Check if the room instance provided is overlapping with existing rooms in the dungeon graph.
// Also checks if bUseWorldCollisionChecks is true, in which case a box overlap test is made in the persistent world.
bool CheckRoomOverlap(const URoom* const& Room, const UWorld* World = nullptr) const;
// Finalize the room creation by adding it to the dungeon graph. OnRoomAdded is called here.
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected, ReturnDisplayName = "Success", AutoCreateRefTerm = "DoorsToConnect", AdvancedDisplay = "DoorsToConnect,bFailIfNotConnected"))
bool AddRoomToDungeon(URoom* const& Room, const TArray<int>& DoorsToConnect, bool bFailIfNotConnected = true);
bool AddRoomToDungeon(URoom* const& Room);
// Tells the generator to wait next frame to continue the generation process.
UFUNCTION(BlueprintCallable, Category = "GenerationAlgorithm", meta = (BlueprintProtected))
void YieldGeneration();
private:
// Choose the door classes for all room connections.
// This must happen *after* Graph->InitRooms() to be able to choose door class for unconnected doors.
void ChooseDoorClasses();
// Update the player rooms based on the player position
void UpdatePlayerRooms();
// Update the rooms visibility based on the player position
void UpdateRoomVisibility(bool bForceUpdate = false);
// Update the rooms relevancy based on the player position
void UpdateRoomRelevancy();
// Reset all data from a specific generation
void Reset();
// Initialize the seed depending on the seed type setting
void UpdateSeed();
bool IsGenerating() const { return EnumHasAllFlags(Flags, EGeneratorFlags::Generating); }
bool IsLoadingSavedDungeon() const { return EnumHasAllFlags(Flags, EGeneratorFlags::LoadSavedDungeon); }
void DrawDebug() const;
// ===== FSM =====
void SetState(EGenerationState NewState);
void OnStateBegin(EGenerationState State);
void OnStateTick(EGenerationState State);
void OnStateEnd(EGenerationState State);
public:
// If ticked, the rooms location and rotation will be relative to this actor transform.
// If unticked, the rooms will be placed relatively to the world's origin.
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation")
bool bUseGeneratorTransform;
// How to handle the seed at each generation call.
// Random: Generate and use a random seed.
// Auto Increment: Use Seed for first generation, and increment it by SeedIncrement in each subsequent generation.
// Fixed: Use only Seed for each generation.
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation|Seed")
ESeedType SeedType;
// The increment number for each subsequent dungeon generation when SeedType is AutoIncrement.
UPROPERTY(EditAnywhere, SaveGame, Category = "Procedural Generation|Seed", meta = (EditCondition = "SeedType==ESeedType::AutoIncrement", EditConditionHides, DisplayAfter = "Seed"))
uint32 SeedIncrement;
// If ticked, when trying to place a new room during a dungeon generation,
// a box overlap test will be made to make sure the room will not spawn
// inside existing meshes in the persistent world.
// This is a heavy work and should be ticked only when necessary.
// Does not have impact during gameplay. Only during the generation process.
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Procedural Generation", AdvancedDisplay)
bool bUseWorldCollisionChecks {false};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Procedural Generation", AdvancedDisplay)
class UDungeonSettings* SettingsOverrides {nullptr};
UFUNCTION(BlueprintCallable, Category = "Dungeon Generator")
void SetSeed(int32 NewSeed);
UFUNCTION(BlueprintPure, Category = "Dungeon Generator", meta = (CompactNodeTitle = "Seed"))
int32 GetSeed() const;
FGuid GetGuid() const { return Id; }
inline bool UseGeneratorTransform() const { return bUseGeneratorTransform; }
FVector GetDungeonOffset() const;
FQuat GetDungeonRotation() const;
const FTransform& GetDungeonTransform() const;
const UDungeonSettings* GetSettings() const { return SettingsOverrides; }
FORCEINLINE const UDungeonGraph* GetRooms() const { return Graph; }
FORCEINLINE EGenerationState GetCurrentState() const { return CurrentState; }
protected:
UPROPERTY(BlueprintReadOnly, Instanced, Category = "Dungeon Generator", meta = (DisplayName = "Rooms", ExposeFunctionCategories = "Dungeon Graph"))
UDungeonGraph* Graph;
UPROPERTY(BlueprintReadOnly, VisibleInstanceOnly, NonPIEDuplicateTransient, TextExportTransient, Category = "GUID")
FGuid Id;
private:
UPROPERTY(Replicated, EditAnywhere, SaveGame, Category = "Procedural Generation|Seed", meta = (EditCondition = "SeedType!=ESeedType::Random", EditConditionHides))
uint32 Seed;
UPROPERTY(BlueprintReadOnly, Category = "Dungeon Generator", meta = (DisplayName = "Random Stream", AllowPrivateAccess = true))
FRandomStream Random;
#if WITH_EDITORONLY_DATA
// If true the dungeon will be saved in a human readable json format.
// *WARNING*: This is only available in editor and dev builds and will not change anything in packaged builds. It should be used for debugging purposes only.
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
bool bUseJsonSave {false};
// Draws the computed dungeon bounding box.
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
bool bDrawDebugDungeonBounds {false};
#endif
// If true, the generator will manage the default UE navmesh system to rebuild it at the end of a generation.
// If false, the generator will do nothing with the navigation system.
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
bool bRebuildNavmesh {true};
// Maximum distance (in number of rooms) at which a room is considered relevant for a player.
UPROPERTY(EditAnywhere, AdvancedDisplay, Category = "Procedural Generation", meta = (AllowPrivateAccess = true))
int32 RoomRelevanceMaxDistance {5};
EGenerationState CurrentState {EGenerationState::Idle};
EGeneratorFlags Flags {EGeneratorFlags::None};
// Set to avoid adding increment the seed after we've set manually the seed
bool bShouldIncrement {false};
struct FPlayerRooms
{
// The rooms the player has left this frame
TSet<URoom*> OldRooms;
// The rooms the player is currently inside this frame
TSet<URoom*> CurrentRooms;
// Whether the current rooms has changed this frame
bool bHasChanged {false};
// Move current rooms to old rooms and clear current rooms
void Roll()
{
OldRooms = MoveTemp(CurrentRooms);
CurrentRooms.Empty();
}
void AddCurrentRoom(URoom* Room)
{
OldRooms.Remove(Room);
CurrentRooms.Add(Room);
}
};
// Occlusion culling system
TMap<int32, FPlayerRooms> PlayerRooms;
// Transient. Only used to detect when occlusion setting is changed.
bool bWasOcclusionEnabled {false};
// Transient. Only used to detect when occlusion distance is changed.
uint32 PreviousOcclusionDistance {0};
// Transient. Used to count unloaded/loaded/initialized rooms during generation.
int32 CachedTmpRoomCount {0};
// Transient. Cached collision params used when bUseWorldCollisionChecks is true
FCollisionQueryParams WorldCollisionParams;
// Transient. Current generation status
EGenerationStatus GenerationStatus {EGenerationStatus::NotStarted};
};
@@ -0,0 +1,279 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "ReplicableObject.h"
#include "Interfaces/RoomContainer.h"
#include "Interfaces/GeneratorProvider.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Templates/SubclassOf.h"
#include "Templates/Function.h"
#include "ProceduralDungeonTypes.h"
#include "VoxelBounds/VoxelBounds.h"
#include "DungeonOctree.h"
#include "DungeonGraph.generated.h"
class URoom;
class URoomData;
class URoomCustomData;
class URoomConnection;
class ADungeonGeneratorBase;
// Holds the generated dungeon.
// You can access the rooms using many functions.
UCLASS(BlueprintType)
class PROCEDURALDUNGEON_API UDungeonGraph : public UReplicableObject, public IRoomContainer, public IGeneratorProvider, public IDungeonCustomSerialization, public IDungeonSaveInterface
{
GENERATED_BODY()
friend ADungeonGeneratorBase;
#if WITH_DEV_AUTOMATION_TESTS
friend class FDungeonGraphTest;
#endif
public:
UDungeonGraph();
//~ Begin IRoomContainer Interface
// Returns the room instance with the provided index.
// Returns null if no room exists with the provided index.
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
virtual URoom* GetRoomByIndex(int64 Index) const final;
virtual URoomConnection* GetConnectionByIndex(int32 Index) const override;
//~ End IRoomContainer Interface
//~ Begin IDungeonCustomSerialization Interface
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override;
//~ End IDungeonCustomSerialization Interface
//~ Begin IDungeonSaveInterface Interface
virtual void PostLoadDungeon_Implementation() override;
//~ End IDungeonSaveInterface Interface
//~ Begin IGeneratorProvider Interface
virtual ADungeonGeneratorBase* GetGenerator() const override { return Generator.Get(); }
//~ End IGeneratorProvider Interface
void AddRoom(URoom* Room);
void InitRooms();
void Clear();
bool CanRoomFit(const URoom* Room) const;
bool TryConnectDoor(URoom* Room, int32 DoorIndex);
bool TryConnectToExistingDoors(URoom* Room);
TArray<URoom*> GetAllRoomsOverlapping(const FBox& Box) const;
bool HasRooms() const { return Rooms.Num() > 0; }
bool IsDirty() const { return bIsDirty; }
// Returns all rooms
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
const TArray<URoom*>& GetAllRooms() const { return Rooms; }
// Returns all room connections
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
const TArray<URoomConnection*>& GetAllConnections() const { return RoomConnections; }
// Returns all room instances of the provided room data
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
void GetAllRoomsFromData(const URoomData* Data, TArray<URoom*>& Rooms);
// Returns all room instances of any of the provided room data
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
void GetAllRoomsFromDataList(const TArray<URoomData*>& Data, TArray<URoom*>& Rooms);
// Returns the first found room instance of the provided room data
// (no defined order, so could be any room of the dungeon)
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
const URoom* GetFirstRoomFromData(const URoomData* Data);
// Returns all room instances having the provided custom data
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "CustomData"))
void GetAllRoomsWithCustomData(const TSubclassOf<URoomCustomData>& CustomData, TArray<URoom*>& Rooms);
// Returns all room instances having ALL the provided custom data
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
void GetAllRoomsWithAllCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& Rooms);
// Returns all room instances having at least one of the provided custom data
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
void GetAllRoomsWithAnyCustomData(const TArray<TSubclassOf<URoomCustomData>>& CustomData, TArray<URoom*>& Rooms);
// Returns a random room from an array of room
UFUNCTION(BlueprintCallable, Category = "Dungeon Graph")
URoom* GetRandomRoom(const TArray<URoom*>& RoomList) const;
// Returns the total number of room
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (CompactNodeTitle = "Count"))
int32 Count() const { return Rooms.Num(); }
// Returns true if a specific RoomData is already in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
bool HasAlreadyRoomData(const URoomData* RoomData) const;
// Returns true if at least one of the RoomData from the list provided is already in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
bool HasAlreadyOneRoomDataFrom(const TArray<URoomData*>& RoomDataList) const;
// Returns the number of a specific RoomData in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
int CountRoomData(const URoomData* RoomData) const;
// Returns the total number of RoomData in the dungeon from the list provided
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
int CountTotalRoomData(const TArray<URoomData*>& RoomDataList) const;
// Returns true if a specific RoomData type is already in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "RoomType"))
bool HasAlreadyRoomType(const TSubclassOf<URoomData>& RoomType) const;
// Returns true if at least one of the RoomData type from the list provided is already in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
bool HasAlreadyOneRoomTypeFrom(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const;
// Returns the number of a specific RoomData type in the dungeon
UFUNCTION(BlueprintPure, Category = "Dungeon Graph", meta = (AutoCreateRefTerm = "RoomType"))
int CountRoomType(const TSubclassOf<URoomData>& RoomType) const;
// Returns the total number of RoomData type in the dungeon from the list provided
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
int CountTotalRoomType(const TArray<TSubclassOf<URoomData>>& RoomTypeList) const;
// Returns wether a path is valid between 2 rooms (no locked room blocking the way)
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ReturnDisplayName = "Yes"))
bool HasValidPath(const URoom* From, const URoom* To, bool IgnoreLockedRooms = false) const;
// Returns the minimum number of connected rooms between A and B.
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph")
int32 NumberOfRoomBetween(const URoom* A, const URoom* B, bool IgnoreLockedRooms = false) const;
// Returns the minimum number of connected rooms between A and B.
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (DisplayName = "Number Of Room Between (using ReadOnlyRoom)"))
int32 NumberOfRoomBetween_ReadOnly(TScriptInterface<IReadOnlyRoom> A, TScriptInterface<IReadOnlyRoom> B) const;
// Returns the path between A and B.
// Note: Could be pure, but since it can be heavy duty for large dungeons, keep it impure to avoid duplicate calls.
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ReturnDisplayName = "Has Path"))
bool GetPathBetween(const URoom* A, const URoom* B, TArray<URoom*>& ResultPath, bool IgnoreLockedRooms = false) const;
// Returns the room instance at the provided room cell (expressed in Room Units, not Unreal Units!!!).
// Returns null if no room located at the provided cell.
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
URoom* GetRoomAt(FIntVector RoomCell) const;
// Returns the center of the bounding box of the dungeon.
// @see GetDungeonBoundsExtents
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
FVector GetDungeonBoundsCenter() const;
// Returns the extent (half size) of the bounding box of the dungeon.
// @see GetDungeonBoundsCenter
UFUNCTION(BlueprintPure, Category = "Dungeon Graph")
FVector GetDungeonBoundsExtent() const;
UFUNCTION(BlueprintPure = false, Category = "Dungeon Graph", meta = (ExpandBoolAsExecs = "ReturnValue", AdvancedDisplay = "CustomFilter", AutoCreateRefTerm = "CustomScore"))
bool FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms, const FScoreCallback& CustomScore) const;
bool FilterAndSortRooms(const TArray<URoomData*>& RoomList, const FDoorDef& FromDoor, TArray<FRoomCandidate>& SortedRooms) const;
// Returns the computed dungeon bounds.
class FBoxCenterAndExtent GetDungeonBounds(const FTransform& Transform = FTransform::Identity) const;
struct FBoxMinAndMax GetIntBounds() const;
FVoxelBounds GetVoxelBounds() const { return Bounds; }
// Returns in OutRooms all the rooms in the Distance from each InRooms and optionally apply Func on each rooms.
// Distance is the number of room connection between 2 rooms, not the distance in any unit.
static void TraverseRooms(const TSet<URoom*>& InRooms, TSet<URoom*>* OutRooms, uint32 Distance, TFunction<void(URoom*, uint32)> Func);
static bool FindPath(const URoom* From, const URoom* To, TArray<const URoom*>* OutPath = nullptr, bool IgnoreLocked = false);
protected:
int CountRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const;
void GetRoomsByPredicate(TArray<URoom*>& OutRooms, TFunction<bool(const URoom*)> Predicate) const;
const URoom* FindFirstRoomByPredicate(TFunction<bool(const URoom*)> Predicate) const;
//~ Begin UReplicableObject Interface
virtual bool ReplicateSubobjects(UActorChannel* Channel, FOutBunch* Bunch, FReplicationFlags* RepFlags) override;
virtual void RegisterReplicableSubobjects(bool bRegister) override;
//~ End UReplicableObject Interface
// Sync Rooms and ReplicatedRooms arrays
void SynchronizeRooms();
// Replace existing room array from the one loaded in saved data.
// This does nothing if there is no data loaded from a saved dungeon.
void RetrieveRoomsFromLoadedData();
// Create and store a new connection between two rooms in RoomConnections.
void Connect(URoom* RoomA, int32 DoorA, URoom* RoomB, int32 DoorB);
bool AreRoomsLoaded(int32& NbRoomLoaded) const;
bool AreRoomsUnloaded(int32& NbRoomUnloaded) const;
bool AreRoomsInitialized(int32& NbRoomInitialized) const;
bool AreRoomsReady() const;
void SpawnAllDoors();
void LoadAllRooms();
void UnloadAllRooms();
void MarkDirty() { bIsDirty = true; }
// Extends the bounds if necessary to include the provided room.
void UpdateBounds(const URoom* Room);
// Recreate the bounds using the whole room list.
void RebuildBounds();
void UpdateOctree(URoom* Room);
void RebuildOctree();
private:
UPROPERTY(Transient)
TArray<URoom*> Rooms;
UPROPERTY(Replicated, Transient)
TArray<URoomConnection*> RoomConnections;
// This array is synchronized with the server
// We keep it separated to be able to unload previous rooms on clients
UPROPERTY(ReplicatedUsing = OnRep_Rooms, Transient)
TArray<URoom*> ReplicatedRooms;
UFUNCTION()
void OnRep_Rooms();
bool bIsDirty {false};
// @TODO: Make something to decouple the ADungeonGenerator from the UDungeonGraph.
// It is currently used only to get its random stream in the `Get Random Room` function.
// We could instead either:
// - Use an interface that provides a random stream => good way to not induce breaking changes in the code.
// - Pass the random stream as an input to that function => will need to make some changes in existing projects.
TWeakObjectPtr<ADungeonGeneratorBase> Generator {nullptr};
// Transient. The computed bounds of the dungeon. Updated each time the room list changes.
FVoxelBounds Bounds;
// Transient, used for room collision checks.
FDungeonOctree Octree;
private:
struct FSaveData
{
TArray<URoom*> Rooms;
TArray<URoomConnection*> Connections;
};
TUniquePtr<FSaveData> SavedData {nullptr};
};
@@ -0,0 +1,82 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "Math/GenericOctree.h"
#include "Misc/EngineVersionComparison.h"
#if UE_VERSION_OLDER_THAN(4, 26, 0)
#define USE_LEGACY_OCTREE 1
#else
#define USE_LEGACY_OCTREE 0
#endif
class URoom;
struct FDungeonOctreeElement
{
URoom* Room;
FBoxCenterAndExtent Bounds;
int32 Index;
FDungeonOctreeElement(URoom* Room, int32 BoxIndex);
};
struct FDungeonOctreeSemantics
{
enum { MaxElementsPerLeaf = 16 };
enum { MinInclusiveElementsPerNode = 7 };
enum { MaxNodeDepth = 12 };
typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;
FORCEINLINE static const FBoxCenterAndExtent& GetBoundingBox(const FDungeonOctreeElement& Element)
{
return Element.Bounds;
}
FORCEINLINE static bool AreElementsEqual(const FDungeonOctreeElement& A, const FDungeonOctreeElement& B)
{
return A.Room == B.Room && A.Index == B.Index;
}
FORCEINLINE static void SetElementId(const FDungeonOctreeElement& Element
#if USE_LEGACY_OCTREE
, FOctreeElementId Id)
#else
, FOctreeElementId2 Id)
#endif
{
}
FORCEINLINE static void ApplyOffset(FDungeonOctreeElement& Element, FVector Offset)
{
Element.Bounds.Center += Offset;
}
};
using FDungeonOctree =
#if USE_LEGACY_OCTREE
TOctree<FDungeonOctreeElement, FDungeonOctreeSemantics>;
#else
TOctree2<FDungeonOctreeElement, FDungeonOctreeSemantics>;
#endif
template<typename IterateBoundsFunc>
inline void FindElementsWithBoundsTest(const FDungeonOctree& Octree, const FBoxCenterAndExtent& Bounds, const IterateBoundsFunc& Func)
{
#if USE_LEGACY_OCTREE
for (FDungeonOctree::TConstElementBoxIterator<> OctreeIt(Octree, Bounds); OctreeIt.HasPendingElements(); OctreeIt.Advance())
{
const FDungeonOctreeElement& Element = OctreeIt.GetCurrentElement();
Func(Element);
}
#else
Octree.FindElementsWithBoundsTest(Bounds, Func);
#endif
}
@@ -0,0 +1,41 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
#include "Templates/SubclassOf.h"
#include "UObject/SoftObjectPath.h"
#include "Serialization/StructuredArchive.h"
// Archive proxy specialized for the dungeon.
struct FDungeonSaveProxyArchive : public FObjectAndNameAsStringProxyArchive
{
public:
FDungeonSaveProxyArchive(FArchive& InInnerArchive)
: FObjectAndNameAsStringProxyArchive(InInnerArchive, true)
{
ArIsSaveGame = true;
//ArNoDelta = true;
}
virtual FArchive& operator<<(FSoftObjectPath& Value) override
{
// Calls Value.SerializePath()
FObjectAndNameAsStringProxyArchive::operator<<(Value);
//UE_LOG(LogTemp, Warning, TEXT("Custom serialization of a SoftObjectPath!"));
// If we have a defined core redirect, make sure that it's applied
if (IsLoading() && !Value.IsNull())
{
Value.FixupCoreRedirects();
}
return *this;
}
};
@@ -0,0 +1,31 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "DungeonSettings.generated.h"
// Data asset to allow overriding the plugin's settings
UCLASS(BlueprintType)
class PROCEDURALDUNGEON_API UDungeonSettings : public UDataAsset
{
GENERATED_BODY()
public:
UDungeonSettings();
UFUNCTION(BlueprintPure, Category = "Procedural Dungeon|Settings")
static FVector GetRoomUnit(const UDungeonSettings* Settings = nullptr);
private:
// Size of a room unit. Room's size in data assets will express the multiple of this unit size.
// For example a room size of (5, 10, 1) with a unit size of (100, 100, 400) will result of a real room size of (500, 1000, 400).
UPROPERTY(EditAnywhere, Category = "General", meta = (ClampMin = 0, AllowPrivateAccess=true))
FVector RoomUnit;
};
@@ -0,0 +1,36 @@
// Copyright Benoit Pelletier 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "DoorInterface.generated.h"
class UDoorType;
class URoomConnection;
UINTERFACE(MinimalAPI, Blueprintable, BlueprintType)
class UDoorInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface to implement on any Actor or ActorComponent used as a Door in the dungeon.
*/
class PROCEDURALDUNGEON_API IDoorInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Door Interface")
const UDoorType* GetDoorType() const;
UFUNCTION(BlueprintNativeEvent, Category = "Door Interface")
void SetRoomConnection(URoomConnection* RoomConnection);
};
@@ -0,0 +1,38 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "DungeonCustomSerialization.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
class UDungeonCustomSerialization : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class PROCEDURALDUNGEON_API IDungeonCustomSerialization
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
// Serialize non-trivial object properties (e.g. UObject pointers)
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) = 0;
// Fixup object references after loading
virtual bool FixupReferences(UObject* Context) { return true; }
// Calls FixupReferences on Obj and its subobjects.
static bool DispatchFixupReferences(UObject* Obj, UObject* Context);
};
@@ -0,0 +1,48 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "DungeonSaveInterface.generated.h"
UINTERFACE(BlueprintType, Blueprintable, meta = (Tooltip = "Give access to some serialization events to actors saved within a dungeon."))
class UDungeonSaveInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface to add some events to the saved actors/objects during the save/load process of the dungeon.
*/
class PROCEDURALDUNGEON_API IDungeonSaveInterface
{
GENERATED_BODY()
public:
// Called just before serializing this object into the dungeon save.
// Useful to initialize some saved variables based on actor states.
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
void DungeonPreSerialize(bool bIsLoading);
// Called just after deserializing this object from the dungeon save
// Useful to initialize some actor states based on saved variables.
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
void DungeonPostSerialize(bool bIsLoading);
// Called first before saving the dungeon
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
void PreSaveDungeon();
// Called last after loading the dungeon
UFUNCTION(BlueprintNativeEvent, Category = "Procedural Dungeon")
void PostLoadDungeon();
static void DispatchPreSaveEvent(UObject* Obj);
static void DispatchPostLoadEvent(UObject* Obj);
};
@@ -0,0 +1,34 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "GeneratorProvider.generated.h"
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
class UGeneratorProvider : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for classes that give access to a ADungeonGeneratorBase instance.
* @TODO: Currently only used to resolve URoom::GeneratorOwner references when loading a saved dungeon.
* It would be better in a future version to decouple the URoom from the DungeonGenerator and instead
* pass some Interface references to the functions needed (currently a Transform and a RandomStream).
* I just want to say that this interface is just temporary and must not be used by users of the plugin,
* as it will certainly be removed in a near future version of the plugin.
*/
class IGeneratorProvider
{
GENERATED_BODY()
public:
virtual class ADungeonGeneratorBase* GetGenerator() const = 0;
};
@@ -0,0 +1,42 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "RoomActorGuid.generated.h"
UINTERFACE(BlueprintType, Blueprintable, meta = (Tooltip = "Interface to access a custom Guid for actors saved within a dungeon."))
class URoomActorGuid : public UInterface
{
GENERATED_BODY()
};
// Interface for all saveable actors placed in room levels
// The guid must be constant across game sessions to be able to save/load the actors.
// It can be placed on ActorComponents too, but the interface on the Actor itself will be prioritized.
// Only the first component found that implements the interface will be used. Make sure to have only one to prevent any confusions.
class PROCEDURALDUNGEON_API IRoomActorGuid
{
GENERATED_BODY()
public:
// Return the guid associated with this actor.
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Room Actor Id")
FGuid GetGuid() const;
// Returns true if the actor should be included in the saved dungeon.
// Returns false to just use a Guid without the need to include the actor in saved games.
UFUNCTION(BlueprintNativeEvent, Category = "Room Actor Id")
bool ShouldSaveActor() const;
// Return the object implementing the IRoomActorGuid interface from the provided actor.
// It can be implemented on the Actor itself or its components.
// If both, the actor implementation will be returned.
static UObject* GetImplementer(AActor* Actor);
};
@@ -0,0 +1,34 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "RoomContainer.generated.h"
class URoom;
class URoomConnection;
UINTERFACE(MinimalAPI, NotBlueprintable, NotBlueprintType, meta = (CannotImplementInterfaceInBlueprint))
class URoomContainer : public UInterface
{
GENERATED_BODY()
};
/**
* Common interface for all containers that holds rooms and their connections.
* Currently used to get back references in URoom and URoomConnection when loaded from a saved dungeon.
*/
class PROCEDURALDUNGEON_API IRoomContainer
{
GENERATED_BODY()
public:
virtual URoom* GetRoomByIndex(int64 Index) const = 0;
virtual URoomConnection* GetConnectionByIndex(int32 Index) const = 0;
};
@@ -0,0 +1,26 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FProceduralDungeonModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
virtual bool SupportsDynamicReloading() override { return true; }
private:
void RegisterSettings();
void UnregisterSettings();
bool HandleSettingsSaved();
};
@@ -0,0 +1,33 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Misc/Guid.h"
// Custom serialization version for Procedural Dungeon
struct FProceduralDungeonCustomVersion
{
enum Type
{
// Before any version changes were made
InitialVersion = 0,
SoftObjectPtrFix, // Fixed issues with SoftObjectPtr replication in Room.h
RoomDataBoundingBoxesMigration, // Migrated FirstPoint/SecondPoint to BoundingBoxes in RoomData.h
DoorLogicRefactored, // Migrated logic for the ADoor into DoorComponent + DoorState
// -----<new versions can be added above this line>-------------------------------------------------
VersionPlusOne,
LatestVersion = VersionPlusOne - 1
};
// The GUID for this custom version number
PROCEDURALDUNGEON_API const static FGuid GUID;
FProceduralDungeonCustomVersion() = delete;
};
@@ -0,0 +1,55 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Containers/UnrealString.h"
#include "ProceduralDungeonSettings.h"
DECLARE_LOG_CATEGORY_EXTERN(LogProceduralDungeon, Log, All);
void LogOnScreen(const FString& Message, FColor Color, bool bForceOnScreen = false);
#if NO_LOGGING
#define _DungeonLog_PrivateImpl(...) {}
#else
// Private implementation. Dot not use it.
#define _DungeonLog_PrivateImpl(ShowOnScreen, ForceOnScreen, Detailed, Color, Verbosity, Format, ...) \
{ \
if constexpr (Detailed) \
{ UE_LOG(LogProceduralDungeon, Verbosity, TEXT("[%s:%d] " Format), *FString(__FUNCTION__), __LINE__, ##__VA_ARGS__); } \
else \
{ UE_LOG(LogProceduralDungeon, Verbosity, TEXT(Format), ##__VA_ARGS__); } \
if constexpr (ShowOnScreen) \
LogOnScreen(FString::Printf(TEXT(Format), ##__VA_ARGS__), Color, ForceOnScreen); \
}
#endif // NO_LOGGING
// Logs error message to output and on screen
#define DungeonLog_Debug(Format, ...) \
_DungeonLog_PrivateImpl(false, false, true, FColor::White, VeryVerbose, Format, ##__VA_ARGS__)
// Logs info message to output and on screen
#define DungeonLog_Info(Format, ...) \
_DungeonLog_PrivateImpl(true, false, false, FColor::White, Log, Format, ##__VA_ARGS__)
// Logs info message *only* to output
#define DungeonLog_InfoSilent(Format, ...) \
_DungeonLog_PrivateImpl(false, false, false, FColor::White, Log, Format, ##__VA_ARGS__)
// Logs warning message to output and on screen
#define DungeonLog_Warning(Format, ...) \
_DungeonLog_PrivateImpl(true, false, true, FColor::Yellow, Warning, Format, ##__VA_ARGS__)
// Logs warning message *only* to output
#define DungeonLog_WarningSilent(Format, ...) \
_DungeonLog_PrivateImpl(false, false, true, FColor::Yellow, Warning, Format, ##__VA_ARGS__)
// Logs error message to output and on screen
#define DungeonLog_Error(Format, ...) \
_DungeonLog_PrivateImpl(true, true, true, FColor::Red, Error, Format, ##__VA_ARGS__)
@@ -0,0 +1,114 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Engine/EngineTypes.h"
#include "ProceduralDungeonSettings.generated.h"
// Holds the plugin's settings.
UCLASS(Config = Game, DefaultConfig)
class PROCEDURALDUNGEON_API UProceduralDungeonSettings : public UObject
{
GENERATED_BODY()
public:
UProceduralDungeonSettings(const FObjectInitializer& ObjectInitializer);
// Size of a room unit. Room's size in data assets will express the multiple of this unit size.
// For example a room size of (5, 10, 1) with a unit size of (100, 100, 400) will result of a real room size of (500, 1000, 400).
UPROPERTY(EditAnywhere, config, Category = "General", meta = (ClampMin = 0))
FVector RoomUnit;
// The bounding size of the doors. It is used only to display the door's blue box when "Draw Debug" is set to true.
UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Default Door Size", ClampMin = 0))
FVector DoorSize;
// The height of the door's base from the room's base (in percentage of the room unit Z)
UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Default Door Offset", ClampMin = 0, ClampMax = 1, UIMin = 0, UIMax = 1))
float DoorOffset;
// When true, doors will be connected as long they are at the same place.
// When false, only the doors between the previous and the new generated room will be connected.
// DEPRECATED: Keep it true and use the CanLoop setting in the DungeonGenerator actor instead. This project-wide setting will be removed in a future version.
UPROPERTY(EditAnywhere, config, Category = "General")
bool CanLoop;
// The object type used for the dungeon rooms trigger boxes.
// Defaulted to Engine Trace Channel 6.
// You can create new ones in your project settings under the Collision tab.
UPROPERTY(EditAnywhere, config, Category = "General")
TEnumAsByte<ECollisionChannel> RoomObjectType {ECollisionChannel::ECC_EngineTraceChannel6};
// The number of dungeon generation retry before the generator gives up.
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
int32 MaxGenerationTry;
// The number of room placement retry on a specific door before the generator gives up and continues with the next door.
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
int32 MaxRoomPlacementTry;
// The number of room placement retry on a specific door before the generator gives up and continues with the next door.
UPROPERTY(EditAnywhere, config, Category = "General", AdvancedDisplay, meta = (UIMin = 1, ClampMin = 1))
int32 RoomLimit;
// The rooms visibility will be toggled off when the player is not inside it or in a room next to it.
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (DisplayName = "Enable Occlusion Culling"))
bool OcclusionCulling;
// The legacy occlusion culling system only toggles the visibility of the actors in the rooms, keeping the collisions, ticking and all oher things.
// The new system toggles instead the whole room levels visibility, shutting off the ticking and other things of the actors and the level script.
//UPROPERTY(EditAnywhere, config, Category = "Procedural Dungeon", meta=(EditCondition="OcclusionCulling"))
//bool LegacyOcclusion;
// Defines how many connected rooms are visible from the player's room (1 means only the room where the player is).
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (EditCondition = "OcclusionCulling", UIMin = 1, ClampMin = 1))
int32 OcclusionDistance;
// Keep track of dynamic actors entering and leaving the room to be able to show/hide them with the room.
// TODO: Still useful? It was there for performance issues, but there is none anymore...
// Maybe moving it in a console variable only for debug purpose?
UPROPERTY(EditAnywhere, config, Category = "Occlusion Culling", meta = (EditCondition = "OcclusionCulling"))
bool OccludeDynamicActors;
// Show room and door outlines in editor and development builds
UPROPERTY(EditAnywhere, config, Category = "Debug")
bool DrawDebug;
// Show room and door outlines in editor and development builds
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
bool bDrawOnlyWhenEditingRooms;
// Show the room origin in magenta
// DEPRECATED: This setting will be removed in a future release of the plugin.
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
bool ShowRoomOrigin;
// Flip side the arrow that shows door facing direction.
// True means that the arrow gets inside the room (opposite of door actor's forward).
// False means that the arrow goes outside the room (same as door actor's forward).
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
bool bFlipDoorArrowSide;
// Length of the door's debug arrow.
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
float DoorArrowLength;
// Size of the door's debug arrow head.
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "DrawDebug"))
float DoorArrowHeadSize;
// Show some logs on the screen
UPROPERTY(EditAnywhere, config, Category = "Debug")
bool OnScreenPrintDebug;
// Duration of the screen logs
UPROPERTY(EditAnywhere, config, Category = "Debug", meta = (EditCondition = "OnScreenPrintDebug"))
float PrintDebugDuration;
};
@@ -0,0 +1,236 @@
// Copyright Benoit Pelletier 2019 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Math/GenericOctree.h" // FBoxCenterAndExtent
#include "ProceduralDungeonTypes.generated.h"
UENUM()
enum class EGenerationState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Generation UMETA(DisplayName = "Generation"),
Load UMETA(DisplayName = "Load"),
Initialization UMETA(DisplayName = "Initialization"),
Unload UMETA(DisplayName = "Unload"),
Play UMETA(DisplayName = "Play"),
NbState UMETA(Hidden)
};
UENUM()
enum class EGenerationStatus : uint8
{
NotStarted UMETA(DisplayName = "Not Started"),
InProgress UMETA(DisplayName = "In Progress"),
Completed UMETA(DisplayName = "Completed"),
Failed UMETA(DisplayName = "Failed"),
NbStatus UMETA(Hidden)
};
// The different directions a door can face.
UENUM(BlueprintType, meta = (DisplayName = "Door Direction"))
enum class EDoorDirection : uint8
{
North = 0 UMETA(DisplayName = "North", ToolTip = "rotation = 0 | positive X (world forward)"),
East = 1 UMETA(DisplayName = "East", ToolTip = "rotation = 90 | positive Y (world right)"),
South = 2 UMETA(DisplayName = "South", ToolTip = "rotation = 180 | negative X (world backward)"),
West = 3 UMETA(DisplayName = "West", ToolTip = "rotation = 270 | negative Y (world left)"),
NbDirection = 4 UMETA(Hidden)
};
bool PROCEDURALDUNGEON_API operator!(const EDoorDirection& Direction);
EDoorDirection PROCEDURALDUNGEON_API operator+(const EDoorDirection& A, const EDoorDirection& B);
EDoorDirection PROCEDURALDUNGEON_API operator-(const EDoorDirection& A, const EDoorDirection& B);
// TODO: Don't know how to export these...
EDoorDirection& operator+=(EDoorDirection& A, const EDoorDirection& B);
EDoorDirection& operator-=(EDoorDirection& A, const EDoorDirection& B);
EDoorDirection& operator++(EDoorDirection& Direction);
EDoorDirection& operator--(EDoorDirection& Direction);
EDoorDirection PROCEDURALDUNGEON_API operator++(EDoorDirection& Direction, int);
EDoorDirection PROCEDURALDUNGEON_API operator--(EDoorDirection& Direction, int);
EDoorDirection PROCEDURALDUNGEON_API operator-(const EDoorDirection& Direction);
EDoorDirection PROCEDURALDUNGEON_API operator~(const EDoorDirection& Direction);
inline EDoorDirection PROCEDURALDUNGEON_API Opposite(const EDoorDirection& Direction) { return ~Direction; }
FIntVector PROCEDURALDUNGEON_API ToIntVector(const EDoorDirection& Direction);
FVector PROCEDURALDUNGEON_API ToVector(const EDoorDirection& Direction);
FQuat PROCEDURALDUNGEON_API ToQuaternion(const EDoorDirection& Direction);
float PROCEDURALDUNGEON_API ToAngle(const EDoorDirection& Direction);
FIntVector PROCEDURALDUNGEON_API Rotate(const FIntVector& Pos, const EDoorDirection& Rot);
FVector PROCEDURALDUNGEON_API Rotate(const FVector& Pos, const EDoorDirection& Rot);
FIntVector PROCEDURALDUNGEON_API Transform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation);
FIntVector PROCEDURALDUNGEON_API InverseTransform(const FIntVector& Pos, const FIntVector& Translation, const EDoorDirection& Rotation);
// Those ones are just for consistent naming and centralized code
EDoorDirection PROCEDURALDUNGEON_API Transform(const EDoorDirection& Direction, const EDoorDirection& Rotation);
EDoorDirection PROCEDURALDUNGEON_API InverseTransform(const EDoorDirection& Direction, const EDoorDirection& Rotation);
//The different types of generation algorithms.
UENUM(BlueprintType, meta = (DisplayName = "Generation Type"))
enum class EGenerationType : uint8
{
DFS = 0 UMETA(DisplayName = "Depth First", Tooltip = "Make the dungeon more linear"),
BFS = 1 UMETA(DisplayName = "Breadth First", Tooltip = "Make the dungeon less linear"),
NbType = 2 UMETA(Hidden)
};
// The different types of seed update at each generation.
UENUM(BlueprintType, meta = (DisplayName = "Seed Type"))
enum class ESeedType : uint8
{
Random = 0 UMETA(DisplayName = "Random", Tooltip = "Random seed at each generation"),
AutoIncrement = 1 UMETA(DisplayName = "Auto Increment", Tooltip = "Get the initial seed and increment at each generation"),
Fixed = 2 UMETA(DisplayName = "Fixed", Tooltip = "Always use initial seed (or you can set it manually via blueprint)"),
NbType = 3 UMETA(Hidden)
};
// Visibility mode for Room Visibilty Components.
UENUM(BlueprintType, meta = (DisplayName = "Room Visibility"))
enum class EVisibilityMode : uint8
{
Default UMETA(DisplayName = "Same As Room"),
ForceVisible UMETA(DisplayName = "Force Visible"),
ForceHidden UMETA(DisplayName = "Force Hidden"),
Custom UMETA(DisplayName = "Custom"),
NbMode UMETA(Hidden)
};
// Structure that defines a door.
// A door is defined by its position, its direction, and its type.
USTRUCT(BlueprintType)
struct PROCEDURALDUNGEON_API FDoorDef
{
GENERATED_BODY()
public:
static const FDoorDef Invalid;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef")
FIntVector Position {FIntVector::ZeroValue};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef")
EDoorDirection Direction {EDoorDirection::North};
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DoorDef", meta = (DisplayThumbnail = false))
class UDoorType* Type {nullptr};
public:
FDoorDef() = default;
FDoorDef(const FIntVector& InPosition, EDoorDirection InDirection, class UDoorType* InType = nullptr);
bool IsValid() const;
operator bool() const { return IsValid(); }
bool operator==(const FDoorDef& Other) const;
static bool AreCompatible(const FDoorDef& A, const FDoorDef& B);
FVector GetDoorSize() const;
float GetDoorOffset() const;
FColor GetDoorColor() const;
FString GetTypeName() const;
FString ToString() const;
FDoorDef GetOpposite() const;
FBoxCenterAndExtent GetBounds(const FVector RoomUnit, bool bIncludeOffset = true) const;
static FVector GetRealDoorPosition(const FDoorDef& DoorDef, const FVector RoomUnit, bool bIncludeOffset = true);
static FVector GetRealDoorPosition(FIntVector DoorCell, EDoorDirection DoorRot, const FVector RoomUnit, float DoorOffset = 0.0f);
static FQuat GetRealDoorRotation(const FDoorDef& DoorDef, bool bFlipped = false);
static FDoorDef Transform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation);
static FDoorDef InverseTransform(const FDoorDef& DoorDef, FIntVector Translation, EDoorDirection Rotation);
#if !UE_BUILD_SHIPPING
static void DrawDebug(const class UWorld* World, const FDoorDef& DoorDef, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity, bool bIncludeOffset = false, bool bIsConnected = true);
static void DrawDebug(const class UWorld* World, const FColor& Color, const FVector& DoorSize, const FVector RoomUnit, const FIntVector& DoorCell = FIntVector::ZeroValue, const EDoorDirection& DoorRot = EDoorDirection::NbDirection, const FTransform& Transform = FTransform::Identity, float DoorOffset = 0.0f, bool bIsConnected = true);
#endif // !UE_BUILD_SHIPPING
};
// TODO: Use UE built-in TBox<FIntVector> instead?
// The downside of doing that would be the Center and Extent computation that is slightly different...
// Also, the IsInside with another box does not consider coincident faces as inside...
// Also, operators + and += don't mean the same (extending box to include a point instead of shifting the box)...
USTRUCT(BlueprintType)
struct PROCEDURALDUNGEON_API FBoxMinAndMax
{
GENERATED_BODY();
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Box")
FIntVector Min {0};
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Box")
FIntVector Max {1};
public:
FBoxMinAndMax() = default;
FBoxMinAndMax(const FIntVector& A, const FIntVector& B);
void SetMinAndMax(const FIntVector& A, const FIntVector& B);
FIntVector GetMin() const { return Min; }
FIntVector GetMax() const { return Max; }
bool IsValid() const;
FIntVector GetSize() const;
FBoxCenterAndExtent ToCenterAndExtent() const;
bool IsInside(const FIntVector& Cell) const;
bool IsInside(const FBoxMinAndMax& Other) const;
void Rotate(const EDoorDirection& Rot);
void Extend(const FBoxMinAndMax& Other);
FString ToString() const;
FIntVector GetClosestPoint(const FIntVector& Point) const;
static bool Overlap(const FBoxMinAndMax& A, const FBoxMinAndMax& B);
FBoxMinAndMax& operator+=(const FIntVector& X);
FBoxMinAndMax& operator-=(const FIntVector& X);
FBoxMinAndMax operator+(const FIntVector& X) const;
FBoxMinAndMax operator-(const FIntVector& X) const;
bool operator==(const FBoxMinAndMax& Other) const;
bool operator!=(const FBoxMinAndMax& Other) const;
public:
static const FBoxMinAndMax Invalid;
};
FBoxMinAndMax PROCEDURALDUNGEON_API Rotate(const FBoxMinAndMax& Box, const EDoorDirection& Rot);
// Describe a potential room to be added to the dungeon.
// Mainly used by FilterAndSortRooms function.
USTRUCT(BlueprintType)
struct PROCEDURALDUNGEON_API FRoomCandidate
{
GENERATED_BODY();
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
class URoomData* Data {nullptr};
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
int32 DoorIndex {-1};
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Room Candidate")
int32 Score {-1};
public:
static FRoomCandidate Invalid;
};
USTRUCT()
struct FDoorState
{
GENERATED_BODY()
public:
UPROPERTY(SaveGame)
bool bIsLocked {false};
UPROPERTY(SaveGame)
bool bIsOpen {false};
};
@@ -0,0 +1,171 @@
// Copyright Benoit Pelletier 2023 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "Math/Vector.h"
#include "Math/IntVector.h"
#include "Engine/EngineTypes.h"
#include "EngineUtils.h"
#include "Utils/CompatUtils.h"
namespace IntVector
{
// Returns the component-wise minimum of A and B
FIntVector PROCEDURALDUNGEON_API Min(const FIntVector& A, const FIntVector& B);
// Returns the component-wise maximum of A and B
FIntVector PROCEDURALDUNGEON_API Max(const FIntVector& A, const FIntVector& B);
// Returns the component-wise minimum and maximum of A and B
void PROCEDURALDUNGEON_API MinMax(const FIntVector& A, const FIntVector& B, FIntVector& OutMin, FIntVector& OutMax);
} //namespace IntVector
class FBoxCenterAndExtent;
struct FBoxMinAndMax;
namespace Dungeon
{
// Returns the real world location of a point in room units
FVector PROCEDURALDUNGEON_API ToWorldLocation(FIntVector RoomPoint, const FVector RoomUnit);
// Returns the real world vector (no offset) of a vector in room units
FVector PROCEDURALDUNGEON_API ToWorldVector(FIntVector RoomVector, const FVector RoomUnit);
// Convertthe Box from dungeon coordinate to world coordinate, applying an optional transform on it.
FBoxCenterAndExtent PROCEDURALDUNGEON_API ToWorld(const FBoxMinAndMax& Box, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity);
// Convertthe Box from dungeon coordinate to world coordinate, applying an optional transform on it.
FBoxCenterAndExtent PROCEDURALDUNGEON_API ToWorld(const FBoxCenterAndExtent& Box, const FVector RoomUnit, const FTransform& Transform = FTransform::Identity);
// Returns the location in room units from a point in real world
FIntVector PROCEDURALDUNGEON_API ToRoomLocation(FVector WorldPoint, const FVector RoomUnit);
// Returns the vector (no offset) in room units from a vector in real world
FIntVector PROCEDURALDUNGEON_API ToRoomVector(FVector WorldVector, const FVector RoomUnit);
// Returns the real world snapped location to the nearest point in room units from a real world point
FVector PROCEDURALDUNGEON_API SnapPoint(FVector Point, const FVector RoomUnit);
template<typename T>
int GetTotalWeight(const TMap<T, int>& WeightMap)
{
int Total = 0;
for (const auto& Pair : WeightMap)
{
Total += Pair.Value;
}
return Total;
}
template<typename T>
T GetWeightedAt(const TMap<T, int>& WeightMap, int Index)
{
if (Index < 0)
return T();
int Current = 0;
for (const auto& Pair : WeightMap)
{
Current += Pair.Value;
if (Current > Index)
return Pair.Key;
}
return T();
}
// ===== Plugin's Settings =====
FVector PROCEDURALDUNGEON_API RoomUnit();
FVector PROCEDURALDUNGEON_API DefaultDoorSize();
FColor PROCEDURALDUNGEON_API DefaultDoorColor();
float PROCEDURALDUNGEON_API DoorOffset();
bool PROCEDURALDUNGEON_API OcclusionCulling();
bool PROCEDURALDUNGEON_API UseLegacyOcclusion();
uint32 PROCEDURALDUNGEON_API OcclusionDistance();
bool PROCEDURALDUNGEON_API OccludeDynamicActors();
bool PROCEDURALDUNGEON_API DrawDebug();
bool PROCEDURALDUNGEON_API DrawOnlyWhenEditingRoom();
bool PROCEDURALDUNGEON_API ShowRoomOrigin();
bool PROCEDURALDUNGEON_API FlipDoorArrow();
float PROCEDURALDUNGEON_API DoorArrowLength();
float PROCEDURALDUNGEON_API DoorArrowHeadSize();
bool PROCEDURALDUNGEON_API CanLoop();
ECollisionChannel PROCEDURALDUNGEON_API RoomObjectType();
uint32 PROCEDURALDUNGEON_API MaxGenerationTryBeforeGivingUp();
uint32 PROCEDURALDUNGEON_API MaxRoomPlacementTryBeforeGivingUp();
int32 PROCEDURALDUNGEON_API RoomLimit();
void PROCEDURALDUNGEON_API EnableOcclusionCulling(bool Enable);
void PROCEDURALDUNGEON_API SetOcclusionDistance(int32 Distance);
} //namespace Dungeon
namespace Random
{
uint32 PROCEDURALDUNGEON_API Guid2Seed(FGuid Guid, int64 Salt);
}
namespace WorldUtils
{
template<class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
void FindAllActors(UWorld* InWorld, TArray<T*>& OutActors)
{
OutActors.Empty();
for (TActorIterator<T> It(InWorld); It; ++It)
{
T* Actor = *It;
OutActors.Add(Actor);
}
}
template<class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
void FindAllActorsByPredicate(UWorld* InWorld, TArray<T*>& OutActors, TFunction<bool(const T*)> Predicate)
{
OutActors.Empty();
for (TActorIterator<T> It(InWorld); It; ++It)
{
T* Actor = *It;
if (Predicate(Actor))
{
OutActors.Add(Actor);
}
}
}
template<typename U, class T UE_REQUIRES(TIsDerivedFrom<T, AActor>::Value)>
void MapActors(UWorld* InWorld, TMap<U, T*>& OutActorMap, TFunction<U(const T*)> MapFunction)
{
OutActorMap.Empty();
for (TActorIterator<T> It(InWorld); It; ++It)
{
T* Actor = *It;
OutActorMap.Add(MapFunction(Actor), Actor);
}
}
} //namespace WorldUtils
namespace ObjectUtils
{
void PROCEDURALDUNGEON_API DispatchToObjectAndSubobjects(UObject* Obj, TFunction<void(UObject*)> Func, int32 Depth = 0);
}
namespace ActorUtils
{
// Returns the bounding box of an actor considering only components that would interact with rooms (based on collision settings).
FBox PROCEDURALDUNGEON_API GetActorBoundingBoxForRooms(AActor* Actor, const FTransform& DungeonTransform = FTransform::Identity);
// Returns the player controller associated with the player state id.
class APlayerController* GetPlayerControllerFromPlayerId(const UObject* WorldContextObject, int32 PlayerId);
UObject* GetInterfaceImplementer(AActor* Actor, TSubclassOf<UInterface> InterfaceClass);
template<typename T UE_REQUIRES(TIsDerivedFrom<T, UInterface>::Value)>
UObject* GetInterfaceImplementer(AActor* Actor)
{
return GetInterfaceImplementer(Actor, T::StaticClass());
}
}
@@ -0,0 +1,82 @@
// Copyright Benoit Pelletier 2021 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "Containers/Queue.h"
#include "Containers/Array.h"
template<typename T>
class TQueueOrStack
{
public:
enum class EMode { QUEUE, STACK };
TQueueOrStack(EMode _Mode = EMode::QUEUE)
: Mode(_Mode), Queue(), Stack()
{
}
// Sets the mode (queue or stack) and clears its content.
void SetMode(EMode _Mode)
{
Empty();
Mode = _Mode;
}
void Push(T& Element)
{
switch (Mode)
{
case EMode::QUEUE:
Queue.Enqueue(Element);
break;
case EMode::STACK:
Stack.Push(Element);
break;
}
}
T Pop()
{
check(!IsEmpty());
T item = T();
switch (Mode)
{
case EMode::QUEUE:
Queue.Dequeue(item);
break;
case EMode::STACK:
item = Stack.Pop();
break;
}
return item;
}
bool IsEmpty()
{
switch (Mode)
{
case EMode::QUEUE:
return Queue.IsEmpty();
case EMode::STACK:
return Stack.Num() <= 0;
}
return true;
}
void Empty()
{
Queue.Empty();
Stack.Empty();
}
private:
EMode Mode;
TQueue<T> Queue;
TArray<T> Stack;
};
@@ -0,0 +1,60 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "ProceduralDungeonTypes.h"
#include "ReadOnlyRoom.generated.h"
class URoomData;
// This class does not need to be modified.
UINTERFACE(MinimalAPI, BlueprintType, NotBlueprintable, meta = (CannotImplementInterfaceInBlueprint, Documentable, Tooltip = "Allow access to only some members of Room instances during the generation process."))
class UReadOnlyRoom : public UInterface
{
GENERATED_BODY()
};
// Interface to access some room instance's data during the generation process.
class PROCEDURALDUNGEON_API IReadOnlyRoom
{
GENERATED_BODY()
public:
// Returns the room data asset of this room instance.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual const URoomData* GetRoomData() const { return nullptr; }
// Returns the unique ID (per-dungeon) of the room.
// The first room has ID 0 and then it increases in the order of placed room.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual int64 GetRoomID() const { return -1ll; }
// Returns the position of the room (in Room Units).
UFUNCTION(BlueprintCallable, Category = "Room")
virtual FIntVector GetPosition() const { return FIntVector::ZeroValue; }
// Returns the direction of the room.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual EDoorDirection GetDirection() const { return EDoorDirection::North; }
// Returns true if all the doors of this room are connected to other rooms.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual bool AreAllDoorsConnected() const { return false; }
// Returns the number of doors in this room connected to another room.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual int CountConnectedDoors() const { return -1; }
// Returns the world center position of the room.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual FVector GetBoundsCenter() const { return FVector::ZeroVector; }
// Returns the world extents (half size) of the room.
UFUNCTION(BlueprintCallable, Category = "Room")
virtual FVector GetBoundsExtent() const { return FVector::ZeroVector; }
};

Some files were not shown because too many files have changed in this diff Show More