OOPing Guidelines

I’m OOPing my code,

does literally everything that deals with tables have to be a separate object? What are the general guidelines on deciding whether something should be an object or not?

1 Like

What exactly are your ideas? What do you plan to do? There’s specifics for OOP and need to know if your ideas match with them.

1 Like

I’m basically asking what’s best practice.

function Objective.new()
	local self = {}
	self.fillPoints = {}
	
	self.fillPoints["fuelTank"] = {
		requiredItems = {
			["jerryCan"] = {
				model = game.ReplicatedStorage.JerryCan,
				count = 0,
				required = 4,
			}
		}
	}
	
	return self
end

should I create a new module script for the fuelTank fill point as an object with it’s own constructor and everything?

Should I go even further and make jerryCan it’s own module script as an object?

1 Like

I don’t see an issue with doing it that way. I wouldn’t necessarily make an entire object for a jerryCan as it can be included within the fuelTank as a necessity item.

1 Like

At what point should I make things a separate object then?

Note that I also have callback functions.

function Objective.new()
	local self = {}
	self.fillPoints = {}
	
	self.fillPoints["fuelTank"] = {
		requiredItems = {
			["jerryCan"] = {
				model = game.ReplicatedStorage.JerryCan,
				count = 0,
				required = 4,
			}
		},
        callBack = function() --gets called when something happens with fuelTank
            --do something
        end
	}
	
	return self
end

I would do it for objects which are created at different times and that have different methods (and therefore different functionality)

I don’t think making fuel tank a separate object is a good idea. Instead, you could set it as a metatable with its own methods (but keep it as a property of the objective thing). You can also give fuel tank a parent property to immediately access the ‘objective’.

2 Likes

In theory, anything that can be categorized as a noun (a thing) in your code is a candidate for being an object. In practice, you’ll bundle related parts into one object for simplicity unless there is a need within your code to treat a part as a separate thing (it needs it’s own separate state and actions).

Something like a jerry can that operates in the world as it’s own thing is a valid candidate. The nozzle for the can, any caps/lids, a fill level indicator (or whatever), other parts that do something simple and germane to the “jerry can” concept could all be considered as part of the same object unless there are compelling reasons to treat some of these as separate things. There’s a bit of subjectivity/art in where you separate related components into different objects, and quite a lot depends on what you are trying to achieve. Naturally, it will be easier to figure out where to draw lines for some things than others.

This info is wrong if a can belongs to a fill point rather than the other way around

Separating the fuel tank fill point into it’s own thing seems unnecessary. That functionality is probably fairly simple and it’s closely tied to what a jerry can is. The state of a fill point seems like it could be part of the can object. If you did decide to make it a separate object, then you would do well to look into an OOP concept called composition. The basic idea is that you make one object an attribute of another and let the attached object handle things it needs to do for itself. If you are planning to use that fuel tank fill point code in objects other than the jerry can (maybe it’s a complex and sophisticated implementation or something), then that might be a reason to go that route.

This page, this page, and this post are helpful Lua/Roblox OOP resources if you haven’t seen them. Here’s a stackoverflow discussion on what should be objects and a list of OOP best practices.

I think OOP is something you can ease into. Bundling state and methods together can provide immediate benefit. Don’t fret over best practices too soon (developing expert knowledge of all that stuff is not a trivial undertaking). Let it be an evolution.

2 Likes

So let’s say all fill points and items have the same general functionality, with the exception of the callback being differently depending on which fill point it is

Should I just keep it simple and have all of this done all in the Objective object

Well, I’m not entirely certain what you mean by fill points, so I’m tip-toeing around a little bit here. Is this a location where fuel tanks are stored, or is this a level indicator on a can (or something else entirely)? It looks like the former… so the cans are located at a fill point and fill points are part of an Objective?

If that’s the case, then for an OOP approach you would look for the objects in that scenario. A Jerry Can would probably be an object. The fill point (if this is a kind of filling station) and the Objective itself could also be objects. What you would do is look at those and figure out what state and actions you need for each. A Jerry can may have a model associated with it, a location, values for the amount of fuel is has and can have. Those are all attributes of the Jerry can (stored in a table that provides the foundation for the object). The can object may also have actions (methods) associated with it. Maybe an effect is triggered when the fuel is being poured out or refilled. It could have methods used for setting and getting the attributes.

The fill point would have it’s own state separate and apart from any cans it held. Maybe it has a point value associated with it, or it tracks the number of fuel cans, or who knows. There may be actions associated with the fill point like restocking empty cans or triggering lights to come on when it gets dark for example. Things which are not part of what an individual can would handle. The Objective would maybe have a number of these fill points in addition to a state of it’s own and methods that are specific to what the Objective needs to manage in your code.

If you are talking about something like that, then no, you wouldn’t want to do all that in an Objective class. You would maybe opt for something like composition where an Objective class has fill points as an attribute (or a collection object that managed fill points). These in turn may have a similar relationship to individual cans. Trying to be specific without really understanding where you’re trying to go with this is probably not very helpful.

1 Like

How you OOP depends on how your game is set up.

For example, in my open-source DCC system I recently published, I have a hybrid of traditional OOP, in-situ scripts, and traditional roblox ModuleScripts.

You can check it out here: https://www.roblox.com/library/2871454501/Ro-Scale-Coal-Mine-Starter-Set

1 Like

There’s not a quick, easy answer to this. I would say just start writing your game and let intuition be your guide, rather than hoping to be able to design the ideal OOP architecture for your game up-front, on the first go (that is genuinely pointless IMO). Start simple, and as you increase the number of items your generic object is trying to represent, let necessity be the mother of invention, as they say. There will be clues and “a ha” moments that suggest when and where it will make sense to use separate classes, or to use composition instead of inheritance, etc.

For example, suppose you start with a generic class for “inventory item” which represents anything you can pick up in the world and carry with you: a gun, an apple, a hat. As you add more types of things to your game–different types of guns, food, clothing–it will quickly become apparent that one generic item class is not ideal: food items might have a property for how much hunger they replenish, which makes no sense for a gun or hat. You don’t want one bloated class with all the properties and functions that any item might ever have, as this is just a waste of memory. It suggests that it probably makes sense to have distinct, base classes for guns, food items, and clothing items respectively.

These could all be derived from the base inventory item class, but, as the needs of the items become more fleshed out and specialized, you may find that these three classes might not have enough in common for even this to make sense. Consider this: you might introduce a gun that has nearly all of the functionality of the guns you can carry in your inventory, but can’t actually be carried in your inventory; for example, a .50 caliber weapon you can fire and reload, but which is permanently mounted on something. Here it becomes obvious how multiple inheritance became a thing in OOP: Your .50 cal and pistol might both derive from a common gun class, but only the pistol might also derive from inventory item.

That said, true multiple inheritance (a class deriving from multiple classes as in C++) is not available in a lot of programming languages, and not something I’d even recommend trying to implement in Lua, but there is another related and very popular multiple-inheritance concept worth knowing about: the Interface. If direct inheritance is an “is a” relationship, and composition is an “has a” relationship, then I think interfaces could be thought of as a “can be used as a” sort of deal, an abstraction. An interface is like an abstract base class with only virtual methods, no data members or implementation. So, going back to the original example: guns, foods, and clothes might all be top-level base classes in your game, but you might also have the concept of an inventory item interface. In most OOP languages, interfaces are conventionally named starting with capital I, like “ISortable”, or “IEdible”, and authored just like classes. Pistols, apples, and baseball caps might all implement “ICarryable”, which basically just says “these things can be carried in your inventory”, and what this looks like from the implementation side of things is that they all implement interface methods needed by the inventory system, e.g. getInventorySpaceRequired(), pickUp(), drop(), getWeight(), etc. The inventory system doesn’t care what an item’s base class is, it is written such that it can work with anything that implements the ICarryable methods.

5 Likes

A “fill point” is a physical point associated with an objective.

The fill points have required items that you must fill it with. It’s just called a fill point but it can be anything other that a fuel tank that require Jerry can items. It can be a car’s drive engine that requires 4 wheels. Or a storage unit that requires 5 sofas.

Every time the fill points gets progressed, it fires a call back function that is speacilized to do different things depending on which fill point it is. The name of this function is the same. The fill point itself is a table with all this information.

Fill points do have to be connected to the objective object so it could access it’s properties and methods.

It just gets kinda blurry whether I should make a fill point a different object or just part of the objective object.

The more I think about this the more I think this question resembles the halting problem.

1 Like

Okay. In that case you might not need to make things like the JerryCan into an object. A fill point is basically a counter, but it does have state (required, count, type of item being counted), and it does have actions (increment count, compare count to required, fire the callback action), so this would be a good candidate for a class. Maybe it would look something like this:

local FillPoint = {}
FillPoint.__index = FillPoint

function FillPoint.new(name, fptype, required, action)
	return setmetatable({
		Name = name,
		Type = fptype, 
		Required = required, 
		Count = 0,
		Action = action
		}, FillPoint)
end

function FillPoint:IsDone()
	return self.Count >= self.Required
end

function FillPoint:IncrementRequired()
	self.Count = self.Count + 1
	if self:IsDone() then
		self:FireAction()
	end
end

function FillPoint:FireAction()
	return self.Action()
end

return FillPoint

Maybe you wouldn’t want the FireAction to happen as a side effect of IncrementRequired, but I’m just thinking out loud here. Anyway, the idea is that a FillPoint could be a counter that triggers some action when a threshold is met.

An Objective could also be a class that, among other things, holds a list or dictionary of these FillPoint objects. Maybe it looks something like this:

local Objective = {}
Objective.__index = Objective

function Objective.new(name)
	return setmetatable({
		Name = name,
		FillPoints = {} -- collection of FillPoints
		}, Objective)
end

function Objective:AddFillPoint(name, fptype, required, action)
	local ok, fp = pcall(FillPoint.new, name, fptype, required, action)
	if ok then
		self.FillPoints[name] = fp
	else
		-- error stuff
		print("ERROR", fp)
	end
end

function Objective:AddItemToFillPoint(fp)
	if self.FillPoints[fp] == nil then
		-- error
		return
	end
	self.FillPoints[fp]:IncRequired()
end

function Objective:IsFillPointFull(fp)
	return self.FillPoints[fp]:IsDone()
end

function Objective:IsComplete()
	for _, fp in pairs(self.FillPoints) do
		--print(fp.Name)
		if not fp:IsDone()then
			return false
		end
	end
	return true
end

Again, this isn’t fully fleshed out and may not meet your needs insofar as the direction I’ve gone with it, but I hope it communicates the basic idea.

So, in the client code (where you are using the Objective class) you would create Objective objects and call methods on those Objectives that add FillPoints, increment FillPoint counters, check if FillPoints/Objectives are complete, etc. That could all happen through the Objective interface as it looks like I’ve done here, but this is just the first approach that came to mind.

To your question: yes, in my opinion it would be a good idea to make the FillPoint a different object AND have a collection of these be a part (attribute) of the Objective object. If there is only ever one FillPoint for an Objective, then i’d argue that these can be combined into the same thing.

Clearly, this is not the only way to go about this; it’s not even the only OOP way to go about it. Just talking in terms of where to separate things into different objects, the FillPoint is a good candidate here because it has a state and actions that can be cleanly separated from what the Objective is trying to do. There is another best practice that I don’t see on the page I linked that’s called the single responsibility principle. In short, a class should be responsible for one thing. Since we can talk about Objectives and FillPoints as individual things with different roles and responsibilities, having them merged within the same class implementation would go against this principle.

2 Likes

Alright that’s how I’m doing it right now

The problem is, a fill point needs the objective that it’s connected to. How would I go about making that work? Should I redesign my structure so this doesn’t happen?

This is what my fill point class looks like:

function FillPoint.new(objective, cf, requiredItems, callBack)
	
	local self = {}
	setmetatable(self, FillPoint)
	
	self.linkedObjective = objective --so it can easy pull information out of the objective
	self.requiredItems = requiredItems

end

Don’t know if there’s anything wrong with it.

A textbutton needs a ScreenGui (or the like) in order to work. If FillPoint objects are only ever used within an Objective, then I don’t know if requiring that connection is a problem. If you want these fill points to work as standalone objects, then just take care that all the real work is being done inside the FillPoint methods and let the Objective methods that deal with FillPoints pass arguments to the FillPoints.

You might consider using event-driven approach that lets the FillPoint objects manage their own incoming events locally. If the FillPoint is given an “owner” attribute, it can notify the Objective class about important changes. If there’s no owner, let the object skip that step and operate in standalone mode. With that kind of setup, all the action is handled at the bottom and the Objective is primarily used for handling notifications it gets from it’s FillPoints, sending queries to FillPoints it owns, or sending reset messages or other things to FillPoints using the interface of the FillPoint class.

Edit: if it’s doing what you need it to do, then there’s probably nothing wrong with it. Hack something together that does what you need it to do and refactor once you have a path forward. It isn’t going to be solid gold on the first pass unless you have a reference to work from where someone else already worked through all the pitfalls.

2 Likes