These files compile together (same module folder).
golf_manager.verse
using. /Fortnite.com/Characters
using. /Fortnite.com/Devices
using. /UnrealEngine.com/Temporary/SpatialMath
using. /Verse.org/Simulation
using. PhysicsUtils
using. PhysicsUtils.GolfUtils
#Class representing a player's golf ball and associated devices
player_ball := class:
@editable
BallProp : creative_prop = creative_prop{}
@editable
OrbitCamera : gameplay_camera_orbit_device = gameplay_camera_orbit_device{}
@editable
PowerCinematic : cinematic_sequence_device = cinematic_sequence_device{}
@editable
LauncherStages : []launcher_stage = array{}
var StartPosition : vector3 = vector3{}
#Wrapper class for each launcher stage
launcher_stage := class:
@editable
LauncherProp : creative_prop = creative_prop{}
#Float that represents the maximum time needed while holding the shoot input to use this stage
@editable
TimeNeeded : float = 0.0
#Class representing golf settings used across the map
golf_settings := class:
@editable
RespawnDelay : float = 2.0
@editable
StartNextCourseDelay : float = 1.5
#Verse device that manages the overall golf game logic
golf_manager := class(creative_device):
#Reference to the golf settings class
@editable
GolfSettings : golf_settings = golf_settings{}
#Array of player balls
#IMPORTANT: For each player there has to be a player_ball instance inside the array in the Verse Device
@editable
BallPerPlayer : []player_ball = array{}
#Array of golf courses in the map
@editable
GolfCourses : []golf_course = array{}
#Array of volume devices that represent out of bounds areas
@editable
OutOfBoundsVolumes : []volume_device = array{}
#Input trigger device to handle shooting input
@editable
ShootInput : input_trigger_device = input_trigger_device{}
#Function called when the device begins play
#Used to Initialize device logic and subscribe to input and volume events
OnBegin<override>()<suspends>:void=
ShootInput.PressedEvent.Subscribe(OnShootPressed)
ShootInput.ReleasedEvent.Subscribe(OnShootReleased)
for(OutOfBoundsVolume : OutOfBoundsVolumes):
OutOfBoundsVolume.PropEnterEvent.Subscribe(OnOutOfBounds)
for(PlayerBall : BallPerPlayer):
set PlayerBall.StartPosition = PlayerBall.BallProp.GetTransform().Translation
InitGolfCourses()
#Sleep 0.1 seconds to ensure the GlobalEventsMap and CustomPlayers weak maps are set
Sleep(0.1)
spawn. AwaitStartNextCourse()
InitGolfCourses(): void=
for(I -> Course : GolfCourses):
set Course.CourseID = I
set Course.GolfSettings = Self.GolfSettings
Course.HoleVolume.PropEnterEvent.Subscribe(Course.OnHoleEnter)
#Function to initialize a player when they join the game
InitPlayer(CustomPlayer : custom_player)<suspends>: void=
if(BallPlayer := BallPerPlayer[CustomPlayer.PlayerID]):
BallPlayer.OrbitCamera.AddTo(CustomPlayer.Player)
set CustomPlayer.CurrentCourseID = 0
set CustomPlayer.BallPlayer = BallPlayer
StartCourse(CustomPlayer.Player)
#Sleep to make sure the GlobalEventsMap weak map is initialized
Sleep(0.15)
#Signals Events in the event_wrapper class to notify other devices
if(GlobalEvents := GlobalEventsMap[GetSession()]):
GlobalEvents.AgentInitializedEvent.Signal(CustomPlayer.Player)
#Function to await the start next course event for players
AwaitStartNextCourse()<suspends>: void=
if(GlobalEvents := GlobalEventsMap[GetSession()]):
loop:
Agent := GlobalEvents.StartNextCourseEvent.Await()
StartCourse(Agent)
#Function to start the course for the player by teleporting their ball to the start position
StartCourse(Agent : agent): void=
if:
CustomPlayer := CustomPlayersMap[GetSession()][Agent]
BallPlayer := CustomPlayer.BallPlayer
CurrentCourse := GolfCourses[CustomPlayer.CurrentCourseID]
StartPositionProp := CurrentCourse.StartPositionProps[CustomPlayer.PlayerID]
BallProp := BallPlayer.BallProp
then:
StartPos := StartPositionProp.GetTransform().Translation
if(BallProp.TeleportTo[StartPos, BallProp.GetTransform().Rotation]):
set CustomPlayer.PlayerState = player_state.idle
#Function called when a ball enters an out of bounds volume
OnOutOfBounds(Prop : creative_prop): void=
#Get the owner of the prop that entered the out of bounds volume
if(BallOwner := Prop.FindPropOwner[], BallOwner.PlayerState = player_state.idle):
#Reset the ball
set BallOwner.PlayerState = player_state.respawning
spawn. ResetBall(BallOwner.Player)
#Function to reset the ball for the player after a delay
ResetBall(Agent : agent)<suspends>: void=
#Get the custom player and ball player instances
if(CustomPlayer := CustomPlayersMap[GetSession()][Agent]):
Sleep(GolfSettings.RespawnDelay)
AwaitPropStationary(CustomPlayer.BallPlayer.BallProp)
StartCourse(Agent)
#Function called when the shoot input is pressed
OnShootPressed(Agent : agent): void=
spawn. ValidatePress(Agent)
#Function to validate if the player can start the power cinematic
ValidatePress(Agent : agent)<suspends>: void=
if(CustomPlayer := CustomPlayersMap[GetSession()][Agent]):
IsStationary := IsPropStationary(CustomPlayer.BallPlayer.BallProp)
if(IsStationary = true):
StartPower(Agent)
#Function to start the power cinematic for the player
StartPower(Agent : agent)<suspends>: void=
if:
CustomPlayer := CustomPlayersMap[GetSession()][Agent],
BallPlayer := BallPerPlayer[CustomPlayer.PlayerID]
CustomPlayer.PlayerState = player_state.idle
then:
BallPlayer.PowerCinematic.Play(Agent)
set CustomPlayer.PlayerState = player_state.shooting
#Function called when the shoot input is released
OnShootReleased(Agent : agent, TimePressed : float): void=
spawn. ValidateRelease(Agent, TimePressed)
#Function to validate if the player can shoot the ball
ValidateRelease(Agent : agent, TimePressed : float)<suspends>: void=
if(CustomPlayer := CustomPlayersMap[GetSession()][Agent]):
IsStationary := IsPropStationary(CustomPlayer.BallPlayer.BallProp)
if(IsStationary = true):
ShootBall(Agent, TimePressed)
#Function to shoot the ball based on the time the shoot input was held
ShootBall(Agent : agent, TimePressed : float)<suspends>: void=
PausePowerCinematic(Agent)
if:
CustomPlayer := CustomPlayersMap[GetSession()][Agent]
CustomPlayer.PlayerState = player_state.shooting
BallProp := CustomPlayer.BallPlayer.BallProp
UsedLauncherStage := CustomPlayer.GetLauncherStage(TimePressed)
FortCharRotation := Agent.GetFortCharacter[].GetTransform().Rotation
PrevTransform := UsedLauncherStage.LauncherProp.GetTransform()
GlobalEvents := GlobalEventsMap[GetSession()]
UsedLauncherStage.LauncherProp.TeleportTo[BallProp.GetTransform().Translation, FortCharRotation]
then:
GlobalEvents.ShootBallEvent.Signal(Agent)
#Slight Delay before teleporting Launcher back to make sure it affected the ball physics
Sleep(0.1)
if(UsedLauncherStage.LauncherProp.TeleportTo[PrevTransform]):
StopPowerCinematic(Agent)
#Function to pause the power cinematic for the player
PausePowerCinematic(Agent : agent): void=
if:
CustomPlayer := CustomPlayersMap[GetSession()][Agent]
BallPlayer := CustomPlayer.BallPlayer
then:
BallPlayer.PowerCinematic.Pause(Agent)
#Function to stop the power cinematic for the player
StopPowerCinematic(Agent : agent): void=
if:
CustomPlayer := CustomPlayersMap[GetSession()][Agent]
BallPlayer := CustomPlayer.BallPlayer
then:
BallPlayer.PowerCinematic.Play(Agent)
BallPlayer.PowerCinematic.SetPlaybackFrame(0)
BallPlayer.PowerCinematic.Stop(Agent)
set CustomPlayer.PlayerState = player_state.idle
golf_course.verse
using. /Fortnite.com/Devices
using. /Verse.org/Simulation
using. PhysicsUtils
using. PhysicsUtils.GolfUtils
#Verse device that represents a golf course
golf_course := class(creative_device):
#Props to represent the starting positions for each player
#Create a empty BP prop and place it in the desired start location for each player
@editable
StartPositionProps : []creative_prop = array{}
#Volume device that represents the hole area
#Used to detect when the ball enters the hole
@editable
HoleVolume : volume_device = volume_device{}
#The ID of this course in the golf manager's course array
#First ID starts at 0, increments by 1 for each course in the array
var CourseID : int = -1
#Reference to the golf settings class
var GolfSettings : golf_settings = golf_settings{}
#Function called when a ball enters the hole volume
OnHoleEnter(Prop : creative_prop): void=
#Get the owner of the prop that entered the hole
if(OwnerPlayer := Prop.FindPropOwner[]):
#Check if the course ID matches the player's current course to prevent triggering when on a different course
if(OwnerPlayer.CurrentCourseID = CourseID):
#Signals Events in the event_wrapper class to notify other devices
if(GlobalEvents := GlobalEventsMap[GetSession()]):
GlobalEvents.BallIntoHoleEvent.Signal(OwnerPlayer.Player)
GlobalEvents.CompletedCourseEvent.Signal(OwnerPlayer.Player)
spawn. StartNextCourse(OwnerPlayer)
#Function to start the next course for the player after a delay
StartNextCourse(CustomPlayer : custom_player)<suspends>: void=
Sleep(GolfSettings.StartNextCourseDelay)
AwaitPropStationary(CustomPlayer.BallPlayer.BallProp)
set CustomPlayer.CurrentCourseID += 1
#Signals Events in the event_wrapper class to notify other devices
if(GlobalEvents := GlobalEventsMap[GetSession()]):
GlobalEvents.StartNextCourseEvent.Signal(CustomPlayer.Player)
player_manager.verse
using. /Fortnite.com/Characters
using. /Fortnite.com/Devices
using. /Verse.org/Simulation
using. /Verse.org/Colors
# Removes an element from the given map and returns a new map without that element
RemoveKeyFromMap(CustomPlayerMap:[agent]custom_player, ElementToRemove:custom_player):[agent]custom_player=
var NewMap:[agent]custom_player = map{}
# Concatenate Keys from CustomPlayerMap into NewMap, excluding ElementToRemove
for (Key -> Value : CustomPlayerMap, Value <> ElementToRemove):
set NewMap = ConcatenateMaps(NewMap, map{Key => Value})
return NewMap
#Wrapper class for player IDs to track if they are in use
player_id_wrapper := class:
ID : int = -1
var IsUsed : logic = false
#Verse device that manages players joining the game and their custom player data
player_manager := class(creative_device):
#Connection to the golf manager to initialize players upon joining
@editable
GolfManager : golf_manager = golf_manager{}
#Team settings device to safely track player joins by using the TeamMemberSpawnedEvent
#IMPORTANT: Set the team to ALL in the device settings
@editable
TeamSettingsDevice<private> : team_settings_and_inventory_device = team_settings_and_inventory_device{}
#Max Amount of players that will be able to play the map
#IMPORTANT: Make sure this matches the max player number inside the Island Settings
@editable
PlayerAmount : int = 4
#Array of player ID wrappers to assign unique IDs to players
#Match the ID with the array elelemt index
#e.g. PlayerIDs[0].ID has to be 0, PlayerIDs[1].ID has to be 1, etc.
var PlayerIDs<private> : []player_id_wrapper = array{}
#Function called when the device begins play
#Used to initialize device
OnBegin<override>()<suspends>:void=
InitPlayerIDs()
if(set CustomPlayersMap[GetSession()] = map{}):
if(set GlobalEventsMap[GetSession()] = event_wrapper{}):
TeamSettingsDevice.TeamMemberSpawnedEvent.Subscribe(OnPlayerJoined)
GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerLeft)
#Function to initialize the PlayerIDs array based on the PlayerAmount
InitPlayerIDs()<suspends>: void=
for(I := 0..PlayerAmount - 1):
set PlayerIDs += array{player_id_wrapper{ID := I, IsUsed := false}}
#Function called when a player spawn in the map
OnPlayerJoined(Agent : agent): void=
#Check to see if the player is already initialized, return if they are
if(CustomPlayersMap[GetSession()][Agent]):
return
if:
Player := player[Agent]
FortChar := Agent.GetFortCharacter[]
PlayerID := GetPlayerID()
CustomPlayer := custom_player{Player := Player, FortChar := FortChar, PlayerID := PlayerID}
set CustomPlayersMap[GetSession()][Agent] = CustomPlayer
then:
spawn. GolfManager.InitPlayer(CustomPlayer)
#Function called when a player leaves the map
OnPlayerLeft(Agent : agent): void=
if:
CustomPlayerMap := CustomPlayersMap[GetSession()]
CustomPlayer := CustomPlayerMap[Agent]
BallPlayer := CustomPlayer.BallPlayer
then:
ReleasePlayerID(CustomPlayer.PlayerID)
if(BallPlayer.BallProp.TeleportTo[BallPlayer.StartPosition, BallPlayer.BallProp.GetTransform().Rotation]):
BallPlayer.PowerCinematic.Stop(CustomPlayer.Player)
#Clean up the custom player map to ensure no performance issues after multiple joins/leaves
NewMap := RemoveKeyFromMap(CustomPlayerMap, CustomPlayer)
if(set CustomPlayersMap[GetSession()] = NewMap):
#Function to release a player ID when a player leaves to make it available for future players
ReleasePlayerID(PlayerID : int): void=
loop:
for(IDWrapper : PlayerIDs):
if(IDWrapper.ID = PlayerID):
set IDWrapper.IsUsed = false
break
break
#Function to get an unused player ID from the PlayerIDs array
GetPlayerID()<transacts>: int=
var FinalPlayerID : player_id_wrapper = player_id_wrapper{}
loop:
for(I -> IDWrapper : PlayerIDs):
if(IDWrapper.IsUsed = false):
set FinalPlayerID = IDWrapper
set IDWrapper.IsUsed = true
break
break
if(FinalPlayerID.ID = -1):
Print("No valid player IDs available! Please make sure the player_managers PlayerAmount matches the max player amount.", ?Duration := 7.0,?Color := NamedColors.Red)
return FinalPlayerID.ID
event_wrapper.verse
using. /Verse.org/Simulation
#Weak map used to Signal and register events from the event_wrapper class
var GlobalEventsMap : weak_map(session, event_wrapper) = map{}
#Class wrapper for events
event_wrapper := class:
#Called when the player shoots the ball
ShootBallEvent : event(agent) = event(agent){}
#Called when the player's ball goes into the hole of a golf course
BallIntoHoleEvent : event(agent) = event(agent){}
#Called when the player completes a golf course i.e. ball goes into the hole of a golf course
CompletedCourseEvent : event(agent) = event(agent){}
#Called when a player is initialized and ready
AgentInitializedEvent : event(agent) = event(agent){}
#Signal to start the next golf course for the player
StartNextCourseEvent : event(agent) = event(agent){}
custom_player.verse
using. /Fortnite.com/Characters
using. /Verse.org/Simulation
#Weak map to store custom player data for each session
var CustomPlayersMap : weak_map(session, [agent]custom_player) = map{}
#Enum to track the current state of the player
player_state := enum:
idle
respawning
shooting
#Class representing a custom player with associated data
custom_player := class<unique>:
Player : player
FortChar : fort_character
#ID that the player is assigned when joining the game
PlayerID : int = -1
#The player's golf ball class
var BallPlayer : player_ball = player_ball{}
#The current state of the player
var PlayerState : player_state = player_state.idle
#The Index of the current golf course the player is on
var CurrentCourseID : int = -1
audio_manager.verse
using. /Fortnite.com/Devices
using. /Verse.org/Simulation
#Verse device that manages audio for the map
audio_manager := class(creative_device):
#Sound that is played when the player shoots the ball
@editable
ShootBallSound<private> : audio_player_device = audio_player_device{}
#Sound that is played when the player's ball goes into the hole
@editable
BallIntoHoleSound<private> : audio_player_device = audio_player_device{}
#Sound that is played when the player completes a golf course
@editable
SuccessSound<private> : audio_player_device = audio_player_device{}
#Function called when the device begins play
#Spawns all the await functions for the various audio events
OnBegin<override>()<suspends>:void=
#Sleep 0.1 seconds to ensure the GlobalEventsMap weak map is set
Sleep(0.1)
spawn. AwaitShootingBall()
spawn. AwaitBallIntoHole()
spawn. AwaitCompletedCourse()
#Function to await ShootBallEvent and play the associated sound
AwaitShootingBall()<suspends>: void=
if(GlobalEvents := GlobalEventsMap[GetSession()]):
loop:
Agent := GlobalEvents.ShootBallEvent.Await()
ShootBallSound.Play(Agent)
#Function to await BallIntoHoleEvent and play the associated sound
AwaitBallIntoHole()<suspends>: void=
if(GlobalEvents := GlobalEventsMap[GetSession()]):
loop:
Agent := GlobalEvents.BallIntoHoleEvent.Await()
BallIntoHoleSound.Play(Agent)
#Function to await CompletedCourseEvent and play the associated sound
AwaitCompletedCourse()<suspends>: void=
if(GlobalEvents := GlobalEventsMap[GetSession()]):
loop:
Agent := GlobalEvents.CompletedCourseEvent.Await()
SuccessSound.Play(Agent)
PhysicsUtils.verse
using. /UnrealEngine.com/Temporary/SpatialMath
using. /Fortnite.com/Devices
using. /Verse.org/Simulation
using. /Fortnite.com/Game
# Utility functions for physics calculations
PhysicsUtils := module:
# Module for golf-related physics utilities
GolfUtils<public> := module:
#Utility function to get the launcher stage based on the time the shoot input was held
(CustomPlayer : custom_player).GetLauncherStage<public>(Time : float)<transacts>: launcher_stage=
var FinalLauncherStage : launcher_stage = launcher_stage{}
loop:
for(I -> Stage : CustomPlayer.BallPlayer.LauncherStages):
if(NextStage := CustomPlayer.BallPlayer.LauncherStages[I + 1]):
if(Time >= Stage.TimeNeeded and Time < NextStage.TimeNeeded):
set FinalLauncherStage = Stage
break
else:
if(Time >= Stage.TimeNeeded):
set FinalLauncherStage = Stage
break
break
return FinalLauncherStage
#Utility function to get the owner of a prop by checking all custom players' ball props
(Prop : creative_prop).FindPropOwner<public>()<transacts><decides>: custom_player=
var MaybeOwner : ?custom_player = false
if(AllCustomPlayers := CustomPlayersMap[GetSession()]):
loop:
for(Key -> CustomPlayer : AllCustomPlayers):
if(IsSameProp[Prop, CustomPlayer.BallPlayer.BallProp]):
set MaybeOwner = option{CustomPlayer}
break
break
MaybeOwner?
#Velocity threshold to consider a prop as stationary
NoMoveVelocity : float = 15.0
#Calculate the velocity of a the positional interface over a time
(Positional:positional).GetVelocity<public>(WaitTime:float)<suspends>: float=
StartLocation := Positional.GetTransform().Translation
StartTime := GetSimulationElapsedTime()
Sleep(WaitTime)
EndLocation := Positional.GetTransform().Translation
EndTime := GetSimulationElapsedTime()
DistanceMoved := Distance(StartLocation, EndLocation)
TimeElapsed := EndTime - StartTime
Velocity := DistanceMoved / TimeElapsed
return Velocity
#Checks if two props are the same by comparing their locations
#Currently creative_prop are not <unique>, this means we cannot directly compare them inside a if statement
IsSameProp<public>(Prop1 : creative_prop, Prop2 : creative_prop)<transacts><decides>: void=
Prop1.GetTransform().Translation = Prop2.GetTransform().Translation
#Check if a prop is stationary based on its velocity
#Props are considered stationary if their velocity is below NoMoveVelocity
#This function completes after 0.1 seconds to get an accurate velocity reading
IsPropStationary<public>(Prop : creative_prop)<suspends>: logic=
var IsStationary : logic = false
Velocity := Prop.GetVelocity(0.1)
if(Velocity < NoMoveVelocity):
set IsStationary = true
return IsStationary
#Awaits until the prop is stationary
#Props are considered stationary if their velocity is below NoMoveVelocity
AwaitPropStationary<public>(Prop : creative_prop)<suspends>: void=
loop:
IsStationary := IsPropStationary(Prop)
if(IsStationary = true):
break
ui_manager.verse
using. /Fortnite.com/Devices
using. /Verse.org/Simulation
#Verse device that manages UI elements for the map
ui_manager := class(creative_device):
#Hud message device to show a power meter UI element
@editable
PowerHud<private> : hud_message_device = hud_message_device{}
OnBegin<override>()<suspends>:void=
#Sleep 0.1 seconds to ensure the GlobalEventsMap weak map is set
Sleep(0.1)
spawn. AwaitInitialized()
#Function to await player initialization events and then call the Init function
AwaitInitialized()<suspends>: void=
if(GlobalEvents := GlobalEventsMap[GetSession()]):
loop:
InitializedAgent := GlobalEvents.AgentInitializedEvent.Await()
Init(InitializedAgent)
#Function to initialize UI for the player
Init(Agent : agent): void=
PowerHud.Show(Agent)