Random Room Spawning using Scene Graph
Module — 3 files
These files compile together (same module folder).
dungeon_room.verse
using { /Fortnite.com/Devices }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
# Defines a room in the dungeon.
dungeon_room := class<abstract>:
# Whether or not the room contains enemies.
@editable
ContainsEnemies:logic = false
# The physical bounds of the room. Defaults to double the fortnite tile size of 512cm x 384cm x 512cm to represent a 2x2 tile room.
@editable_vector_number(float):
ToolTip := RoomBoundsTip
RoomBounds:vector3 = vector3{Left := 512.0, Up := 256.0, Forward := 512.0}
# What enemies should spawn in the room.
@editable
EnemySpawners:[]guard_spawner_device = array{}
# Constructs the entity prefab associated with this dungeon room.
ConstructRoomPrefab()<transacts>:?entity = false
<#
simple_dungeon_room := class<concrete>(dungeon_room):
# Constructs the entity prefab associated with this dungeon room.
ConstructRoomPrefab<override>()<transacts>:?entity=
NewRoom := DungeonCrawler.Prefabs.Prefab_FortSizeRoom{}
option{NewRoom}
#>
file_2.verse
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
# A Verse-authored component that can be added to entities
room_spawn_component<public> := class<final_super>(component):
# Runs when the component should start simulating in a running game.
# Can be suspended throughout the lifetime of the component. Suspensions
# will be automatically cancelled when the component is disposed or the
# game ends.
OnSimulate<override>()<suspends>:void =
if:
InteractableComponent := Entity.GetComponent[interactable_component]
then:
InteractableComponent.SucceededEvent.Subscribe(OnRoomSpawnSignalled)
# Signal the dungeon manager to spawn a new room.
OnRoomSpawnSignalled(Agent:agent):void=
if:
DungeonManager := GlobalDungeon[GetSession()]
# Print("room_spawn_component: Got the global dungeon manager")
InteractableComponent := Entity.GetComponent[interactable_component]
MeshComponent := Entity.GetComponent[mesh_component]
then:
InteractableComponent.Disable()
set MeshComponent.Visible = false
DungeonManager.RoomSpawnEvent.Signal(Entity)
# Represents the entrace entity of a room. Acts as a primitive version of a tag component.
room_entrance_component<public> := class<final_super>(component):
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
# Represents a location an enemy can spawn from. Acts as a primitive version of a tag component.
enemy_spawn_location<public> := class<final_super>(component):
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
my_debug_draw.verse
using { /Fortnite.com/Characters }
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/SpatialMath}
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/Random }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
# Stores a global reference to the dungeon_manager_component.
var GlobalDungeon:weak_map (session, dungeon_manager_component) = map{}
my_debug_draw := class(debug_draw_channel) {}
RoomBoundsTip<localizes>:message = "The physical bounds of the room. Defaults to double the fortnite tile size of 512cm x 384cm x 512cm to represent a 2x2 tile room."
# Set the access specifier of the prefabs module to public.
DungeonCrawler := module:
Prefabs<public> := module:
dungeon_manager_component<public> := class<final_super>(component):
# The debug draw channel used to render collision checks.
DebugDraw:debug_draw = debug_draw{Channel := my_debug_draw}
# Event signalled when a room should be spawned.
RoomSpawnEvent<public>:event(entity) = event(entity){}
# The array of dungeon rooms to spawn
@editable
var RoomsToSpawn:[]dungeon_room = array{}
# Runs when the component should start simulating in a running game.
OnBeginSimulation<override>():void =
# Run OnBeginSimulation from the parent class before
# running this component's OnBeginSimulation logic
(super:)OnBeginSimulation()
# Get and set the global dungeon manager, then spawn a loop to monitor room spawning.
if:
set GlobalDungeon[GetSession()] = Self
then:
# Print("Set the global dungeon manager.")
spawn{SpawnManagerLoop()}
# Monitor for RoomSpawnEvents and attempt to spawn a room when the event is signalled.
SpawnManagerLoop()<suspends>:void=
loop:
# Print("Waiting to spawn the next room...")
RoomParentEntity := RoomSpawnEvent.Await()
# Print("Got the room spawn signal!")
SpawnRoom(RoomParentEntity)
Sleep(0.0)
# Attempt to spawn a room and move it into the correct position.
SpawnRoom(RoomExit:entity)<suspends>:void=
# Print("Attempting to spawn a room...")
if:
SimulationEntity := RoomExit.GetSimulationEntity[]
# Pick a room to spawn from the dungeon rooms array.
RoomIndex := GetRandomInt(0, RoomsToSpawn.Length-1)
ChosenRoom := RoomsToSpawn[RoomIndex]
not WillRoomOverlap[ChosenRoom.RoomBounds, RoomExit]
# Get the entity prefab associated with this dungeon room.
NewRoom := ChosenRoom.ConstructRoomPrefab()?
then:
# Print("Attempting to spawn room index {RoomIndex}")
# Add the new room to the scene.
SimulationEntity.AddEntities(array{NewRoom})
# Position the new room in the scene relative to the exit of the previous room.
spawn{PositionDungeonRoom(NewRoom, RoomExit.GetGlobalTransform())}
# Remove the room exit from its parent to prevent multiple room generation.
RoomExit.RemoveFromParent()
# If the room should have enemies and has spawners assigned, attempt to spawn them.
if(ChosenRoom.ContainsEnemies? and ChosenRoom.EnemySpawners.Length > 0):
# Print("Room should contain enemies")
spawn{SpawnRoomEnemies(NewRoom, ChosenRoom.EnemySpawners)}
# Spawn the enemies for a given room and move them into the correct position.
SpawnRoomEnemies(NewRoom:entity, EnemySpawners:[]guard_spawner_device)<suspends>:void=
if:
SimulationEntity := NewRoom.GetSimulationEntity[]
# Find each entrance associated with the new room. Because a room may have multiple entrances,
PositionEntities := NewRoom.FindDescendantEntitiesWithComponent(enemy_spawn_location)
then:
for:
# For each enemy node, randomly pick a spawner to use to populate that node.
PositionEntity:PositionEntities
SpawnerIndex := GetRandomInt(0, EnemySpawners.Length-1)
ChosenSpawner := EnemySpawners[SpawnerIndex]
MoveCreativeDeviceToEntity[PositionEntity, ChosenSpawner]
do:
# This sleep is used for timing purposes to allow the guard spawner to move into position
# before spawning the guard.
Sleep(0.0)
# Print("Spawning an enemy from SpawnerIndex {SpawnerIndex}")
ChosenSpawner.Spawn()
# --- Alternative Implementation ---
# This waits to get the guard spawned from the spawner, and moves the guard into position instead.
# Make sure to comment out the MoveCreativeDeviceToEntity[] call above if trying this.
# AgentToMove := ChosenSpawner.SpawnedEvent.Await()
# MoveAgentToEntity(AgentToMove, PositionEntity)
# Move the given creative device to the location of the given entity.
MoveCreativeDeviceToEntity(PositionEntity:entity, CreativeDevice:t where t:subtype(creative_device_base))<decides><transacts>:void=
if:
# Create an UnrealEngine transform using the PositionEntity transform.
SpawnTransform := (/UnrealEngine.com/Temporary/SpatialMath:)transform:
Translation := FromVector3(PositionEntity.GetGlobalTransform().Translation)
Rotation := FromRotation(PositionEntity.GetGlobalTransform().Rotation)
# Add a small vertical offset to prevent teleporting inside of the PositionEntity.
VerticalOffset := (/UnrealEngine.com/Temporary/SpatialMath:)vector3{X := 0.0, Y:= 0.0, Z:= 128.0}
# Print("Attempting to teleport to {SpawnTransform.Translation + VerticalOffset}")
CreativeDevice.TeleportTo[SpawnTransform.Translation + VerticalOffset, SpawnTransform.Rotation]
then:
# Print("Teleported Device to location {SpawnTransform.Translation}!")
# Move the given agent to the location of the given entity.
MoveAgentToEntity(AgentToMove:agent, PositionEntity:entity)<suspends>:void=
if:
# Create an UnrealEngine transform using the PositionEntity transform.
EnemyCharacter := AgentToMove.GetFortCharacter[]
SpawnTransform := (/UnrealEngine.com/Temporary/SpatialMath:)transform:
Translation := FromVector3(PositionEntity.GetGlobalTransform().Translation)
Rotation := FromRotation(PositionEntity.GetGlobalTransform().Rotation)
# Add a small vertical offset to prevent teleporting inside of the PositionEntity.
VerticalOffset := (/UnrealEngine.com/Temporary/SpatialMath:)vector3{X := 0.0, Y:= 0.0, Z:= 128.0}
# Print("Attempting to teleport to {SpawnTransform.Translation + VerticalOffset}")
EnemyCharacter.TeleportTo[SpawnTransform.Translation + VerticalOffset, SpawnTransform.Rotation]
then:
# Print("Teleported Enemy to location {SpawnTransform.Translation}!")
# Position the given dungeon room so that the entrance of the new room aligns with the exit of the old room.
# This is done by spawning the room, creating an anchor entity, positioning the anchor at the entrance of the new room,
# parenting the new room to the anchor, then moving the anchor to the exit of the old room.
PositionDungeonRoom(NewRoom:entity, ExitLocation:(/Verse.org/SpatialMath:)transform)<suspends>:void=
# Print("Exit Location is {ExitLocation}")
if:
SimulationEntity := NewRoom.GetSimulationEntity[]
# Find each entrance associated with the new room. Because a room may have multiple entrances,
NewRoomEntrances := NewRoom.FindDescendantEntitiesWithComponent(room_entrance_component)
then:
# Construct an entity to move the new room entrance to the position of the old room exit and add it to the scene.
RoomAnchor := entity{}
TransformComponent := transform_component:
Entity := RoomAnchor
RoomAnchor.AddComponents(array{TransformComponent})
SimulationEntity.AddEntities(array{RoomAnchor})
# Because FindDescendantEntitiesWithComponent() returns a generator, need to iterate through it using a for expression.
# Alternatively, you can convert the generator to an array. An example of this is provided in the WillRoomOverlap function below.
for:
NewRoomEntrance:NewRoomEntrances
do:
# Print("Added room anchor to scene")
EntranceGlobal := NewRoomEntrance.GetGlobalTransform()
RoomAnchor.SetGlobalTransform(EntranceGlobal)
# Print("Moved anchor to {EntranceGlobal}")
# Print("New Room Local Transform is {NewRoom.GetLocalTransform()}")
# Sleep(2.0) - Enable if you want to see the steps the room moves through.
RoomAnchor.AddEntities(array{NewRoom})
# Sleep(2.0) - Enable if you want to see the steps the room moves through.
RoomAnchor.SetGlobalTransform(ExitLocation)
# Check if the dungeon room will overlap any other entities when spawned. Uses FindOverlapHits to find and return an array of
# overlaps, and uses debug draw to visualize the overlap.
WillRoomOverlap(RoomBounds:(/Verse.org/SpatialMath:)vector3, ExitLocation:entity)<decides><transacts>:void=
EntranceTransform := ExitLocation.GetGlobalTransform()
# Create a collision box using the bounds of the new room.
CollisionBox:collision_box = collision_box:
Extents := RoomBounds
# Find the displacement from the previous room to know where to spawn the new room.
EntranceAxis:(/Verse.org/SpatialMath:)vector3 = EntranceTransform.Rotation.GetForwardAxis()
Displacement:(/Verse.org/SpatialMath:)vector3 = EntranceAxis * (RoomBounds + (/Verse.org/SpatialMath:)vector3{Left := 0.0, Up:= 1.0, Forward := 1.0})
# Find the position to simulate the overlap hit from and draw the debug box at.
SimulateTransform := (/Verse.org/SpatialMath:)transform:
Translation := EntranceTransform.Translation + Displacement
Rotation := EntranceTransform.Rotation
Scale := (/Verse.org/SpatialMath:)vector3{Left := 1.0, Up:= 1.0, Forward := 1.0}
DebugDraw.DrawBox(SimulateTransform.Translation, SimulateTransform.Rotation, ?Extent := RoomBounds, ?DrawDurationPolicy := debug_draw_duration_policy.Persistent)
# Find and return each overlap by simulating the CollisionBox at the SimulateTransform.
FoundOverlaps := ExitLocation.FindOverlapHits(SimulateTransform, CollisionBox)
# Convert the generator of overlap hits to an array by setting the array with each
# element returned from the generator.
var OverlapArray:[]overlap_hit = array{}
# Comment this section out if you want to test spawning rooms without worrying about bounds checking.
set OverlapArray = for:
Overlap:FoundOverlaps
do:
Print("Found overlap at {Overlap.TargetComponent.Entity.GetGlobalTransform()}")
Overlap
# Return whether or not the OverlapArray contains any members, indicating an overlap occured and that the room will collide with an entity.
OverlapArray.Length >= 1
Sign in to download module
Copy-paste each file above is always free.