How would I properly script a Submit button for my Character Creation GUI?

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.

Current Hierarchy


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)
:arrows_counterclockwise: Always reinitialize on new bundle
module.Clone = clone
module.Torso = clone:FindFirstChild(“UpperTorso”) or clone:FindFirstChild(“Torso”)
module.OriginalData = {} – :boom: 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

Don’t cry now! It’s okay to be stuck on something.

You’re right that anything deposited into replicated storage on the client side, isn’t going to update the server.
This is usually just the nature of client-server architectures.

Have you tried using a remote event or a remote function to pass information across the client server boundary?
You can try passing the different custom values that you player has choosen to the server, and then have a dedicated script in ServerScriptService that then takes these values and updates the player on the server side.

2 Likes

That’s kind of what I was attempting to do, but somewhere along the way I got lost and instead of getting morphed into my clone, I was instead morphed into a black head with no body rolling on the ground lol. In other instances, either nothing happened, or the body disappeared and I’d be a stuck floating head with no movement.

Bumping for a good cause <3 Still struggling

Fundamentally the easiest solution would be to simply pass a list of all changes that need to be applied. So first step would be to write functions on the server that can make any valid changes to a players character that can be made with the clients scripts. This would be something like applying hair, changing outfits, recoloring hair or body parts or whatever else you have.

Once you have the server sided functions made, you simply hook up the client and pass it commands that you are issuing to the server. Like ChangeHair, idOrName and let the server handle that.

So ignoring the client for now just make the server side stuff work with some test cases for each type. Then come back and add the client.

1 Like

For people searching for an answer like I was, I’m currently going through and making sure each piece works by itself before doing what other’s have suggested; I’ll update again once I’m through with that!

ETA: By piece, I mean I’m doing JUST face, JUST hair, JUST clothing, JUST bundles, and so on.