Thoughts on tree generation

In my game, players can place saplings, that will eventually grow into trees. However, I’m stuck on how to do this efficiently. Brief overview, when a player places a block, I store it in a module, based on the items coordinates. This allows me to go through the module and see what blocks are in what spaces.

Noticeable problems I’m struggling to find solutions to are updating the PlayerCoords module when player has left, or destroyed said sapling. I can’t do like .Changed on an item inside a table. I also don’t think I want to use SetAttribute() every second (seems like it’d create lag?)

But my original idea was to indicate on the block the time it was planted (os.time()) and how long left for it to grow. That way if they leave, I could do CurrentOsTime - PlantedOsTime = TimeLeft

function Tree.Planted(player, sapling, x, y, z)
	local PlayersCoords = IslandCoords[player.UserId]
	if not PlayersCoords then return end
	
	local Block = PlayersCoords[x][y][z]
	
	local TimeLeft = GrowTime
	
	sapling:SetAttribute("TimeLeft", GrowTime)
	sapling:SetAttribute("Planted", os.time())
	
	coroutine.wrap(function()
		while wait(1) do
			if not player or not player.Parent then break end -- Player left
			
			if PlayersCoords[x][y][z] ~= Block then break end -- Block has changed
			
			if TimeLeft <= 0 then break end -- End of time
			
			TimeLeft -= 1
		end
		
		sapling:SetAttribute("TimeLeft", TimeLeft)
		
		if TimeLeft <= 0 then -- Finished growing
			PlayersCoords[x][y][z] = "" -- Empty
			
			-- Clone tree objcet
			
			-- Parent tree object to island
			
			-- Add coords
		end
	end)()
end
2 Likes

This is almost always why I have the client count down instead of the server. The server’s responsibility would be only to track when the action began and check every frame if the given timeframe has elapsed or not before proceeding to the next action. The client’s would be, every frame, to decrement the counter. I mean at that point you can still decrement from the server though, lol.

SetAttribute every second won’t cause any lag. Feel free to try to repro that - I did so too. I used Heartbeat as well so the frequency is faster: if the time it takes for SetAttribute in Heartbeat is small, then absolutely it’d be safe every ~1 second. If your original implementation involved SetAttribute every second and it worked better to your tastes, go right ahead and try doing so.

local hb = game:GetService("RunService").Heartbeat

local its = 0
local targ_its = 100
local rng = Random.new()
local con

local times = table.create(targ_its)

con = hb:Connect(function (deltaTime)
	if its >= targ_its then con:Disconnect() con = nil end
	
	its += 1
	
	local timenow = os.clock()
	workspace:SetAttribute("foobar", rng:NextNumber())
	table.insert(times, os.clock() - timenow)
end)

while con do
	hb:Wait()
end

print("min time to setattribute for 100 its:", math.min(table.unpack(times)))
print("max time to setattribute for 100 its:", math.max(table.unpack(times)))

Results (obviously variable each time):

min time to setattribute for 100 its: 1.5199999324977e-05
max time to setattribute for 100 its: 8.0500001786277e-05

Takes no time at all for a SetAttribute call.

1 Like

So what I am doing is fine so far?? I still struggle to comprehend how I could update the stored id when player has left or they destroy the block.

My ids are stored like so

Cords = {
    [x] = {
        [y] = {
            [z] = "1#240"
        }
    }
}

So the idea here is that “1” is the blocks id, and then “#240” is the time left for the sapling to grow. So when the player leaves, I’d need to go through their entire table looking for any “#” and then adjust that number, based on the saplings time left to grow?

I think your idea of storing the time planted is a good one (but I would use os.time("!*t") on the server to make sure you’re using UTC time).

When a sapling is planted, store the time planted.

Have a single .Stepped event loop on the server that loops through all the saplings currently on the island and checks which ones are ready to grow. If you want you can do a while loop with a longer wait instead of binding to frames, but I don’t think it matters.

Some pseudocode:

local saplings = {}

-- when planted:
local sapling = {
  position = Vector3.new(...),
  plantTimestamp = os.time(os.date("!*t")),
  timeToGrow = 60 -- seconds
}
table.insert(saplings, sapling)

-- checking
game:GetService("RunService").Stepped:Connect(function()
  local now = os.time(os.date("!*t"))
  for _, sapling in pairs(PlayerModule:GetSaplingsList()) do
    if os.difftime(now, sapling.plantTimestamp) >= sapling.timeToGrow then
      -- do sapling growth stuff
      -- remove sapling from table or whatever you use (can't do
      -- directly in this loop so figure something out :) )
    end
  end
end)
1 Like

Sorry, just wanted to address your concern of using SetAttribute every second which wouldn’t be a problem. This is assuming that you’re updating the stored id because you didn’t want to use SetAttribute every second, as well as assuming that if you went with the attribute idea then you could successfully calculate CurrentTime - PlantedTime = TimeLeft without needing to go through the hassle of updating the stored id or searching for it when the player leaves (not sure if this part is required or not?).

I’m not sure I completely comprehend the problem so I can’t tell if you’re on the right track with what you’re doing specifically right now as well as if that’s a result of concerns arising from calling SetAttribute every second or not.

1 Like

It seems like your question is actually more of a data structure question now that I think about it.

I see that you’re trying to get away with only storing everything in a grid, and only doing operations on that grid.

I’m guessing you’re inspired by minecraft.

Minecraft tree growth works slightly differently to what you’re envisioning:

All trees in the active chunk radius around the player make attempts to grow at random intervals. For any given tree this can work out to about 3 growth attempts per minute.

In other words, minecraft doesn’t set a time-to-grow, just a random growth chance that it applies to saplings every now and then. Maybe you want to do a similar thing?

If you do want to stick to your time-to-grow mechanism, I recommend you also take inspiration from minecraft. See the chunk file format page for block entities.

Basically, in addition to the grid-based storage that says "this specific position has a block with ID “sapling” (+ maybe some basic metadata), some blocks need more info still. So, when they’re created, the game also creates a new entity (not inside the grid system, just an object) that is associated with that block.

Look at something like a furnace on that chunk format page, which has to care about all sorts of stuff besides just looking like a furnace—how long has it been cooking, what items are in it, etc:

image

Again, that BurnTime and CookTime and CookTimeTotal aren’t stored in the chunk’s block grid array (the [x][y][z] table in your case). They are stored in their own entity, that is merely associated with some block.

All those entities can be stored in a big list, so when you want to find all the furnaces (or saplings), minecraft (you) don’t loop through every block and check if it’s a furnace—you just look at the furnace entities and update those.

Keeping the block grid/block entity lists in sync is crucial—when you add a sapling, make sure it gets a block entity too. When you delete a sapling, delete the entity.

Fun fact: Minecraft had/has trouble with this. If a block has a block entity in the Java edition, it can’t be pushed by a piston (presumably because it’s too much work to make sure the grid and block entity components are kept in sync when it moves).

There’s a lot more inspiration to be found on that chunk format page, I think.

1 Like

This has helped a lot, thank you!! :smiley:

So I should store all blocks in their own modules, and then handle information relating to certain blocks in a seperate module?? And carry over their position so I can go between both modules easily??

Yeah, more or less I think.

First, have your 3D grid of block IDs as normal.

Separately, have a list of block entities.

A block entity can just be a table of properties like

local saplingEntity = {
  growTime = -- ...
}

You’ll need a list of those.

Now how to associate an entity with a block in a position… not sure. You could add the block position in that saplingEntity table, then you could go from entity → block but not the reverse.

You could also have two data structures, one that’s just a list of entities for iteration, and one that’s a 3D grid (with weak keys) so you can go from the block → entity as well.

There may be better ways to associate blocks with entities, but that’s my first pass.

1 Like

Could I turn the coordinates into a stringed value, and set that as they key??

For example:

Metadata[player.UserId] = {
     ["Saplings"] = {
        ["0,2,10"] = {
            Planted = os.time(), -- Time the sapling was planted
		    TimeToGrow = 4 * 60, -- How long it takes for sapling to fully grow
        },
        ["-5,-2,10"] = {
            Planted = os.time(), -- Time the sapling was planted
		    TimeToGrow = 4 * 60, -- How long it takes for sapling to fully grow
        },
    }
}

If you wanted, sure. You’re signing up to do a string concatenation every time you want to access the key though. You could also give each coordinate a single position number, like increasing by row then column then height.