Surface Art

Players often enjoy feeling like they're a part of constructing the space they're in. The SurfaceArt developer module lets players literally leave their mark in an experience.

Module Usage

Installation

To use the SurfaceArt module in an experience:

  1. From the View tab, open the Toolbox and select the Creator Store tab.

    Toolbox toggle button in Studio
  2. Make sure the Models sorting is selected, then click the See All button for Categories.

  3. Locate and click the Dev Modules tile.

  4. Locate the Surface Art module and click it, or drag-and-drop it into the 3D view.

  5. In the Explorer window, move the entire SurfaceArt model into ServerScriptService. Upon running the experience, the module will distribute itself to various services and begin running.

Positioning the Canvas

The module comes with one SurfaceCanvas model that you can position in the 3D world. This model is what players will interact with to place art on its surface.

  1. Locate the SurfaceCanvas mesh inside the Workspace folder of the module's main folder.

  2. Move it into the top-level Workspace hierarchy and position it where desired.

  3. Upon publishing/running a test session, players will be able to interact with the object via a ProximityPrompt and place art on the defined surface.

Changing the Canvas Face

Under the hood, the module uses a SurfaceGui to display art items. To configure which surface the art appears on:

  1. Select the SurfaceCanvas mesh.

  2. At the bottom of the Properties window, locate the SurfaceCanvasFace attribute with a default value of Right.

  3. Click the attribute and enter one of six values that describe a Enum.NormalId.

Using Custom Art Assets

To better fit the theme of your experience, you may use your own set of custom assets instead of the defaults. This can be done via the configure function, called from a Script in ServerScriptService.

Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
local customAssets = {
CustomAsset1 = {
name = "Custom Asset 1",
assetId = "rbxassetid://7322508294",
},
CustomAsset2 = {
name = "Custom Asset 2",
assetId = "rbxassetid://7322547665",
},
}
SurfaceArt.configure({
assets = customAssets,
})

Clearing All Canvases

To remove all existing art from all canvases in the world, call the removeAllArt function from a Script.

Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.removeAllArt()

Showing Custom Effects

There may be cases where you'd like to include additional visual effects when an artwork is placed. This module exposes an event called artChanged on the client that you can connect to and add your own logic.

LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
local function createParticleEmitter(canvas, position)
local attachment = Instance.new("Attachment")
attachment.Position = canvas.CFrame:PointToObjectSpace(position)
attachment.Axis = Vector3.new(0, 0, 1)
attachment.SecondaryAxis = Vector3.new(1, 0, 0)
attachment.Parent = canvas
local particleEmitter = Instance.new("ParticleEmitter")
particleEmitter.Speed = NumberRange.new(50)
particleEmitter.Rate = 50
particleEmitter.Color = ColorSequence.new(Color3.fromRGB(128, 254, 7))
particleEmitter.SpreadAngle = Vector2.new(35, 35)
particleEmitter.Parent = attachment
return attachment
end
SurfaceArt.artChanged:Connect(function(canvas, spot, spotPosition, artId, ownerId)
if artId then
-- Show some sparkles for 3 seconds
task.spawn(function()
local emitterAttachment = createParticleEmitter(canvas, spotPosition)
task.wait(3)
emitterAttachment:Destroy()
end)
end
end)

API Reference

Types

SurfaceArtAsset

Images to be used as art for the canvas are represented by a table with two values.

KeyDescription
nameMetadata display name.
assetIdAsset ID of the image to include.

Functions

configure

configure(config: table)

Overrides default configuration options through the following keys/values in the config table. This function can only be called from a Script.

KeyDescriptionDefault
enabledToggles the module's functionality on or off.true
assetsList of SurfaceArtAsset types.(see code below)
quotaPerPlayerMaximum number of art pieces that can be placed by each player.2
Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.configure({
quotaPerPlayer = 4,
promptKeyCode = Enum.KeyCode.T,
promptMaxActivationDistance = 8,
})

getCanvases

getCanvases(): table

Returns all of the canvases tagged with the SurfaceCanvas tag.

Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
local canvases = SurfaceArt.getCanvases()

placeArt

placeArt(player: Player, canvas: BasePart)

Places an art piece programmatically on behalf of a player. Note that the canvas object must be tagged with the SurfaceCanvas tag when the server is initialized. It is recommended to use this only with a canvas returned from getCanvases.

Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
local remoteEvent = ReplicatedStorage:WaitForChild("SurfaceArtRemoteEvent")
remoteEvent.OnServerEvent:Connect(function(player)
-- Place the Bloxy Award from default art assets into the first canvas
local canvases = SurfaceArt.getCanvases()
SurfaceArt.placeArt(player, canvases[1], "BloxyAward")
end)

removeAllArt

removeAllArt()

Removes all artwork from all surfaces.

Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.removeAllArt()

Events

artChanged

Fires when an artwork is changed at a particular location on a canvas. When an artwork is removed, artId will be nil. Note that a Vector3 value is passed as the third parameter to the event handler so that you can position a custom effect at the exact position where the artwork is placed. This event can only be connected in a LocalScript.

Parameters
canvas: BasePartCanvas upon which the artwork was changed.
spot: FrameInternal Frame that contains the artwork ImageLabel.
spotPosition: Vector3Exact position where the artwork was placed.
artId: stringAsset ID of the new artwork.
ownerUserId: numberUserId of the player who placed the art.
LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.artChanged:Connect(function(canvas, spot, spotPosition, artId, ownerId)
print("Art placed at:", spotPosition)
print("Art asset ID:", artId)
print("Art placed by:", ownerId)
end)

promptShown

Fires when a canvas interaction prompt is shown to a player. The connected function receives the canvas upon which the prompt is showing. This event can only be connected in a LocalScript.

Parameters
canvas: BasePartCanvas upon which the prompt is showing.
LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.promptShown:Connect(function(canvas)
print(Players.LocalPlayer, canvas)
end)

promptHidden

Fires when a canvas interaction prompt is hidden. The connected function receives the canvas upon which the prompt was showing. This event can only be connected in a LocalScript.

Parameters
canvas: BasePartCanvas upon which the prompt was showing.
LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.promptClosed:Connect(function(canvas)
print(Players.LocalPlayer, canvas)
end)

selectorShown

Fires when the surface art selector UI is shown to a player. This event can only be connected in a LocalScript.

LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.selectorShown:Connect(function()
print(Players.LocalPlayer, "opened surface art selector")
end)

selectorHidden

Fires when the surface art selector UI is hidden for a player. This event can only be connected in a LocalScript.

LocalScript

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local SurfaceArt = require(ReplicatedStorage:WaitForChild("SurfaceArt"))
SurfaceArt.selectorHidden:Connect(function()
print(Players.LocalPlayer, "closed surface art selector")
end)