Reference Verse compiles

Floor and Ceil: Snapping Numbers to Whole Values

When your game logic produces a fractional number — dividing coins by a price, splitting health across teammates, or calculating how many full waves fit in a timer — you need a clean integer to act on. `Floor()` and `Ceil()` are Verse's two rounding workhorses: Floor snaps down to the nearest integer, Ceil snaps up. Knowing which one to reach for (and how to handle their failable nature) is a skill every Verse creator needs.

Updated Examples verified on the live UEFN compiler
Watch the Knotmath_floor_ceil in ~90 seconds.

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))

Build your own lesson with math_floor_ceil

Generate a personalized, step-by-step lesson plan built around this object — grounded in this exact reference and our compile-verified knowledge base.

Build a lesson →