User Interface

We wanted to add an interactive map UI to let users consume information in the space station that looked and felt like it lived in this world. We decided to build the map inside the 3D space instead of on a screen that overlays the experience. This type of diegetic visualization allows for more immersion with the world as opposed to feeling like it is a completely separate experience.

Designing the Map

To design the map:

  1. We mocked the UI in an external application and came up with a rough idea of how we wanted it to look.

    UI Mock
  2. We exported the individual pieces of the map as .png and imported them into Studio.

    UI Elements Exported

Building the map

Building the map inside Studio involved using Parts and SurfaceGuis.

  1. For non-interactive elements, all we needed to do is add a SurfaceGui object to the part.

  2. For interactive elements, the SurfaceGui also needs to be inside the StarterGui container, with the Adornee property linked to the appropriate part in the 3D workspace. Doing so allows you to add button events.

  3. To achieve a parallax effect, we used three separate ScreenGui instances assigned to three unique Parts with different X values.

    Parallax Example
  4. We then added a glow effect with the SurfaceGui.LightInfluence property. If you set the property value to anything less than 1, it enables the SurfaceGui.Brightness property. By adjusting the brightness, you can increase the glow emitting from the image.

  5. To let users toggle the display of the map, we used a ProximityPrompt that we attached to a 3D model. This is an easy way to allow user interaction with world elements.

    Proximity Prompt in Explorer
  6. Finally, using a UITweenModule ModuleScript inside ReplicatedStorage, we animated hiding and showing the UI with TweenService and a bit of logic for determining the state. By tracking what the user clicked, we could hide and show elements by tweening various properties like alpha, position, and size.

    PUI Tween Module in Explorer
    UITweenModule ModuleScript

    local tweenService = game:GetService("TweenService")
    local UITween = {}
    -- for fading images
    function UITween.fadePart(object, amount, time, delay)
    local tweenAlpha = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quad, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay --Delay time
    )
    local tween = tweenService:Create(object, tweenAlpha, {Transparency = amount})
    tween:Play()
    end
    function UITween.fade(object, amount, time, delay)
    local tweenAlpha = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quad, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay --Delay time
    )
    local tween = tweenService:Create(object, tweenAlpha, {ImageTransparency = amount})
    tween:Play()
    end
    -- for fading images
    function UITween.fadeBackground(object, amount, time, delay)
    local tweenAlpha = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quad, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay --Delay time
    )
    local tween = tweenService:Create(object, tweenAlpha, {BackgroundTransparency = amount})
    tween:Play()
    end
    -- for fading text
    function UITween.fadeText(object, amount, time, delay)
    local tweenAlpha = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quad, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay --Delay time
    )
    local tween1 = tweenService:Create(object, tweenAlpha, {TextTransparency = amount})
    tween1:Play()
    end
    -- for moving text and images
    function UITween.move(object, position, time, delay)
    task.wait(delay)
    object:TweenPosition(position, Enum.EasingDirection.Out, Enum.EasingStyle.Quint, time)
    end
    -- for changing size
    function UITween.size(object, size, time, delay, override, callback)
    local tweenSize = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quint, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay, --Delay time
    override,
    callback
    )
    local tween = tweenService:Create(object, tweenSize, {Size = size})
    tween:Play()
    end
    function UITween.rotate(object, rotation, time, delay, override, callback)
    local tweenSize = TweenInfo.new(
    time, --Time
    Enum.EasingStyle.Quint, --EasingStyle
    Enum.EasingDirection.Out, --EasingDirection
    0, --Repeat count
    false, --Reverses if true
    delay, --Delay time
    override,
    callback
    )
    local tween = tweenService:Create(object, tweenSize, {Rotation = rotation})
    tween:Play()
    end
    -- for blurring the game camera
    function UITween.blur(object, amount, time)
    local tweenInfo = TweenInfo.new(time, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
    local tween = tweenService:Create(object, tweenInfo, {Size = amount})
    tween:Play()
    end
    -- for blurring the game camera
    function UITween.turnOn(object, amount, time)
    local tweenInfo = TweenInfo.new(time, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
    local tween = tweenService:Create(object, tweenInfo, {Brightness = amount})
    tween:Play()
    end
    return UITween
    Applying UI Tween to Objects

    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    -- Add UITween Module
    local UITween = require(ReplicatedStorage.UITweenModule)
    -- Find player Guis and UI objects
    local playerGui = game:GetService('Players').LocalPlayer:WaitForChild('PlayerGui')
    local screenGuiMapUIFrame = playerGui:WaitForChild("ScreenGuiMapUIFrame").SurfaceGui
    local mapUIFrameStroke = screenGuiMapUIFrame.FrameStroke
    local mapUIFrameFill = screenGuiMapUIFrame.FrameFill
    -- Sizes used for tweening
    local frameSizeStart = UDim2.new(0, 0, 0, 0)
    local frameSizeMid = UDim2.new(1, 0, 0.05, 0)
    local frameSizeEnd = UDim2.new(1, 0, 1, 0)
    -- Example Tweening
    UITween.fade(mapUIFrameStroke, 0, 2, 0)
    UITween.size(mapUIFrameStroke, frameSizeMid, 0.4, 0)
    UITween.fade(mapUIFrameFill, 0, 2, 0.5)
    UITween.size(mapUIFrameFill, frameSizeEnd, 0.4, 0.25)
    task.wait(0.25)
    UITween.size(mapUIFrameStroke, frameSizeMid, 0.4, 0)
    UITween.size(mapUIFrameFill, frameSizeMid, 0.4, 0.25)
    task.wait(0.25)
    UITween.size(mapUIFrameStroke, frameSizeEnd, 0.4, 0)
    UITween.size(mapUIFrameFill, frameSizeEnd, 0.4, 0.25)

Here's the final result of the interactive map: