Overview
Floor and Ceil are built-in Verse functions that convert a float (or a rational produced by integer division) into an int.
Floor(Val:float)<reads><decides>:int— returns the largest integer ≤ Val. Think "floor beneath your feet": 3.9 → 3, -1.2 → -2.Ceil(Val:float)<reads><decides>:int— returns the smallest integer ≥ Val. Think "ceiling above your head": 3.1 → 4, -1.8 → -1.
Both functions carry the <decides> effect, meaning they are failable — they fail (and must be called inside a failure context such as if) when the input is not a finite number (e.g. infinity or NaN). For normal gameplay values this rarely triggers, but you must still wrap every call in an if expression or the code will not compile.
When to reach for them:
- Distributing a resource evenly (how many full magazines can I craft?)
- Displaying a progress bar that must show whole numbers
- Snapping a float timer to the nearest whole second
- Calculating fair team splits or wave counts
API Reference
(API surface could not be resolved for this device.)
Walkthrough
Scenario: A Shop That Sells Ammo Quivers
Imagine a vending area where players spend gold coins to buy quivers of arrows. Each quiver costs 3 coins and holds 10 arrows. We want to tell the player exactly how many arrows they can afford — no fractions allowed.
We use Floor to round the division result down (you can't buy half a quiver) and Ceil to show the player the minimum coins they'd need to afford one more quiver than they currently can.
using { /Fortnite.com/Devices }
using { /Verse.org/Verse }
using { /Verse.org/Simulation }
# Localisation helper — message params must be localised values
ArrowMessage<localizes>(N:int):message = "You can buy {N} arrows."
NextTierMessage<localizes>(N:int):message = "Spend {N} more coins for an extra quiver."
ammo_shop_device := class(creative_device):
# Wire this to a trigger_device placed on the shop counter in UEFN
@editable
ShopTrigger : trigger_device = trigger_device{}
# Wire this to a hud_message_device to show the player feedback
@editable
HudMessage : hud_message_device = hud_message_device{}
# Coins the player currently has (set this from your economy system)
PlayerCoins : int = 17
CoinsPerQuiver : int = 3
ArrowsPerQuiver : int = 10
OnBegin<override>()<suspends>:void =
ShopTrigger.TriggeredEvent.Subscribe(OnShopVisited)
OnShopVisited(Agent : ?agent) : void =
# Integer division produces a rational — Floor converts it to int.
# Both the division and Floor are failable, so we use if.
if (QuiversAffordable := Floor(PlayerCoins / CoinsPerQuiver)):
ArrowsAffordable := QuiversAffordable * ArrowsPerQuiver
HudMessage.SetText(ArrowMessage(ArrowsAffordable))
# Ceil tells us the minimum coins needed to reach the NEXT quiver tier.
# (QuiversAffordable + 1) quivers * CoinsPerQuiver gives the target spend.
NextTierCost : int = (QuiversAffordable + 1) * CoinsPerQuiver
CoinsShort : int = NextTierCost - PlayerCoins
if (CoinsShort > 0):
HudMessage.SetText(NextTierMessage(CoinsShort))
Line-by-line explanation:
| Lines | What's happening |
|---|---|
ArrowMessage<localizes> |
Declares a localised message template — required because hud_message_device.SetText takes message, not string. |
ShopTrigger.TriggeredEvent.Subscribe(OnShopVisited) |
Subscribes our handler to the trigger. Subscriptions always live in OnBegin. |
OnShopVisited(Agent : ?agent) |
TriggeredEvent sends ?agent; we don't need the agent here so we leave it unwrapped. |
Floor(PlayerCoins / CoinsPerQuiver) |
PlayerCoins / CoinsPerQuiver is integer division → rational. Floor converts it to int, rounding down. The whole expression is failable, so it lives inside if. |
QuiversAffordable * ArrowsPerQuiver |
Pure integer multiplication — no rounding needed. |
Ceil (conceptual use via NextTierCost) |
We compute the ceiling manually here; see the Common Patterns section below for a direct Ceil call on a float. |
Common patterns
Pattern 1 — Ceil on a float: rounding up a wave timer
You have a float countdown (e.g. from a physics simulation) and need to display whole seconds, always rounding up so the UI never shows 0 when time remains.
using { /Fortnite.com/Devices }
using { /Verse.org/Verse }
using { /Verse.org/Simulation }
SecondsMessage<localizes>(N:int):message = "{N}s remaining"
wave_timer_device := class(creative_device):
@editable
TimerDisplay : hud_message_device = hud_message_device{}
# Simulated float time remaining from an external system
TimeRemainingFloat : float = 4.3
OnBegin<override>()<suspends>:void =
# Ceil rounds 4.3 up to 5 — the player always sees at least the
# true remaining time, never an optimistic undercount.
if (WholeSeconds := Ceil(TimeRemainingFloat)):
TimerDisplay.SetText(SecondsMessage(WholeSeconds))
Pattern 2 — Floor on a float: converting a distance in cm to whole metres
Creative devices report positions in centimetres as floats. Floor lets you snap that to a whole-metre display value.
using { /Fortnite.com/Devices }
using { /Verse.org/Verse }
using { /Verse.org/Simulation }
DistanceMessage<localizes>(M:int):message = "Distance: {M}m"
distance_display_device := class(creative_device):
@editable
DistanceHud : hud_message_device = hud_message_device{}
@editable
MarkerDevice : creative_device = creative_device{}
OnBegin<override>()<suspends>:void =
# GetGlobalTransform returns a transform whose Translation is in cm.
MyTransform := GetGlobalTransform()
MarkerTransform := MarkerDevice.GetGlobalTransform()
DeltaX := MarkerTransform.Translation.X - MyTransform.Translation.X
DeltaY := MarkerTransform.Translation.Y - MyTransform.Translation.Y
# Approximate 2D distance in cm as a float
DistanceCm : float = Sqrt(DeltaX * DeltaX + DeltaY * DeltaY)
DistanceM_Float : float = DistanceCm / 100.0
# Floor: always show the distance the player has DEFINITELY covered
if (WholeMetres := Floor(DistanceM_Float)):
DistanceHud.SetText(DistanceMessage(WholeMetres))
Pattern 3 — Both Floor and Ceil together: fair team resource split
You have 7 supply crates to distribute across 2 teams. Team A gets the floor (guaranteed minimum), Team B gets the ceiling (gets the spare).
using { /Fortnite.com/Devices }
using { /Verse.org/Verse }
using { /Verse.org/Simulation }
TeamAMessage<localizes>(N:int):message = "Team A gets {N} crates."
TeamBMessage<localizes>(N:int):message = "Team B gets {N} crates."
crate_splitter_device := class(creative_device):
@editable
HudA : hud_message_device = hud_message_device{}
@editable
HudB : hud_message_device = hud_message_device{}
TotalCrates : int = 7
Teams : int = 2
OnBegin<override>()<suspends>:void =
# Integer division 7/2 = rational 3.5
# Floor → 3 (Team A's guaranteed share)
# Ceil → 4 (Team B gets the remainder)
if:
TeamAShare := Floor(TotalCrates / Teams)
TeamBShare := Ceil(TotalCrates / Teams)
then:
HudA.SetText(TeamAMessage(TeamAShare))
HudB.SetText(TeamBMessage(TeamBShare))
Gotchas
1. Both functions are <decides> — always wrap in if
Floor and Ceil fail when passed a non-finite float (infinity, NaN). Even if your value is always finite at runtime, the compiler requires them to be in a failure context. Forgetting the if is the #1 compile error beginners hit:
# ❌ Won't compile — Floor is failable, must be in a failure context
MyInt := Floor(3.7)
# ✅ Correct
if (MyInt := Floor(3.7)):
# use MyInt here
2. Integer division produces a rational, not a float
7 / 2 in Verse (both operands int) yields a rational, not a float. You can pass a rational directly to Floor/Ceil — but you cannot assign it to a float variable first. Rationals are a compile-time-only type used exclusively as arguments to these two functions.
# ❌ Won't compile — rational is not assignable to float
R : float = 7 / 2
# ✅ Pass the rational expression directly
if (N := Floor(7 / 2)): # N = 3
3. Floor rounds toward negative infinity, not toward zero
This matters for negative numbers:
Floor(-2.3)→ -3 (not -2)Ceil(-2.3)→ -2 (not -3)
If you want truncation toward zero for negative numbers, you need to handle the sign yourself.
4. No implicit int↔float conversion
Verse never automatically converts between int and float. If you need to pass an int to a function expecting float, cast explicitly with Int * 1.0 or use a float literal. Similarly, the int returned by Floor/Ceil cannot be used where a float is expected without conversion.
5. message parameters require <localizes> — not raw strings
If you display Floor/Ceil results in a hud_message_device, you cannot pass a plain string. Declare a localised template:
MyLabel<localizes>(N:int):message = "Score: {N}"
# then: HudMessage.SetText(MyLabel(FlooredValue))