Code Review | WIP Modular crop system using too much memory

Hello! I am pretty new to Roblox Lua but I wanted to make a modular crop registry so eventually I could make some sort of farming game where you could place different types of crops.
My main issue is that it’s using ~1.5 gigabytes when it plants and grows, I am looking for any improvements or ideas on a better way of having tons of crops grow.
I was originally planning on them having their own callback inside themselves to grow after using task.wait until they are fully grown, but I assumed having a global loop for a table to check the grow times would be more optimized. Like I said, very new to Roblox Lua so please tell me if I am doing something wrong and optimized!

If you want a basic rundown I can use this to register a crop in the table:

CropSystem:RegisterCrop(
    'Pumpkin',
    {
        workspace.Models.Pumpkin.PumpkinStage1,
        workspace.Models.Pumpkin.PumpkinStage2,
        workspace.Models.Pumpkin.PumpkinStage3,
        workspace.Models.Pumpkin.PumpkinStage4,
        workspace.Models.Pumpkin.PumpkinStage5,
        workspace.Models.Pumpkin.PumpkinStage6,
        workspace.Models.Pumpkin.PumpkinStage7,
        workspace.Models.Pumpkin.PumpkinStage8,
        workspace.Models.Pumpkin.PumpkinStage9
    },
    20,
    nil,
    true,
    1
)

I don’t have much set up but I just made a for loop to just place crops and register them as planted crops:

local spacing = 5
local totalCrops = 500
local cropsPlanted = 0
local rows = math.ceil(math.sqrt(totalCrops))
local columns = math.ceil(totalCrops / rows)

for row = 1, rows do
    for col = 1, columns do
        if cropsPlanted >= totalCrops then
            break
        end

        local position = Vector3.new(row * spacing, 0, col * spacing)
        CropSystem:PlantCrop('Pumpkin', position)
        cropsPlanted += 1

        if cropsPlanted % math.random(5, 6) == 0 then
            task.wait(math.random(0.1 * 10, 0.5 * 10) / 10)
        end
    end
end

Shared file is below:

local CropSystem = {}

CropSystem.Crops = {}

CropSystem.PlantedCrops = {}

function CropSystem:RegisterCrop(name, models, growTime, harvestTime, removeOnHarvest, cubeSize)
    print('Registering crop:', name)
    self.Crops[name] = {
        Models = models,
        GrowTime = growTime,
        HarvestTime = harvestTime or 0,
        RemoveOnHarvest = removeOnHarvest,
        CubeSize = cubeSize,
    }
end

function CropSystem:PlantCrop(cropName, position)
    print('Planting crop:', cropName, 'at position:', position)
    local cropData = self.Crops[cropName]
    if not cropData then
        warn('Crop not registered:', cropName)
        return
    end

    local cropInstance = Instance.new('Model')
    cropInstance.Name = cropName
    cropInstance:SetAttribute('Stage', 1)
    cropInstance:SetAttribute('GrowthTimeElapsed', 0)
    cropInstance.Parent = workspace

    local firstStageModel = cropData.Models[1]
    for _, child in ipairs(firstStageModel:GetChildren()) do
        local clonedChild = child:Clone()
        clonedChild.Parent = cropInstance

        if clonedChild:IsA('BasePart') then
            clonedChild.Anchored = true
        end
    end

    if firstStageModel.PrimaryPart then
        local clonedPrimaryPart = firstStageModel.PrimaryPart:Clone()
        clonedPrimaryPart.Parent = cropInstance
        clonedPrimaryPart.Anchored = true
        cropInstance.PrimaryPart = clonedPrimaryPart
    else
        warn('First stage model does not have a PrimaryPart. Crop placement may fail.')
    end

    if cropInstance.PrimaryPart then
        cropInstance:SetPrimaryPartCFrame(CFrame.new(position))
    else
        warn('Failed to set PrimaryPart for cropInstance. Cannot set CFrame.')
    end

    table.insert(self.PlantedCrops, {
        Instance = cropInstance,
        CropName = cropName,
    })
end

function CropSystem:Tick()
    for i = #self.PlantedCrops, 1, -1 do
        local plantedCrop = self.PlantedCrops[i]
        local cropInstance = plantedCrop.Instance

        if not cropInstance or not cropInstance.Parent then
            table.remove(self.PlantedCrops, i)
            continue
        end

        local cropName = plantedCrop.CropName
        local cropData = self.Crops[cropName]
        if not cropData then
            table.remove(self.PlantedCrops, i)
            continue
        end

        local elapsedTime = cropInstance:GetAttribute('GrowthTimeElapsed') or 0
        elapsedTime += 1
        cropInstance:SetAttribute('GrowthTimeElapsed', elapsedTime)

        local totalStages = #cropData.Models
        local stageDuration = cropData.GrowTime / totalStages
        local currentStage = math.min(math.floor(elapsedTime / stageDuration) + 1, totalStages)

        if cropInstance:GetAttribute('Stage') ~= currentStage then
            cropInstance:SetAttribute('Stage', currentStage)
            print('Updating crop:', cropName, 'to stage:', currentStage)

            local currentPosition = cropInstance.PrimaryPart and cropInstance.PrimaryPart.CFrame or CFrame.new()
            cropInstance:ClearAllChildren()
            local newModel = cropData.Models[currentStage]
            if newModel then
                local clonedModel = newModel:Clone()
                clonedModel.Parent = cropInstance

                if clonedModel.PrimaryPart then
                    cropInstance.PrimaryPart = clonedModel.PrimaryPart
                    cropInstance:SetPrimaryPartCFrame(currentPosition)
                else
                    warn('New model does not have a PrimaryPart. Position may not be preserved.')
                end
            else
                warn('Model for stage', currentStage, 'not found for crop:', cropName)
            end
        end

        if elapsedTime >= cropData.GrowTime then
            if cropData.RemoveOnHarvest then
                print('Crop is ready for harvest or removal:', cropName)
                cropInstance:Destroy()
            else
                -- Handle multiple harvest logic here
            end

            table.remove(self.PlantedCrops, i)
        end
    end
end

task.spawn(function()
    while true do
        CropSystem:Tick()
        task.wait(1)
    end
end)

return CropSystem
1 Like

This looks really solid! The only thing I think is unnecessary is saving the instance of the model and the name of it inside of planted crops. If you made the CropName an attribute of the plantedCrop then you could just do

table.insert(self.PlantedCrops, cropInstance)

And local cropName = plantedCrop.CropName

Other than that it looks amazing. (But im kinda curious, are there any performance issues with you looping through up to 500 crops in a frame? I dont think that would be bad for memory but it does seem like a lot to do without a task.wait() every 50-100 plants or so)

Thanks! Did the change you suggested, and yes it does seem to have performance issues at high counts. It peaks at about 350Mb and that is with 500 crops placed and growing at the same time. I am unsure about how Roblox works so I have no clue about the memory garbage collection, but I assume just the base game takes a specific amount, so once i figure that out I can figure out how much impact the crops have.

Like I said in my first paragraph, my original idea was adding callbacks for each crop and have them utilize task.wait, however I assumed that would lead to thousands of what I assume are the equivalent of timers being ran all at the same time. I might test this and see, but I also am unsure about the table and if I should use ipairs or something else (I am used to LuaJIT, so its a good amount of changes).