Determining If A Player Is Idle
In this section, you will learn how to check if a player has moved a certain distance since the last simulation update. If they have, the player’s current position is saved and checked again. If they haven’t, the loop breaks and the method completes. This method uses GetFortCharacter[], GetTransform(), and Translation to get the location of the player. You can learn more about these on their API Reference pages.
This page includes Verse snippets that show how to execute gameplay mechanics needed in this gameplay. Follow the steps below and copy the full script on step 6 of this tutorial.
Follow these steps to determine if a player is idle.
Create an extension method for the agent class named
AwaitStopMoving(). This means you’re adding a custom method to an already defined class.Verse(PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent has moved less than the minimum distance.")Get the initial position of the player.
Verse(PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent has moved less than the minimum distance.") # Get the initial position of the agent from the agent's character in the scene. if (Tracked := PropAgent.GetFortCharacter[]): var StartPosition:vector3 = Tracked.GetTransform().TranslationGet the next position of the player in the next simulation update.
Verse(PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent has moved less than the minimum distance.") # Get the initial position of the agent from the agent's character in the scene. if (Tracked := PropAgent.GetFortCharacter[]): var StartPosition:vector3 = Tracked.GetTransform().Translation Sleep(0.0) # Get the position of the agent in the next game tick. NewPosition := Tracked.GetTransform().TranslationCheck if the distance between the initial position and the latest position is within an acceptable threshold, passed to the function as the
MinimumDistanceparameter.(PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent has moved less than the minimum distance.") # Get the initial position of the agent from the agent's character in the scene. if (Tracked := PropAgent.GetFortCharacter[]): var StartPosition:vector3 = Tracked.GetTransform().Translation Sleep(0.0) # Get the position of the agent in the next game tick. NewPosition := Tracked.GetTransform().Translation # If the distance of the new position from the starting position is less than MinimumDistance, the agent has not moved and we break the loop. if (Distance(StartPosition, NewPosition) < MinimumDistance): Logger.Print("Agent has moved less than the minimum distance.") # Otherwise, we reset StartPosition to make sure the player moves from the new position. else: set StartPosition = NewPositionNow we want to loop the check between the initial and latest positions and break out of the loop when the distance between the positions is above the
MinimumDistancethreshold.Verse# Loops until the agent moves less than the MinimumDistance. (PropAgent:agent).AwaitStopMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent has moved less than the minimum distance.") # Get the initial position of the agent from the agent's character in the scene. if (Tracked := PropAgent.GetFortCharacter[]): var StartPosition:vector3 = Tracked.GetTransform().Translation loop: Sleep(0.0) # Get the position of the agent in the next game tick. NewPosition := Tracked.GetTransform().Translation # If the distance of the new position from the starting position is less than MinimumDistance, the agent has not moved and we break the loop.
Countdown Before Heartbeat
Follow these steps to wait for an amount of time equal to HeartBeat.MoveTime - HeartBeat.WarningTime before displaying a warning and countdown timer until the countdown time runs out, and then clears the warning and countdown text.
Create a function named CountdownTimer().
~~~(verse) # Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred. CountdownTimer(PropAgent:agent):void = Logger.Print("Starting heartbeat countdown.") ~~~
You first need to try to get the
heartbeat_warning_uifor the associated player from the map you set up in heartbeat.verse. If that succeeds, you then need to start the delay between the player stopping and the countdown timer displaying.~~~(verse) # Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred. CountdownTimer(PropAgent:agent)<suspends>:void= Logger.Print("Starting heart beat countdown.") if (UIData := HeartBeat.WarningUI[PropAgent]): Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears. Logger.Print("Starting heartbeat warning.") else: Logger.Print("UIData not found.") ~~~
Now create the variable that will appear on the screen and will be decreased by one every second. Name it
WarningTimeRemaining. Set it toWarningTimefrom heartbeat.verse. SinceWarningTimeRemainingis anintandWarningTimeis afloat, you’ll need to use theCeil[]function to create anint.Verse# Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred. CountdownTimer(PropAgent:agent)<suspends>:void= Logger.Print("Starting heart beat countdown.") if (UIData := HeartBeat.WarningUI[PropAgent]): Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears. Logger.Print("Starting heartbeat warning.") var WarningTimeRemaining:int = 0 if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {} else: Logger.Print("UIData not found.")Before starting the countdown loop, use the
deferexpression to clear the countdown timer from the player’s UI whenever theCountdownTimer()function completes. It will only complete when the timer runs out or when the player starts moving again. See Defer to learn more.Verse# Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred. CountdownTimer(PropAgent:agent)<suspends>:void= Logger.Print("Starting heart beat countdown.") if (UIData := HeartBeat.WarningUI[PropAgent]): Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears. Logger.Print("Starting heart beat warning.") var WarningTimeRemaining:int = 0 if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {} # A defer happens when the function completes or if it is canceled, such as if it loses a race. # So in this case, the warning text is cleared when the countdown finishes or if the prop agent moves before the countdown finishes.Finally, create the loop that decreases the countdown timer. Use the
SetText()function to displayHeartBeatWarningMessagewith theWarningTimeRemaining. Then wait one second withSleep(), before decrementing the time remaining. IfWarningTimeRemainingis 0 or less, then the countdown is complete and you can break the loop.Verse# Delays until HeartBeatWarningTime should start. Then counts down by HeartBeatWarningTime and sets the countdown text. Clears the text when deferred. CountdownTimer(PropAgent:agent)<suspends>:void= Logger.Print("Starting heart beat countdown.") if (UIData := HeartBeat.WarningUI[PropAgent]): Sleep(HeartBeat.MoveTime - HeartBeat.WarningTime) # Sleep for the amount of time before the warning appears. Logger.Print("Starting heart beat warning.") var WarningTimeRemaining:int = 0 if (set WarningTimeRemaining = Ceil[HeartBeat.WarningTime]) {} # A defer happens when the function completes or if it is canceled, such as if it loses a race. # So in this case, the warning text is cleared when the countdown finishes or if the prop agent moves before the countdown finishes.
Playing Effects On Idle Players
When AwaitStopMoving() completes, you know it’s time to start the player’s countdown timer, and then their heartbeat effects. But as soon as they start moving again, you want to cancel the timer or the heartbeat, whichever is currently running. To do this, you need a race expression, where the two racing expressions are:
PropAgent.AwaitStartMoving(MinimumMoveDistance).blockwhere there’s a countdown before heartbeat effects play.
AwaitStartMoving() is needed to win the race and stop the countdown timer or heartbeat effects.
The block expression is used to ensure that the two functions within it, CountdownTimer() and StartHeartbeart(), run sequentially and do not race against each other. The countdown timer is meant to let the Prop player know that their heartbeat effects will start after the timer completes, so it wouldn’t make sense to start both the timer and heartbeat at the same time.
Follow these steps to play effects when the player hasn’t moved for too long.
Create an extension method named
AwaitStartMoving()where the implementation is the same except for the following:
It checks if the player has moved
MinimumDistanceor more since the last simulation update. Instead of less than theMinimumDistancelike inAwaitStopMoving().It doesn’t reset the
StartPositionafter each loop. IfStartPositionwas reset at the end of every loop, the player would have to move the entireMinimumDistanceor more in the time it takes to do the simulation update, which might be impossible.Verse# Loops until the agent moves more than the MinimumDistance. (PropAgent:agent).AwaitStartMoving(MinimumDistance:float)<suspends>:void= Logger.Print("Checking if the agent moves further than the minimum distance.") # Get the initial position of the agent from the agent's character in the scene. if (Tracked := PropAgent.GetFortCharacter[]): StartPosition:vector3 = Tracked.GetTransform().Translation loop: Sleep(0.0) # Get the position of the agent in the next game tick. NewPosition := Tracked.GetTransform().Translation # If the distance of the new position from the starting position is greater than or equal to MinimumDistance, the agent has moved and we break the loop.
Create a function named
RunPropGameLoop()that manages to play the heartbeat effect when the player is idle for too long.Verse# If the prop agent stops moving then race to see if the prop agent moves beyond the MinimumMoveDistance, the heartbeat timer completes, or the prop agent is eliminated. RunPropGameLoop(PropAgent:agent)<suspends>:void = Logger.Print("Starting prop agent game loop.")Wait until the prop agent moves less than the minimum distance, then advance.
Verse# If the prop agent stops moving then race to see if the prop agent moves beyond the MinimumMoveDistance, the heartbeat timer completes, or the prop agent is eliminated. RunPropGameLoop(PropAgent:agent)<suspends>:void = Logger.Print("Starting prop agent game loop.") # Loop forever through the prop behavior until the prop agent is eliminated or the player leaves the session. loop: # Wait until the prop agent moves less than the minimum distance, then advance. PropAgent.AwaitStopMoving(MinimumMoveDistance) Sleep(0.0)Add the
raceexpression to race betweenAwaitStartMoving()completing, meaning the player has started moving, and ablockexpression withCountdownTimer()and thenStartHeartbeat()running.Verse# If the prop agent stops moving then race to see if the prop agent moves beyond the MinimumMoveDistance, the heartbeat timer completes, or the prop agent is eliminated. RunPropGameLoop(PropAgent:agent)<suspends>:void = Logger.Print("Starting prop agent game loop.") # Loop forever through the prop behavior until the prop agent is eliminated or the player leaves the session. loop: # Wait until the prop agent moves less than the minimum distance, then advance. PropAgent.AwaitStopMoving(MinimumMoveDistance) # Until the prop agent moves beyond the minimum distance, countdown to the heartbeat and then play the heartbeat indefinitely. race: PropAgent.AwaitStartMoving(MinimumMoveDistance)