Skip to content

Common patterns

Drop-in replacements for the APIs that come up most often when porting. Each snippet is self-contained; adapt names and types to your place.

Raycast

local function raycast(origin: Vector3, direction: Vector3, ignore: { Instance })
    local ray = Ray.new(origin, direction)
    local part, position, normal, material =
        workspace:FindPartOnRayWithIgnoreList(ray, ignore, false, true)

    if part then
        return {
            Instance = part,
            Position = position,
            Normal = normal,
            Material = material,
        }
    end
end

See Physics and spatial queries.

Region query

local function partsInBox(center: Vector3, size: Vector3, ignore: { Instance })
    local half = size / 2
    local region = Region3.new(center - half, center + half)
    return workspace:FindPartsInRegion3WithIgnoreList(region, ignore, 100)
end

Vertical list (UIListLayout)

local function layoutColumn(container: ScrollingFrame, padding: number)
    local y = 0
    for _, child in ipairs(container:GetChildren()) do
        if child:IsA("GuiObject") and child.Visible then
            child.Position = UDim2.new(0, 0, 0, y)
            y += child.AbsoluteSize.Y + padding
        end
    end
    container.CanvasSize = UDim2.new(0, 0, 0, y)
end

See Build a UI without UIListLayout.

Aspect ratio (UIAspectRatioConstraint)

local function keepAspect(gui: GuiObject, aspect: number)
    local function update()
        local width = gui.AbsoluteSize.X
        if width > 0 then
            gui.Size = UDim2.new(0, width, 0, math.floor(width / aspect))
        end
    end
    gui:GetPropertyChangedSignal("AbsoluteSize"):Connect(update)
    update()
end

Rounded corners (UICorner)

Use a 9-slice image:

local corner = Instance.new("ImageLabel")
corner.Name = "Corner"
corner.Size = UDim2.fromScale(1, 1)
corner.BackgroundTransparency = 1
corner.Image = "rbxassetid://0" -- your rounded-rect 9-slice
corner.ScaleType = Enum.ScaleType.Slice
corner.SliceCenter = Rect.new(16, 16, 48, 48)
corner.ZIndex = 0
corner.Parent = frame

Set the content's ZIndex above the corner image. If you do not have a 9-slice asset, keep square corners until you can author one.

ProximityPrompt

Use a ClickDetector for the interaction and a BillboardGui for the prompt:

local detector = Instance.new("ClickDetector")
detector.MaxActivationDistance = 10
detector.Parent = part

detector.MouseClick:Connect(function(player)
    print(player.Name, "interacted with", part:GetFullName())
end)

local billboard = Instance.new("BillboardGui")
billboard.Name = "Prompt"
billboard.Size = UDim2.new(0, 160, 0, 32)
billboard.StudsOffset = Vector3.new(0, 3, 0)
billboard.AlwaysOnTop = true
billboard.Adornee = part

local label = Instance.new("TextLabel")
label.Size = UDim2.fromScale(1, 1)
label.BackgroundTransparency = 1
label.Text = "[E] Interact"
label.TextColor3 = Color3.new(1, 1, 1)
label.TextStrokeTransparency = 0
label.Parent = billboard

billboard.Parent = part

ClickDetector is mouse-oriented. For keyboard interaction, combine it with UserInputService and a distance check on the server.

TextChatService

Nova does not implement TextChatService. Use the legacy chat and Player.Chatted, or build a custom chat UI:

game:GetService("Players").PlayerAdded:Connect(function(player)
    player.Chatted:Connect(function(message)
        print(player.Name, "said", message)
    end)
end)

For custom chat, create a ScreenGui with a ScrollingFrame and a TextBox, and send messages through a RemoteEvent. Keep filtering/moderation in mind if your deployment requires it.

UnreliableRemoteEvent

Use RemoteEvent and batch high-frequency updates:

-- Client: send at most 20 times per second
local accumulator = 0
game:GetService("RunService").RenderStepped:Connect(function(dt)
    accumulator += dt
    if accumulator >= 0.05 then
        accumulator = 0
        remote:FireServer(lookDirection)
    end
end)

HumanoidDescription

Apply classic body parts and clothing instead:

local function dress(character: Model)
    local bodyColors = character:FindFirstChildOfClass("BodyColors")
    if bodyColors then
        bodyColors.HeadColor = BrickColor.new("Bright yellow")
        bodyColors.TorsoColor = BrickColor.new("Bright blue")
    end

    local shirt = Instance.new("Shirt")
    shirt.ShirtTemplate = "rbxassetid://123456789"
    shirt.Parent = character

    local pants = Instance.new("Pants")
    pants.PantsTemplate = "rbxassetid://987654321"
    pants.Parent = character
end

SurfaceAppearance

Bake the PBR maps into a texture and use MeshPart.TextureID (or a SpecialMesh TextureId). SurfaceAppearance itself is not rendered.

Atmosphere and Clouds

Use Lighting fog and a Sky:

local Lighting = game:GetService("Lighting")
Lighting.FogEnd = 500
Lighting.FogStart = 100
Lighting.FogColor = Color3.fromRGB(180, 190, 200)

local sky = Instance.new("Sky")
sky.SkyboxBk = "rbxassetid://0"
sky.SkyboxDn = "rbxassetid://0"
sky.SkyboxFt = "rbxassetid://0"
sky.SkyboxLf = "rbxassetid://0"
sky.SkyboxRt = "rbxassetid://0"
sky.SkyboxUp = "rbxassetid://0"
sky.Parent = Lighting

DataStoreOptions

Nova's GetDataStore takes only (name, scope). Drop the options table and use the supported methods:

-- Before
local store = DataStoreService:GetDataStore("Data", "scope", options)

-- After
local store = DataStoreService:GetDataStore("Data", "scope")

See Data stores.

buffer and vector

buffer and vector are not registered. Use strings/tables for byte data and Vector3/Vector2 for vector math. If you need binary packing, do it in small chunks with string.byte/string.char, or keep the data server-side.

ViewportFrame

There is no ViewportFrame. Use a world camera or a pre-rendered thumbnail. For an inventory preview, render the item once in Studio and use the image.

Next