How would I go about making non-linear architecture for a tycoon

I’m sorry for asking some of the dumbest questions here, I just always get a brain clog whenever making something new from scratch.

So, in a severe simplificiation, I want to create a tycoon (one with buttons) that can branch in, and out. That can add and delete stuff once a button is pressed. Sometimes both at once.)

I imagine I’d need to keep the tycoon’s structure inside of some elaborate table, but how do I go about efficiently sorting through that table (to display new buttons)?

And how do I go about calculating the % of the tycoon that got completed?

If my explanation is vague or unclear, just mention it in the replies, I’m kind of tired at the time of writing this.

Side Note

Would be a good way if someone gave me a good way to save this efficiently and safely in a datastore, aswell as a way to efficiently load the progress after joining again.

3 Likes

Make a table of paths:

local Buttons = {
    Button1 = {
        Purchased = false
        Button1B = {Purchased = false, etc..}
        Button1C = {Purchased = false, etc..}
        Button1D = {Purchased = false, etc..}
    },
    Button2 = {
        Purchased = false
        Button2B = {Purchased = false, etc..}
        Button2C = {Purchased = false, etc..}
    },
    etc...
}

Then just save that table in the datastores. When the button is purchased, the Purchased variable will become true, then loop through the other buttons of the button that was purcahsed. For example, purchasing Button1 will unlock the 3 other buttons in that table, while purcasing Button2 will unlock the 2 other buttons in that table.

1 Like

And when loading the tycoon, do i just iterate through the entire table looking for the last Purchased = true? or would it be better to just save the last button’s name?

oh and I forgot - what if I want to branch out, but never back in? like this:

Yes, this is what you’ll have to do. Don’t worry about performance as you’re probably going to be doing this once per player anyway (not that it’s even performance intensive).

This wouldn’t be a problem. The problem would be branching back in, but you can just copy and paste that branch to that buttons table.

sorry, performance waste is just my pet peeve, lol. However, how do i find the next set of buttons to display after loading a tycoon?

I dont understand this either. I dont want to end up having to copy and paste tons of stuff over and over when I make 1 simple change. I want the system to be as automatic as possible.

This data structure is called a “directed acyclic graph” or DAG, just so you have something to google.

“Directed” because things depend on other things, but not necessarily the other way around, and “acyclic” because there are no cycles.

It’s the easiest type of graph to store and reason about, so that’s lucky. There are lots of algorithms and storage solutions out there.

@VegetationBush is one. I would probably formalize it a bit and use OOP to make every button an object. They can store both their parent (if applicable) and a list of their children. Expose methods like AddChild, IsAvailable, and Click.

Edit: Also I would give every button a unique ID and reference that in the list of children and parent. Then you can just store a flat list of buttons without worrying about duplication or anything.

1 Like

Loop through the table and find the buttons, or just wrap the buttons in a table and loop through that instead:

 Button1 = {
    Purchased = false
    Buttons = { -- Loop through these, and index buttons by name
        Button1B = {Purchased = false, etc..}
        Button1C = {Purchased = false, etc..}
        Button1D = {Purchased = false, etc..}
    }
}

Instead of having a table named Buttons for that button, have an index named Redirect and type int he hierarchy for the redirection:

Button1 = {
    Purchased = false
    Redirect = "Button2.Buttons.Button2B"
},
local Buttons = {
    Button1 = {
        Purchased = false
        Redirect = "Button2.Buttons.Button2B"
    Button2 = {
        Purchased = false
        Buttons = {
            Button2B = {Purchased = false, etc..}
            Button2C = {Purchased = false, etc..}
        },
    },
    etc...
}

local function redirectButton(redirect: string)
    local selected = Buttons
    for _, hierarchy in ipairs(string.split(redirect, ".")) do -- Splits the string, then loops through it
        selected = selected[hierarchy] -- Indexes the table based off of the string
    end
    return selected -- Returns the index of the table
end

--// Indexing the table
if button.Buttons then
    -- Stuff
else
    local newButton = redirectButton(button.Redirect)
end

and what if there are multiple redirects?

Thank you for clarifying the terminology. The OOP solution sounds appealing, could you clarify on the unique ID though?

You can put the different heirarchies in a table:

Redirect = {"Button2.Buttons.Button2A", "Button2.Buttons.Button2B"}

and edit the redirectButton function to account for tables from the Redirect index.

1 Like

I’m saying that one button object would have these fields, roughly:

  • purchased (bool)
  • id (string or number)
  • parent (some other button’s id or nil if root)
  • children (list of other ids)

All that matters is that id is different for every button. That means you could just make it a sequential number, or you could manually set them, or you could reuse the button’s text as it’s ID, or you could randomly create GUIDs for new buttons. Any is fine as long as they’re unique.

As a bonus, this lets you build a map from ids to actual objects when you first load the data:

buttonIdMap = {
  1 = <button object with id 1>,
  2 = <button object with id 2>,
  -- etc.
}

Then, say you’re looking at a button with id = 4 and want to check if it’s able to be clicked. So you check it’s parent, which lets say is “2”.

That’s just a number though. But you have the map! So you can get the actual object associated with that id very quickly with buttonIdMap[2].

I’m on mobile at the moment but I can give a slightly more fleshed out example later.

Edit: Also note that if the actual button structure doesn’t change, all you have to store in your data store is a list of ids that you purchased. You can hardcode all the actual structure data in your game, then it’s just a matter of filling in the user data on top of the structure data (which you already knew so didn’t need to store).

2 Likes

Interesting and helpful! I’m too tired right now to fully visualise how to put it all together, but if I’ll have any questions ill surely follow up tommorow!

What if I wanted to restructure the tycoon for an update, like add or remove an objects child? Change the parent?

Basically, how do I make this dynamically re-sort itself with every new server to ensure the table structures are up to date?

Additionally, to make each tycoon player-connected, do I make the player a class, or do I link the Button map to a player by for ex. a playerdata table.

This is a problem with all data storage :slight_smile:

One way:

When you’re storing the list of button IDs that a player has purchased, also store a version number that matches the version of the tree at the time the data was originally saved.

Every time you change the structure, say from v5 to v6, create a function whose only job is to convert v5 player data to v6 player data—maybe just by mapping the IDs to the most recent ones. This is an easier task if you’ve manually assigned them ids and they’re descriptive strings like "dropper-1".

Sometimes you’re data updater might be more complicated. For instance what if you decide that actually, you wanted players to purchase "dropper-1-a" and "dropper-1-b", and you remove "dropper-1" completely?

Now you’re function would do something like “if a v5 player’s data has "dropper-1", that now means that they’ve bought "dropper-1-a" and "dropper-1-b" in the v6 data, and we’ll give them both.

If a player joins and their data is like from v1, all you do do is run your version functions to bring them from v1->v2, then v2->v3, etc. all the way until you run v5->v6.

This would be a rule in your version updater function—you’ll have to decide what happens though. If they needed A to get B in v5, but you change it so they actually need C to get B in v6, do you just give them both C and B? Do you tell them that they no longer have B?

I would personally do this: if a player loads an old data version, and they have B, give them B and all of the parents of B for free. No need to punish a player because you changed your mind.

Not really sure what you mean, but I would heavily use OOP for this, yeah.

You don’t need to store the actual tree structure in the player data, just a list of which buttons they buy. The tree structure is part of your game data.

When they rejoin and load data or try to buy something new or even if you’re trying to show all the buttons, that’s when you walk up the tree to see if all the parents of a given button ID are also purchased.

By the way, I’m sure there are modules that people have made for a) creating and traversing graphs, and b) loading and saving versioned data based on per-version rules.

They’re pretty common problems, try to look up existing solutions and adapt them.

can you elaborate on that? Under which circumstance do I give player the parents? Deletion, addition, reposition?

When I say “parent” I really mean “the button or buttons that this button depends on”

That circumstance I’m talking about is if a player saves their data with one version, but loads it in a newer server after you’ve decided you want a new structure.

Are you talking about the structure changing on the fly? Because I’m talking about the structure being pre-set in the game, and only changing because you make an update.

I’ve been playing around with this a bit and have a rough sketch.

Trying to implement this graph:

image

Where to unlock B you need to unlock A, to unlock E you need to unlock B and C (and A), etc.

Stole most of the DAG code from ember.js’s implementation.

Just translated that into lua pretty much and put it into a module:

local DAG = {}
DAG.__index = DAG

type Vertex = {
	name: string,
	incoming: {Vertex},
	incomingNames: {string},
	hasOutgoing: boolean,
	value: any | nil
}

local function visit(vertex: Vertex, fn: (Vertex, {string}) -> nil, visited: {[string]: Vertex} | nil, path: {string})
	if not visited then
		visited = {}
	end

	if not path then
		path = {}
	end

	local name = vertex.name

	if visited[name] then
		return
	end

	-- push
	table.insert(path, name);

	visited[name] = true

	local names = vertex.incomingNames
	for i = 1, #names do
		visit(vertex.incoming[names[i]], fn, visited, path)
	end

	fn(vertex, path)

	-- pop
	table.remove(path, #path)
end

function DAG.new()
	local self = {
		names = {},
		vertices = {}
	}

	setmetatable(self, DAG)

	return self
end

function DAG:Add(name: string)
	if not name then return end
	if self.vertices[name] then
		return self.vertices[name]
	end

	local vertex = {
		name = name,
		incoming = {},
		incomingNames = {},
		hasOutgoing = false,
		value = nil
	}

	self.vertices[name] = vertex
	table.insert(self.names, name)
	return vertex
end

function DAG:Map(name: string, value: any | nil)
	self:Add(name).value = value
end

function DAG:AddEdge(fromName: string, toName: string)
	if not fromName or not toName or fromName == toName then
		return
	end

	local from = self:Add(fromName)
	local to = self:Add(toName)
	if to.incoming[fromName] then
		return
	end

	local function checkCycle(vertex, path)
		if vertex.name == toName then
			error("cycle detected: " .. toName .. " <- " .. table.concat(path, " <- "))
		end
	end

	visit(from, checkCycle)

	from.hasOutgoing = true
	to.incoming[fromName] = from
	table.insert(to.incomingNames, fromName)
end

function DAG:AddEdges(name, value, before: nil | string | {string}, after: nil | string | {string})
	self:Map(name, value)

	if before then
		if typeof(before) == "string" then
			self:AddEdge(name, before)
		else
			for i = 1, #before do
				self:AddEdge(name, before[i])
			end
		end
	end

	if after then
		if typeof(after) == "string" then
			self:AddEdge(after, name)
		else
			for i = 1, #after do
				self:AddEdge(after[i], name)
			end
		end
	end
end

return DAG

Then to test that module, I made some buttons:

image

Then I built the graph with the module:

local DAG = require(game.ReplicatedStorage.DAG)
local dag = DAG.new()

local screen = script.Parent

-- create vertices that map IDs to literally anything
-- in this case just little {gui, activated} tables
-- but you could use full OOP objects instead

dag:Map("a", {gui = screen.A, activated = false})
dag:Map("b", {gui = screen.B, activated = false})
dag:Map("c", {gui = screen.C, activated = false})
dag:Map("d", {gui = screen.D, activated = false})
dag:Map("e", {gui = screen.E, activated = false})
dag:Map("f", {gui = screen.F, activated = false})

-- set up their connections

dag:AddEdge("a", "b")
dag:AddEdge("a", "c")
dag:AddEdge("b", "c")
dag:AddEdge("b", "e")
dag:AddEdge("c", "e")
dag:AddEdge("c", "d")
dag:AddEdge("e", "f")
dag:AddEdge("f", "d")

Then I wrote a little visualization code:

seems right, cool

-- just for visualization:
local root2 = math.sqrt(2)
local function DrawArrow(from: Vector2, to: Vector2, color: Color3)
	color = color or Color3.fromHSV(math.random(), 1, 1)
	
	local diff = to - from
	local dist = diff.Magnitude
	local center = from + diff / 2
	local angle = math.atan2(diff.Y, diff.X)
	local angleDeg = math.deg(angle)
		
	local stem = Instance.new("Frame")
	stem.BorderSizePixel = 0
	stem.AnchorPoint = Vector2.new(0.5, 0.5)
	stem.Parent = script.Parent
	stem.Rotation = angleDeg
	stem.Position = UDim2.fromOffset(center.X, center.Y)
	stem.BackgroundColor3 = color
	stem.Size = UDim2.fromOffset(dist, 4)
	
	local arrSize = 20
	
	local arr = Instance.new("ImageLabel")
	arr.Image = "http://www.roblox.com/asset/?id=3186587094" -- right arrow in bottom left
	arr.BackgroundTransparency = 1
	arr.AnchorPoint = Vector2.new(0.5, 0.5)
	arr.Rotation = angleDeg - 135
	arr.ImageColor3 = color
	arr.Size = UDim2.fromOffset(arrSize, arrSize)
	
	local pos = from + diff.Unit * (dist - arrSize * root2 / 2)
	arr.Position = UDim2.fromOffset(pos.X, pos.Y)
	arr.Parent = script.Parent
end

-- loop through all vertices and show their connections

for name, vertex in pairs(dag.vertices) do
	local gui = vertex.value.gui
	
	print(name, "-> ", table.concat(vertex.incomingNames, ", "))
	
	for _, incoming in pairs(vertex.incoming) do
		local from = incoming.value.gui.AbsolutePosition + incoming.value.gui.AbsoluteSize / 2
		local to = gui.AbsolutePosition + gui.AbsoluteSize / 2
		
		DrawArrow(from, to)
	end
end

Finally it’s pretty simple to enforce things like “only allow something to be activated if all its ancestors are” with recursion:

for name, vertex in pairs(dag.vertices) do
	local gui = vertex.value.gui
	
	local function AreParentsActivated(child)
		for _, incoming in pairs(child.incoming) do
			if not incoming.value.activated or not AreParentsActivated(incoming) then
				return false
			end
		end
		
		return true
	end
	
	gui.MouseButton1Down:Connect(function()
		if vertex.value.activated then
			print(vertex.name .. " already activated")
			return
		end
		
		if not AreParentsActivated(vertex) then
			print(vertex.name .. "'s parents not activated")
			return
		end
		
		vertex.value.activated = true
		vertex.value.gui.BackgroundColor3 = Color3.new(0, 1, 0)
		print("activated " .. vertex.name)
	end)
end

Dunno how useful any of that is to you—it doesn’t address anything related to saving/loading data, just the tree structure itself. But I was bored so gave it a shot :slight_smile:

10 Likes

Do you think having the player as a class for the button objects is a bad solution?

Or is it better to just make a central table storing all the players, then storing all the objects inside of that players player table?

Do you mean for keeping track of which players have bought which items?

I dunno, there’s lots of ways. There’s no bad solutions.

I would probably have some kind of centralized list on the server, yeah, but I don’t know. It depends on what you wanna do.