I’ve been stuck on this for days and its driving me up a wall with how frustrating it is lol.
I have a character customization GUI in my game that I’m making that utilizes a Viewport Frame and a player clone within that frame called “ViewportClone”. The player can apply color, hair, hats, faces, heads, bundles, and both UGC and 2D clothing to this clone. What I want is for them to be able to click a “Submit” button that will allow them to morph into their clone they’ve customized. I chose this approach because I didn’t want players to be able to see another person actively changing their avatar since it seems a bit clunky to me and I wanted something a bit more polished.
So far, I’ve been having a hard time getting this to work. Particularly because I can’t figure out how to get the clone to the server since it’s within a client-side script and from what I’ve found online, anything sent to Replicated Storage from the client can’t be seen by the server??
Any help is appreciated.
Code That Isn't Working
Submit Button:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
local morphEvent = ReplicatedStorage:WaitForChild("MorphEvent")
local scaleEvent = ReplicatedStorage:WaitForChild("ApplyScaleEvent")
local hatEvent = ReplicatedStorage:WaitForChild("AddHatEvent")
local submitButton = script.Parent
local cloneManager = require(ReplicatedStorage:WaitForChild("ViewportCloneManager"))
-- Helper to get Shirt and Pants info from clone
local function getClothingInfo(clone)
local shirt = clone:FindFirstChildOfClass("Shirt")
local pants = clone:FindFirstChildOfClass("Pants")
return {
ShirtAssetId = shirt and shirt.ShirtTemplate or nil,
PantsAssetId = pants and pants.PantsTemplate or nil,
}
end
-- Helper to get scale attributes from clone
local function getScaleInfo(clone)
return {
Height = tonumber(clone:GetAttribute("ScaleHeight")) or 1,
Width = tonumber(clone:GetAttribute("ScaleWidth")) or 1,
Depth = tonumber(clone:GetAttribute("ScaleDepth")) or 1,
}
end
-- Helper to get hat name or "None"
local function getHatInfo(clone)
local hatName = clone:GetAttribute("SelectedHat")
return hatName or "None"
end
submitButton.MouseButton1Click:Connect(function()
print("🚀 Submit button clicked!")
local characterClone = cloneManager.GetClone()
if not characterClone then
warn("Character clone not found.")
return
end
local clothing = getClothingInfo(characterClone)
local scale = getScaleInfo(characterClone)
local hat = getHatInfo(characterClone)
print("Firing MorphEvent for appearance")
morphEvent:FireServer(clothing)
print("Firing ScaleEvent for scale")
scaleEvent:FireServer(scale)
print("Firing HatEvent for hat")
hatEvent:FireServer(hat)
end)
My Server Script:
local ApplyAppearance = ReplicatedStorage:WaitForChild("ApplyAppearance")
local ApplyScaleEvent = ReplicatedStorage:WaitForChild("ApplyScaleEvent")
local ApplyHatEvent = ReplicatedStorage:WaitForChild("ApplyHatEvent") -- make sure you have this RemoteEvent
local HatsFolder = ReplicatedStorage:WaitForChild("Hats") -- folder with hat models
-- Helper to clear existing hats/accessories
local function clearAccessories(humanoid)
for _, accessory in ipairs(humanoid:GetAccessories()) do
accessory:Destroy()
end
end
-- Appearance handler
ApplyAppearance.OnServerEvent:Connect(function(player, appearanceData)
local character = player.Character
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Remove clothing and accessories
for _, item in ipairs(character:GetChildren()) do
if item:IsA("Shirt") or item:IsA("Pants") then
item:Destroy()
end
end
clearAccessories(humanoid)
-- Apply clothing
for _, clothing in ipairs(appearanceData.Clothing or {}) do
if clothing.Type == "Shirt" then
local shirt = Instance.new("Shirt")
shirt.ShirtTemplate = clothing.Template
shirt.Parent = character
elseif clothing.Type == "Pants" then
local pants = Instance.new("Pants")
pants.PantsTemplate = clothing.Template
pants.Parent = character
end
end
print("[Server] Appearance applied for player:", player.Name)
end)
-- Scale handler
ApplyScaleEvent.OnServerEvent:Connect(function(player, scaleX, scaleY, scaleZ)
local character = player.Character
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Remove old scale NumberValues
for _, child in pairs(humanoid:GetChildren()) do
if child:IsA("NumberValue") and (child.Name == "BodyHeightScale" or child.Name == "BodyWidthScale" or child.Name == "BodyDepthScale") then
child:Destroy()
end
end
-- Create new scale NumberValues
local heightScale = Instance.new("NumberValue")
heightScale.Name = "BodyHeightScale"
heightScale.Value = scaleY
heightScale.Parent = humanoid
local widthScale = Instance.new("NumberValue")
widthScale.Name = "BodyWidthScale"
widthScale.Value = scaleX
widthScale.Parent = humanoid
local depthScale = Instance.new("NumberValue")
depthScale.Name = "BodyDepthScale"
depthScale.Value = scaleZ
depthScale.Parent = humanoid
print(string.format("[Server] Applied scale to %s: X=%.2f Y=%.2f Z=%.2f", player.Name, scaleX, scaleY, scaleZ))
end)
-- Hat handler
ApplyHatEvent.OnServerEvent:Connect(function(player, hatName)
local character = player.Character
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Remove all accessories first
clearAccessories(humanoid)
if not hatName or hatName == "" or hatName == "None" then
print("[Server] Removed hat for player:", player.Name)
return
end
local hatModel = HatsFolder:FindFirstChild(hatName)
if not hatModel then
warn("[Server] Hat not found: " .. hatName)
return
end
-- Clone and parent hat accessory
local hatClone = hatModel:Clone()
hatClone.Parent = character
print("[Server] Added hat for player:", player.Name, "Hat:", hatName)
end)
And for what it’s worth:
ViewportCloneManager
-- Stores the currently cloned character in the viewport
module.CurrentClone = nil
-- Zoom distance used by the camera
module.ZoomDistance = 7.5
-- Function to return the current clone
function module.GetClone()
return module.CurrentClone
end
ScaleHandler
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ApplyAppearance = ReplicatedStorage:WaitForChild("ApplyAppearance")
local ApplyScaleEvent = ReplicatedStorage:WaitForChild("ApplyScaleEvent")
-- Apply Appearance handler
ApplyAppearance.OnServerEvent:Connect(function(player, appearanceData)
local character = player.Character
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Remove existing clothing & accessories
for _, item in ipairs(character:GetChildren()) do
if item:IsA("Shirt") or item:IsA("Pants") then
item:Destroy()
end
end
for _, acc in ipairs(humanoid:GetAccessories()) do
acc:Destroy()
end
-- Apply Clothing
for _, clothing in ipairs(appearanceData.Clothing or {}) do
if clothing.Type == "Shirt" then
local shirt = Instance.new("Shirt")
shirt.ShirtTemplate = clothing.Template
shirt.Parent = character
elseif clothing.Type == "Pants" then
local pants = Instance.new("Pants")
pants.PantsTemplate = clothing.Template
pants.Parent = character
end
end
-- TODO: Apply accessories from name or assetId if you have a folder or insert logic here.
-- For now, skipping to keep the script focused.
print("[Server] Appearance applied for player:", player.Name)
end)
-- Apply Scale handler (using Humanoid BodyScale NumberValues)
ApplyScaleEvent.OnServerEvent:Connect(function(player, scaleX, scaleY, scaleZ)
local character = player.Character
if not character then return end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return end
-- Clear existing BodyScale values
for _, child in pairs(humanoid:GetChildren()) do
if child:IsA("NumberValue") and (child.Name == "BodyHeightScale" or child.Name == "BodyWidthScale" or child.Name == "BodyDepthScale") then
child:Destroy()
end
end
-- Create new BodyScale NumberValues
local heightScale = Instance.new("NumberValue")
heightScale.Name = "BodyHeightScale"
heightScale.Value = scaleY
heightScale.Parent = humanoid
local widthScale = Instance.new("NumberValue")
widthScale.Name = "BodyWidthScale"
widthScale.Value = scaleX
widthScale.Parent = humanoid
local depthScale = Instance.new("NumberValue")
depthScale.Name = "BodyDepthScale"
depthScale.Value = scaleZ
depthScale.Parent = humanoid
print(string.format("[Server] Applied scale to %s: X=%.2f Y=%.2f Z=%.2f", player.Name, scaleX, scaleY, scaleZ))
end)
MorphHandler
local Players = game:GetService("Players")
local applyAppearanceEvent = ReplicatedStorage:WaitForChild("ApplyAppearance")
local addHatEvent = ReplicatedStorage:WaitForChild("AddHatEvent")
local applyScaleEvent = ReplicatedStorage:WaitForChild("ApplyScaleEvent")
local AccessoriesFolder = ReplicatedStorage:WaitForChild("Accessories")
local function onApplyAppearance(player, appearanceData)
print("Applying appearance for player:", player.Name)
local character = player.Character
if not character or not character:FindFirstChild("Humanoid") then
warn("Player character or humanoid not found")
return
end
local humanoid = character.Humanoid
local humanoidDescription = Instance.new("HumanoidDescription")
if appearanceData then
if appearanceData.ShirtAssetId then
humanoidDescription.Shirt = appearanceData.ShirtAssetId
end
if appearanceData.PantsAssetId then
humanoidDescription.Pants = appearanceData.PantsAssetId
end
else
warn("appearanceData is nil")
end
-- Remove all existing accessories before applying new ones
for _, acc in ipairs(character:GetChildren()) do
if acc:IsA("Accessory") then
acc:Destroy()
end
end
-- Add accessories sent by client
if appearanceData and appearanceData.Accessories then
for _, accName in ipairs(appearanceData.Accessories) do
local accTemplate = AccessoriesFolder:FindFirstChild(accName)
if accTemplate then
local cloneAcc = accTemplate:Clone()
cloneAcc.Parent = character
else
warn("Accessory not found on server:", accName)
end
end
end
humanoid:ApplyDescription(humanoidDescription)
print("Appearance applied for player:", player.Name)
end
local function onAddHat(player, hatName)
print("Adding/removing hat for player:", player.Name, hatName or "None")
local character = player.Character
if not character then return end
-- Remove existing hats first
for _, acc in ipairs(character:GetChildren()) do
if acc:IsA("Accessory") and acc.Name == "Hat" then
acc:Destroy()
end
end
if hatName then
local hatTemplate = AccessoriesFolder:FindFirstChild(hatName)
if hatTemplate then
local cloneHat = hatTemplate:Clone()
cloneHat.Name = "Hat" -- standardize the name for easy removal later
cloneHat.Parent = character
print("Received hatName:", hatName, type(hatName))
else
warn("Hat accessory not found on server:", hatName)
end
end
end
local function applyScale(player, scaleData)
print("📏 Applying scale for player:", player.Name)
print("Scale data received:")
for k,v in pairs(scaleData) do
print(k,v,type(v))
end
local height = tonumber(scaleData.Height) or 1
local width = tonumber(scaleData.Width) or 1
local depth = tonumber(scaleData.Depth) or 1
-- sanity check:
if height <= 0 then height = 1 end
if width <= 0 then width = 1 end
if depth <= 0 then depth = 1 end
print(string.format("Scaling to Height=%.2f, Width=%.2f, Depth=%.2f", height, width, depth))
local character = player.Character
if not character then
warn("No character found for scaling", player.Name)
return
end
-- Now scale your character parts
for _, partName in ipairs({"Torso","LeftUpperArm","RightUpperArm","LeftLowerArm","RightLowerArm","LeftHand","RightHand","LeftUpperLeg","RightUpperLeg","LeftLowerLeg","RightLowerLeg","LeftFoot","RightFoot","Head"}) do
local part = character:FindFirstChild(partName)
if part and part:IsA("BasePart") then
part.Size = Vector3.new(
part.Size.X * width,
part.Size.Y * height,
part.Size.Z * depth
)
end
end
end
applyAppearanceEvent.OnServerEvent:Connect(onApplyAppearance)
addHatEvent.OnServerEvent:Connect(onAddHat)
applyScaleEvent.OnServerEvent:Connect(applyScale)
ScaleController
– ScaleController (ModuleScript)
local module = {}
– Default scale values
module.ScaleX = 1
module.ScaleY = 1
module.ScaleZ = 1
module.Clone = nil
module.Torso = nil
module.OriginalData = {}
function module.Initialize(clone)
– Always reinitialize on new bundle
module.Clone = clone
module.Torso = clone:FindFirstChild(“UpperTorso”) or clone:FindFirstChild(“Torso”)
module.OriginalData = {} – Clear previous part data
if not module.Torso then
warn("❌ Could not find torso on clone.")
return
end
for _, part in clone:GetDescendants() do
if part:IsA("BasePart") then
part.Anchored = true
local relativeCFrame = module.Torso.CFrame:ToObjectSpace(part.CFrame)
module.OriginalData[part] = {
Size = part.Size,
Offset = relativeCFrame
}
end
end
end
function module.ApplyScale()
for part, data in pairs(module.OriginalData) do
if part and part.Parent then
local scaleX = module.ScaleX
local scaleY = module.ScaleY
local scaleZ = module.ScaleZ
local originalSize = data.Size
local originalOffset = data.Offset
local rotX, rotY, rotZ = originalOffset:ToEulerAnglesXYZ()
local size, offsetPos
if part.Name == "Head" then
-- ✅ Scale head width but not height/depth
size = Vector3.new(
originalSize.X * scaleX,
originalSize.Y, -- locked height
originalSize.Z -- locked depth
)
-- ✅ Adjust head Y pos only if torso height changed
offsetPos = Vector3.new(
originalOffset.Position.X * scaleX,
originalOffset.Position.Y * scaleY,
originalOffset.Position.Z -- no scaling Z offset
)
else
size = Vector3.new(
originalSize.X * scaleX,
originalSize.Y * scaleY,
originalSize.Z * scaleZ
)
offsetPos = Vector3.new(
originalOffset.Position.X * scaleX,
originalOffset.Position.Y * scaleY,
originalOffset.Position.Z * scaleZ
)
end
local finalCFrame = module.Torso.CFrame * CFrame.new(offsetPos) * CFrame.Angles(rotX, rotY, rotZ)
part.Size = size
part.CFrame = finalCFrame
end
end
end
return module
I’ll be gone for an hour so if I don’t reply right away, its because I’m crying about this to my therapist lol