INTRODUCTION
Today, I created a cute purchase-prompter system specifically (but not limited to) individuals and groups who would like to sell their clothing and UGC products in-game! This system was solely created for the purpose of having fun and testing my limits, though I’m here to make this system completely free and easy to use for all of you!
Here’s a demonstration of what the system looks like:
~
FEATURES
- Custom-made proximity system
- Purchase and ownership caching (doesn’t prompt player to purchase twice)
- Customizable configurations (animation IDs, viewport rotation speed, etc.)
- Will function with any type of mannequin used
- Will function with mix-match clothing (shirt and t-shirt, pants and hat, or hell - ALL IN ONE!)
~
HOW TO USE
- Get the model here
- Open Configurations and make any necessary adjustments
- Duplicate / Add your own mannequins in the folder, and everything should function (as long as the assets are directly parented to the mannequin model)!
CRUCIAL STEP
ALL ASSETS MUST have an “ID” attribute with its respective ID found in the catalog. This is the numerical part of the URL (ex. https ://www.roblox.com/catalog/356214819, ID = 356214819).
~
CODE
Configurations
return {
ANIMATION_CHANGE_FREQUENCY = 5, -- Changes to random animation every 5 seconds
ANIMATION_IDS = { -- ANIMATIONS PLAYED IN VIEWPORT UI
507771019, -- Dance 1
507776043, -- Dance 2
507777268, -- Dance 3
},
ACTIVATION_DISTANCE = 6, -- Minimum distance mannequin must be from player to prompt UI
ROTATION_INCREMENT = 100, -- Lower number to increase viewport's rotation speed around character
REMOVE_ACCESSORIES_ON_PREVIEW = false, -- Removes in the viewport when player is trying on a hat
ADD_MISSING_CLOTHES_ON_PREVIEW = false, -- Load default missing articles of clothing when player is trying on a shirt/pants
-- EXAMPLE: If a player is trying on shirts with no pants, their default pants are loaded
DISPLAY_MANNEQUIN_NAME = true, -- Shows ProductName (mannequin names) when UI appears
}
Server
--// Awesom3_Eric
--// September 2nd, 2021
--// Server Initialization
--// Services and Variables \\--
local Players = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")
local PhysicsService = game:GetService("PhysicsService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
local ServerScriptService = game:GetService("ServerScriptService")
-- Variables
local viewports = Instance.new("Folder")
viewports.Name = "Viewports"
viewports.Parent = workspace
local clones = Instance.new("Folder")
clones.Name = "Clones"
clones.Parent = ReplicatedStorage
--// Collisions \\--
PhysicsService:CreateCollisionGroup("Players")
PhysicsService:CollisionGroupSetCollidable("Players", "Players", false)
-- Set character to collision group
local function setCollision(instance)
if instance:IsA("BasePart") then
PhysicsService:SetPartCollisionGroup(instance, "Players")
end
for _, child in ipairs(instance:GetChildren()) do
setCollision(child)
end
instance.ChildAdded:Connect(setCollision)
end
--// Move Instances \\--
local Configurations = script.Parent
script.Parent = ServerScriptService
Configurations.ClientHandler.Parent =StarterPlayer.StarterPlayerScripts
Configurations.AddAccessory.Parent = ReplicatedStorage
Configurations.AssetBought.Parent = ReplicatedStorage
Configurations.Prompt.Parent = StarterGui
Configurations.Parent = ReplicatedStorage
Configurations = require(Configurations)
--// CharacterAdded \\--
local function characterAdded(character)
local player = Players:GetPlayerFromCharacter(character)
player.CharacterAppearanceLoaded:Connect(function(fullCharacter)
-- Create clone
fullCharacter.Humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
fullCharacter.Archivable = true
local characterClone = fullCharacter:Clone()
-- Replace old clone
local oldClone = clones:FindFirstChild(player.Name)
if oldClone then
oldClone:Destroy()
end
fullCharacter:Clone().Parent = clones
-- Replace viewport clone
local oldViewportClone = viewports:FindFirstChild(player.Name)
if oldViewportClone then
oldViewportClone:Destroy()
end
-- Set up viewport (invisible in workspace)
local viewportClone = fullCharacter:Clone()
viewportClone.HumanoidRootPart.Anchored = true
viewportClone.HumanoidRootPart.CFrame *= CFrame.new(0, -100, 0)
for _, part in ipairs(viewportClone:GetDescendants()) do
if part:IsA("BasePart") or part:IsA("Decal") then
part.Transparency = 1
end
end
viewportClone.Parent = viewports
-- Play animation
local cache = {}
local runners = {}
for _, animationId in ipairs(Configurations.ANIMATION_IDS) do
local animation = Instance.new("Animation")
animation.AnimationId = "rbxassetid://" .. tostring(animationId)
animation.Parent = viewportClone.Humanoid
table.insert(runners, viewportClone.Humanoid.Animator:LoadAnimation(animation))
end
-- Random animation
local update = tick()
local index = 1
local changeConnection; changeConnection = RunService.Heartbeat:Connect(function()
if player.Parent == nil then
changeConnection:Disconnect()
return
else
if tick() - update >= Configurations.ANIMATION_CHANGE_FREQUENCY then
update = tick()
index = index == #runners and 1 or index + 1
for _, runner in ipairs(runners) do
runner:Stop()
end
runners[index]:Play()
end
end
end)
fullCharacter.Humanoid.Died:Connect(function()
changeConnection:Disconnect()
end)
end)
setCollision(character)
end
--// Player Initialization \\--
-- PlayerAdded
local function playerAdded(player)
characterAdded(player.Character or player.CharacterAdded:Wait())
player.CharacterAdded:Connect(characterAdded)
end
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(playerAdded)(player)
end
Players.PlayerAdded:Connect(playerAdded)
-- PlayerRemoving
local function playerRemoving(player)
local clone = ReplicatedStorage.Clones:FindFirstChild(player.Name)
if clone then
clone:Destroy()
end
end
Players.PlayerRemoving:Connect(playerRemoving)
-- Game shutdown
game:BindToClose(function()
for _, player in ipairs(Players:GetPlayers()) do
coroutine.wrap(playerRemoving)(player)
end
end)
--// Remotes \\--
ReplicatedStorage.AddAccessory.OnServerEvent:Connect(function(player, accessory)
local viewportCharacter = viewports:FindFirstChild(player.Name)
if viewportCharacter then
local alreadyHas = viewportCharacter:FindFirstChild(accessory.Name)
if not alreadyHas then
if accessory ~= nil and accessory:IsA("Accessory") then
local clone = accessory:Clone()
clone.Handle.Transparency = 1
clone.Parent = viewportCharacter
end
end
end
end)
--// Purchased \\--
MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
if isPurchased then
ReplicatedStorage.AssetBought:FireClient(player, assetId)
end
end)
Client
--// Awesom3_Eric
--// September 2nd, 2021
--// Client UI Handling
--// Services and Variables \\--
-- Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
-- Modules
local Maid = require(script.Maid)
local Configurations = require(ReplicatedStorage.Configurations)
-- Character
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
player.CharacterAdded:Connect(function(newCharacter)
character = newCharacter
end)
-- UI
local Prompt = player:WaitForChild("PlayerGui"):WaitForChild("Prompt")
local main = Prompt.Main
local choices = main.Choices
local expand = main.Expand
local collapse = main.Collapse
local preview = main.Preview
local productName = main.ProductName
-- Variables
local mannequins = workspace.Mannequins
local viewports = workspace.Viewports
local clones = ReplicatedStorage.Clones
local camera = workspace.CurrentCamera
local CURRENT_MANNEQUIN = nil
local VIEWPORT_CHARACTER = nil
--// Set-Up Mannequins \\--
-- Index mannequins and position using :GetPivot()
local mannequinData = {}
for _, mannequin in ipairs(mannequins:GetChildren()) do
mannequinData[mannequin] = mannequin:GetPivot().Position
end
-- Returns closest mannequin from character in mannequinDictionary
local function getClosestMannequin()
local closestMannequin = nil
local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
if humanoidRootPart then
local currentDistance = Configurations.ACTIVATION_DISTANCE
for mannequin, position in pairs(mannequinData) do
local distance = (humanoidRootPart.Position - position).Magnitude
if distance <= currentDistance then
currentDistance = distance
closestMannequin = mannequin
end
end
end
return closestMannequin
end
-- Updates viewport character (called when adornee is changed)
local clothesTypes = {"ShirtGraphic", "Shirt", "Pants"}
local function updateCharacter()
productName.Text = Configurations.DISPLAY_MANNEQUIN_NAME and CURRENT_MANNEQUIN.Name or ""
-- Sets button visibility based on clothing on mannequin
choices.BuyTShirt.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic")
choices.BuyPants.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Pants")
choices.BuyShirt.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Shirt")
choices.BuyHat.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Accessory")
-- Apply mannequin clothing to viewport character
if VIEWPORT_CHARACTER then
local mannequinAccessory = CURRENT_MANNEQUIN:FindFirstChildOfClass("Accessory")
if mannequinAccessory then
ReplicatedStorage.AddAccessory:FireServer(mannequinAccessory)
for _, accessory in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
if accessory:IsA("Accessory") then
if accessory:GetAttribute("AddedAccessory") then
accessory.Handle.Transparency = accessory.Name == mannequinAccessory.Name and 0 or 1
else
accessory.Handle.Transparency = Configurations.REMOVE_ACCESSORIES_ON_PREVIEW and 1 or 0
end
end
end
else
for _, accessory in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
if accessory:IsA("Accessory") then
accessory.Handle.Transparency = accessory:GetAttribute("AddedAccessory") and 1 or 0
end
end
end
-- Replicate Clothing
for _, cloth in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
if cloth:IsA("Clothing") or cloth:IsA("ShirtGraphic") then
cloth:Destroy()
end
end
local noClothes = not CURRENT_MANNEQUIN:FindFirstChildOfClass("Shirt") and not CURRENT_MANNEQUIN:FindFirstChildOfClass("Pants")
local characterClone = ReplicatedStorage.Clones[player.Name]
if noClothes then
for _, defaultCloth in ipairs(characterClone:GetChildren()) do
if defaultCloth:IsA("Clothing") or defaultCloth:IsA("ShirtGraphic") then
defaultCloth:Clone().Parent = VIEWPORT_CHARACTER
end
end
if CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic") then
CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic"):Clone().Parent = VIEWPORT_CHARACTER
end
else
for _, cloth in ipairs(CURRENT_MANNEQUIN:GetChildren()) do
if cloth:IsA("Clothing") or cloth:IsA("ShirtGraphic") then
cloth:Clone().Parent = VIEWPORT_CHARACTER
end
end
if Configurations.ADD_MISSING_CLOTHES_ON_PREVIEW then
for _, cloth in ipairs(clothesTypes) do
if not VIEWPORT_CHARACTER:FindFirstChildOfClass(cloth) then
local defaultCloth = characterClone:FindFirstChildOfClass(cloth)
if defaultCloth then
defaultCloth:Clone().Parent = VIEWPORT_CHARACTER
end
end
end
end
end
end
end
-- RunService updates adornee with closest mannequin
local update = tick()
RunService.Heartbeat:Connect(function()
if tick() - update > 0.1 then
update = tick()
local closestMannequin = getClosestMannequin()
if closestMannequin then
local node = closestMannequin:FindFirstChild("Node")
if not node then
node = Instance.new("Part")
node.Anchored = true
node.Transparency = 1
node.CanCollide = false
node.Size = Vector3.new(0, 0, 0)
node.Position = mannequinData[closestMannequin]
node.Name = "Node"
node.Parent = closestMannequin
end
Prompt.Adornee = node
Prompt.Enabled = true
if closestMannequin ~= CURRENT_MANNEQUIN then
CURRENT_MANNEQUIN = closestMannequin
main.Size = UDim2.new(0, 0, 0, 0)
main:TweenSize(UDim2.new(1, 0, 1, 0), "Out", "Quad", 0.2, true)
updateCharacter()
end
else
Prompt.Adornee = nil
Prompt.Enabled = false
end
end
end)
--// Update Part CFrames of Viewport Character \\--
local updater = Maid.new()
local function updateViewport()
updater:DoCleaning()
-- Create new camera instance
local newCamera = Instance.new("Camera")
newCamera.Parent = preview.Viewport
preview.Viewport.CurrentCamera = newCamera
updater:GiveTask(newCamera)
-- Clone character into viewport frame
local viewportCharacter = viewports[player.Name]
local newClone = clones[player.Name]:Clone()
newClone.Parent = preview.Viewport
VIEWPORT_CHARACTER = newClone
updater:GiveTask(newClone)
-- Rotate camera around HumanoidRootPart
local humanoidRootPart = newClone:WaitForChild("HumanoidRootPart")
local rotationalIndex = 1
updater:GiveTask(RunService.Heartbeat:Connect(function()
rotationalIndex += 1
newCamera.CFrame = humanoidRootPart.CFrame * CFrame.Angles(0, rotationalIndex * (math.pi/Configurations.ROTATION_INCREMENT), 0) * CFrame.new(0, -0.25, 6)
end))
-- Index limbs/accessories and match with viewport character in workspace
local partData = {}
for _, part in ipairs(newClone:GetChildren()) do
if part:IsA("BasePart") then
partData[part] = viewportCharacter:WaitForChild(part.Name)
elseif part:IsA("Accessory") and part.Parent then
partData[part:WaitForChild("Handle")] = viewportCharacter:WaitForChild(part.Name):WaitForChild("Handle")
end
end
-- Update CFrames of baseparts to match animation
updater:GiveTask(RunService.RenderStepped:Connect(function()
for part, target in pairs(partData) do
part.CFrame = target.CFrame
end
end))
-- Adds new accessory to viewport
updater:GiveTask(viewportCharacter.ChildAdded:Connect(function(accessory)
if accessory:IsA("Accessory") then
accessory:WaitForChild("Handle")
local clone = accessory:Clone()
clone:SetAttribute("AddedAccessory", true)
clone.Handle.Transparency = 0
clone.Parent = newClone
updater:GiveTask(clone)
partData[accessory.Handle] = clone.Handle
end
end))
end
-- Initialize
coroutine.wrap(function()
viewports:WaitForChild(player.Name); updateViewport()
viewports.ChildAdded:Connect(function(child)
if child.Name == player.Name then
updateViewport()
end
end)
end)()
--// Choice Functions \\--
local ownedCache = {}
local function isOwned(id)
if not ownedCache[id] then
local success, owned = pcall(MarketplaceService.PlayerOwnsAsset, MarketplaceService, player, id)
ownedCache[id] = success and owned or false
end
return ownedCache[id]
end
ReplicatedStorage.AssetBought.OnClientEvent:Connect(function(id)
ownedCache[id] = true
end)
local function promptPurchase(button, class)
local id = CURRENT_MANNEQUIN:FindFirstChildOfClass(class):GetAttribute("ID")
if id then
if not isOwned(id) then
MarketplaceService:PromptPurchase(player, id)
else
print("NOPE")
button.Label.Text = "OWNED"
delay(0.5, function()
button.Label.Text = button:GetAttribute("OriginalText")
end)
end
end
end
choices.BuyShirt.Detection.MouseButton1Click:Connect(function()
promptPurchase(choices.BuyShirt, "Shirt")
end)
choices.BuyPants.Detection.MouseButton1Click:Connect(function()
promptPurchase(choices.BuyPants, "Pants")
end)
choices.BuyTShirt.Detection.MouseButton1Click:Connect(function()
promptPurchase(choices.BuyTShirt, "ShirtGraphic")
end)
choices.BuyHat.Detection.MouseButton1Click:Connect(function()
promptPurchase(choices.BuyHat, "Accessory")
end)
--// Main Button Functions \\--
-- Opens/Closes choices frame
local buttonDebounce = false
local function toggleChoices(toggled)
if toggled then
choices.Visible = true
choices.Size = UDim2.new(0, 0, 0, 0)
choices:TweenSize(UDim2.new(1, 0, 1, 0), "Out", "Quad", 0.1, true)
main.Collapse.Visible = true
main.Collapse.Size = UDim2.new(0, 0, 0, 0)
main.Collapse:TweenSize(UDim2.new(0.15, 0, 0.15, 0), "Out", "Quad", 0.1, true)
else
choices:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
main.Collapse:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
delay(0.1, function()
choices.Visible = false
main.Collapse.Visible = false
end)
end
end
-- Close preview and opens choices
preview.Close.Detection.MouseButton1Click:Connect(function()
if not buttonDebounce then
buttonDebounce = true
toggleChoices(true)
preview:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
delay(0.1, function()
preview.Visible = false
buttonDebounce = false
end)
end
end)
-- Opens preview and closes choices
choices.Preview.Detection.MouseButton1Click:Connect(function()
if not buttonDebounce then
buttonDebounce = true
preview.Visible = true
preview.Size = UDim2.new(0, 0, 0, 0)
preview:TweenSize(UDim2.new(0.85, 0, 0.85, 0), "Out", "Quad", 0.1, true)
toggleChoices(false)
task.wait(0.1)
buttonDebounce = false
end
end)
-- Opens choices and closes expand button and product name
main.Expand.Detection.MouseButton1Click:Connect(function()
if not buttonDebounce then
buttonDebounce = true
toggleChoices(true)
expand.Visible = false
productName:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
delay(0.1, function()
buttonDebounce = false
main.ProductName.Visible = false
end)
end
end)
-- Closes choices and opens expand button and product name
main.Collapse.Detection.MouseButton1Click:Connect(function()
if not buttonDebounce then
buttonDebounce = true
expand.Visible = true
productName.Visible = true
productName:TweenSize(UDim2.new(2, 0, 0.15, 0), "Out", "Quad", 0.1, true)
toggleChoices(false)
delay(0.1, function()
buttonDebounce = false
choices.Visible = false
end)
end
end)
--// Button Effects \\--
local function createButton(frame)
local originalSize = frame.Size
local expandSize = UDim2.fromScale(originalSize.X.Scale * 1.1, originalSize.Y.Scale * 1.1)
local shrinkSize = UDim2.fromScale(originalSize.X.Scale * 0.9, originalSize.Y.Scale * 0.9)
local detection = frame.Detection
detection.MouseEnter:Connect(function()
frame:TweenSize(expandSize, "Out", "Quad", 0.1, true)
end)
detection.MouseLeave:Connect(function()
frame:TweenSize(originalSize, "Out", "Quad", 0.1, true)
end)
detection.MouseButton1Down:Connect(function()
frame:TweenSize(shrinkSize, "Out", "Quad", 0.1, true)
end)
detection.MouseButton1Up:Connect(function()
frame:TweenSize(expandSize, "Out", "Quad", 0.1, true)
end)
end
-- Initialize button effects on buttons
for _, choice in ipairs(choices:GetChildren()) do
if choice:IsA("Frame") then
choice:SetAttribute("OriginalText", choice.Label.Text)
createButton(choice)
end
end
createButton(preview.Close)
createButton(expand)
createButton(collapse)
~
CLOSING
This system probably will NOT be updated, though if there are considerable critiques and suggestions, as well as critical bugs in the system, I most definitely will edit them. Let me know if you have any questions and I will assist you Enjoy!