How to make your character grow when you eat something in Roblox Studio
Touch a part, get bigger, and have it count on the leaderboard. Server-side so it cannot be faked, with the R6 case that makes most versions of this script hang forever and never grow anybody.
Written 11 August 2026 · about 10 minutes · every script here was written against Roblox Studio as it shipped that week
What you are building
Parts scattered around the map. Walk into one, it disappears, your character gets measurably bigger, and a counter on the leaderboard goes up. It is the core loop behind a whole genre on Roblox, and it is about sixty lines.
Everything below goes in a Script in ServerScriptService, not a LocalScript. That is not a style preference. If the client decides how big it is, then a player with an exploit executor decides how big they are, and on a game where size is the score that is the entire game. The server owns the number; the client just sees the result.
The whole script
It expects a Folder called Food in Workspace with some parts in it. If you do not have one, the second script further down makes one for you.
-- ServerScriptService > EatToGrow (a Script, NOT a LocalScript)local Players = game:GetService("Players")local ServerStorage = game:GetService("ServerStorage")local GROWTH_PER_FOOD = 0.06 -- 6% taller and wider per item eatenlocal MAX_SCALE = 4 -- stop growing here. See the note on clipping.local RESPAWN_AFTER = 8 -- seconds before an eaten item comes backlocal SPEED_PER_SCALE = 4 -- extra WalkSpeed per 1.0 of scalelocal foodFolder = workspace:WaitForChild("Food")-- The four scale values live on the Humanoid as NumberValue INSTANCES, not as-- plain properties, so they are read and written through .Value. They exist on-- R15 rigs only — see the R6 note below, which is the whole reason this-- function returns nil instead of assuming.local function getScales(humanoid)local height = humanoid:FindFirstChild("BodyHeightScale")local width = humanoid:FindFirstChild("BodyWidthScale")local depth = humanoid:FindFirstChild("BodyDepthScale")local head = humanoid:FindFirstChild("HeadScale")if not (height and width and depth and head) thenreturn nilendreturn { height = height, width = width, depth = depth, head = head }endlocal function grow(player, character)local humanoid = character:FindFirstChildOfClass("Humanoid")if not humanoid or humanoid.Health <= 0 then return endlocal scales = getScales(humanoid)if not scales then-- R6. Nothing to scale. Warn once rather than failing silently, because-- "the script runs, no errors, nobody grows" is the hardest version of-- this bug to find.warn("[EatToGrow] " .. player.Name .. " is on an R6 rig — cannot scale. See the R6 note.")returnendlocal next = math.min(scales.height.Value + GROWTH_PER_FOOD, MAX_SCALE)scales.height.Value = nextscales.width.Value = nextscales.depth.Value = nextscales.head.Value = next-- A character four times the size moving at the default 16 studs/second-- reads as slow motion, because the eye judges speed against body length,-- not against studs. Scaling WalkSpeed with size is what makes growth feel-- like power instead of like a penalty.humanoid.WalkSpeed = 16 + (next - 1) * SPEED_PER_SCALElocal stats = player:FindFirstChild("leaderstats")local eaten = stats and stats:FindFirstChild("Eaten")if eaten theneaten.Value += 1endendlocal function eat(player, food)-- THE DEBOUNCE, and it has to be an attribute on the part rather than a-- local variable. Touched fires many times for one contact, and the next-- one can arrive before Destroy() has finished, so without this a single-- item is eaten three or four times.if food:GetAttribute("Eaten") then return endfood:SetAttribute("Eaten", true)local position = food.CFramelocal template = food:Clone()food:Destroy()grow(player, player.Character)task.delay(RESPAWN_AFTER, function()template:SetAttribute("Eaten", nil)template.CFrame = positiontemplate.Parent = foodFolderend)endlocal function armFood(food)if not food:IsA("BasePart") then return endfood.CanTouch = truefood.Touched:Connect(function(hit)local character = hit:FindFirstAncestorOfClass("Model")if not character then return endlocal player = Players:GetPlayerFromCharacter(character)if not player then return endeat(player, food)end)endfor _, food in ipairs(foodFolder:GetChildren()) doarmFood(food)endfoodFolder.ChildAdded:Connect(armFood)Players.PlayerAdded:Connect(function(player)local stats = Instance.new("Folder")stats.Name = "leaderstats"stats.Parent = playerlocal eaten = Instance.new("IntValue")eaten.Name = "Eaten"eaten.Parent = stats-- Respawning gives you a brand new character at scale 1. Decide on purpose-- which game you are making: reset the counter here to make death cost-- everything, or re-apply the old scale to make it a save file. This does-- the first, because it is the one that makes the loop a game.player.CharacterAdded:Connect(function()eaten.Value = 0end)end)
The bug that stops most versions of this working
On an R6 character, none of the scale values exist
Roblox characters come in two rigs. R15 has BodyHeightScale, BodyWidthScale, BodyDepthScale and HeadScale sitting under the Humanoid as NumberValue objects. R6 has none of them. There is no property to set and no supported way to scale an R6 character — it is six fixed parts, and resizing them by hand detaches the Motor6D joints and breaks the animations.
The reason this wastes an evening rather than a minute is what the usual code does about it. Almost every version of this script online reaches for the value with humanoid:WaitForChild("BodyHeightScale"). On R15 that returns instantly. On R6 it never returns — WaitForChild yields forever for something that is never coming, so the script does not error, does not print, and does not grow anybody. You get a silent, permanent hang with a green output window.
The script above uses FindFirstChild and checks, so R6 gets a warning naming the problem instead of nothing at all. To actually fix it, set the rig type: Game Settings → Avatar → Rig Type → R15. Players already in the game keep their old rig until they respawn.
The other three things that will bite you
Touched fires far more than once
A single walk-through of one part can raise Touched five or six times, once per limb that brushes it, and they arrive faster than Destroy() completes. Without a guard, one item feeds you five times.
The guard is an attribute on the part rather than a variable in the script, because the thing being debounced is the part, not the script — a table of [part] = true works too but has to be cleaned up when the part dies, and an attribute goes away with it.
Past about 4x, the character starts clipping through the floor
MAX_SCALE is not timidity. Scaling the Humanoid scales the collision parts with it, but a very large character has a HipHeight the engine did not plan for, and the symptoms — feet sinking into the baseplate, falling through a thin platform, getting wedged in a doorway that was fine a moment ago — all start showing up somewhere around 4x on default terrain. Raise it if your map is built for it; test it before you ship it.
Anything the client can see, the client can lie about
This script is server-side, so the size is honest. But the moment you add a clienteffect — a size-based camera pull-back, a UI that shows your scale — do not let that client value feed back into anything the server trusts. Read the Humanoid's scale on the client, never send a number up and have the server believe it.
If you do not have food parts yet
Run this once and you have forty glowing spheres to walk into. It creates the Food folder the main script is looking for.
-- ServerScriptService > ScatterFood (a Script)-- Optional. Only needed if you do not want to place the parts by hand.local COUNT = 40local AREA = 120 -- studs, centred on the originlocal folder = Instance.new("Folder")folder.Name = "Food"folder.Parent = workspacefor i = 1, COUNT dolocal food = Instance.new("Part")food.Name = "Food"food.Shape = Enum.PartType.Ballfood.Size = Vector3.new(2, 2, 2)food.Color = Color3.fromRGB(255, 176, 46)food.Material = Enum.Material.Neonfood.Anchored = truefood.CanCollide = falsefood.Position = Vector3.new(math.random(-AREA, AREA),3,math.random(-AREA, AREA))food.Parent = folderend
Make them spin and it reads as collectible rather than as scenery — though this one costs you something, which is why it is separate:
-- Put this INSIDE the loop in ScatterFood, just before food.Parent = folder.local spin = Instance.new("BodyAngularVelocity")spin.AngularVelocity = Vector3.new(0, 3, 0)spin.MaxTorque = Vector3.new(0, math.huge, 0)spin.P = math.hugespin.Parent = food-- Anchored parts ignore BodyMovers, so if you want the spin you have to give-- up Anchored = true and rely on CanCollide = false plus a high position to-- keep them off the floor. It is a real trade, not a free effect.
Where to take it next
The loop above is complete but it is not yet a game, because nothing is at stake. The usual next step is making other players edible: check both characters' scales on contact, and let the larger one absorb the smaller. That is the same Touched handler with a comparison in it, plus a decision about what happens to the loser.
The honest warning about that step, since this page has been honest so far: player-versus-player size logic is where this genre gets genuinely hard. Two characters touch each other simultaneously, both handlers fire, and if you have not decided which one resolves first you get two winners, or two losers, or a size that doubles. Pick one authority — usually the lower UserId, or a single server-side queue — before you write it.
Was this the thing you searched for?
If what you actually wanted was "the character grows over time" rather than on contact, none of the Touched handling above applies and the whole thing is a while loop with a task.wait in it, incrementing the same four scale values. The R6 problem and the clipping ceiling still apply, and they are still the two things that will actually stop you.