How far do you go with making objects?

A concrete example that I need help with is that I have a Leveler class which lets you make levelers (here is the documentation)

--[[
	Leveler
	
	levels start at 1
	
	Abstract class for creating a leveler
	see LevelCalculator for sample usage
	
	Constant = initial xp to go to level 0 (but it auto awards constant*base xp so you are able to get to level 1 for 0 actual xp)
	Base = base on exponent for next levels
	
	Not very optimized
	
	E.g:
	
	local l = Leveler.New(1000,1.3)
	print(l:Xp(1)) -- 0
	print(l:Xp(2)) -- 1000*1.3*1.3 - 1000*1.3 = 390 (but rounds to 391)
	
--]]

And here is the LevelCalculator Example:

--[[
	Level Calculator
	
	
--]]

-- Constants
local CONSTANT = 2000
local BASE = 1.3

-- efficiency variables
local RB=require(game.RB)
-- local variables

local LevelCalculator = RB.Leveler.New(CONSTANT,BASE)

return LevelCalculator

If you notice Leveler returns an object of a Leveler but that does not let you make objects that have unique Xp (etc) values

Do you think that’s good or bad?

(So to get level of player it would be something like print(LevelCalculator:Level(data.Xp)))

I assume replication would play a role in the decision to create individual objects for the players etc which have functionality such as:

print(PlayerLeveler.Xp)
PlayerLeveler:Add(5)
print(PlayerLeveler.Level)

Why do you need an object? Why can’t you just return a table of functions? This isn’t what OOP was designed for.

That’s what I currently do

So my question is exactly what your question is

There shouldn’t be RB.Leveler.New in the first place is my point.

I would just avoid making a class for this type of thing altogether. Instead, you could just store the values in an array. Better yet, you could write some code that samples a given numeric function over the integers, which would serve almost the same purpose but be far more useful, e.g. when dealing with different types of leveling curves.

My practice is that I design classes to have a broad range of applications and/or be extremely reusable. Object oriented programming makes things complicated, so why bother if you’re just using it for specialized, ad hoc solutions? Oftentimes creating a class can just be a waste of your time.

3 Likes

@Kampfkarren
I must’ve been unclear in my OP

The question isn’t about retaining the Leveler itself - it exists because it is reused
If you want a concrete example, each machine type in my game has its own Leveler stat.Leveler = newLeveler(stat.LevelConstant,stat.LevelBase) that depends on the machine’s LevelConstant and LevelBase which vary per machine

So there is a clear need for some sort of Leveler class
As for @suremark’s idea to do a “curve fitting” function, I think that would be useful but I don’t think its necessary for this although I will do it in the future if the math isn’t insane so thanks for the idea

btw I like that you used ad-hoc lol

Ok anyways so I’ll continue with the Machine example instead of player example:
So each Machine type has its own Leveler, but Machine types are in their own sense “classes” (because multiple machines of that type can exist and have separate “states”), more specifically with the following variables and functions:

--[=[
	return {
		Id = 1,
		Image = "rbxassetid://000000",
		Description = [[
			
		]],
		Type = "Normal",
		MachineType = "Producer",
		
		LevelConstant = 1000,
		LevelBase = 1.3,
		
		CostConstant = 1000,
		CostBase = 1.1,
		
		HealthConstant = 100,
		HealthBase = 1.1,
		
		ShopProbability = 100,
		ShopAmount = 10,
		
		ProductionTime = 3*60,
		ProductionConstant = 10,
		ProductionBase = 1.1,
		
		Add = function(machine) end,
	}
	
	Level constant and base - see leveler	
	others:
		Constants: initial
		Base: base^level
	
	Add - function called when a new model of this machine is added. machine is the Machine object
	
	ShopProbability - prob of this appearing in shop = ShopProbability/(sum of all machine shop probabilities)
	ShopAmount - amount of this item that can be sold at once in shop
	
	Additions:
	Models - array of models for corresponding levels
	MaxLevel - the max upgrade level of this machine (last machine model)
	GetModel - gets the appropriate model for the machine level
	Leveler - returns leveler
	GetCost - method returns cost based on level
	GetHealth - method returns health based on level
	GetHeal - method returns cost to heal health points
	GetProduction - method returns production based on level
	
--]=]

Here is an actual machine object:

machine = Stream.New(tostring(tile.Global),nil,{
		Name = stat.Name,
		Id = id,
		Owner = owner,
		CreationTime = getTime(), -- used for production
		
		Level = level,
		Xp = xp,
		XpLeft = xpLeft,
		
		Model = build,
		
		--Tile = tile, -- not using it atm and it messes up remotes bc cyclic table references
		Global = tile.Global, -- global position
		Health = health,
				
		MachineDataIndex = ind, -- index in machineData if its on an ffa tile to reference from client and when killing, this will be nil if its not in ffa region
		
		Humanoid = humanoid,
		
		-- functions wont be streamed to client so its fine
		GrantXp = grantXp,
		Kill = function(playAnimation)
			humanoid:Kill()
			grid[tostring(machine.Global)].Machine = nil
			if playAnimation then
				removeBuild(machine)
			else
				build:Destroy()
			end
			Stream.Kill(machine)
			
			if ind then -- is ffa machine iff started as ffa machine
				machineFFAIndex[machine.MachineDataIndex] = nil
			end
			
			machine = nil
		end,
	},{"Humanoid",})

As you can see it has the variables:

Level = level,
Xp = xp,
XpLeft = xpLeft,

It doesn’t need Level and XpLeft, I was just bored so I micro-optimized lol

But to get the individual machine’s leveler stats I have to call functions on the machine type’s stat as shown here:

RB.Network("Upgrade"):Bind(function(player,global)
	local machine = grid[global]
	machine = machine and machine.Machine
	local stat = Machines[machine.Id]
	
	if not (machine.Owner == player and machine.Level <  stat.MaxLevel and machine.XpLeft == 1) then
		return
	end
	
	local nl = machine.Level+1
	local nx = machine.Xp+1
	
	local data = Data:Get(player)
	local d = data.Money-stat:GetCost(nl)
	if d>=0 then
		-- update money and machine xp
		data("Money",d)
		machine("Xp",nx)
		machine("XpLeft",stat.Leveler:Left(nx))
		machine("Level",nl)
		
		-- rest is done within the health changed
	end	
end)

(Where it says stat.Leveler:Left(nx))

So my question is do I want to have stuff like that or should it be replaced by things such as machine.LevelerInstance.Left -- __index call // probably need better name than LevelerInstance but you get the point


Are you trying to achieve something like a Base Player Class where you can call

PlayerData:AddExp(50)

and then it does the rest?

Code

– Script –
local MS = require(script.ModuleScript)

local PlayersData = MS.PlayersData -- Store ALL Players Data inside a Module so you can Require it again with any Script

local PlayersData_MetaTable = MS.PlayersData_MetaTable

local Players = game.Players

-- Functions

local function PlayerAdded_Func(plr)
	PlayersData[plr] = {
		SaveData = { -- SaveData For Storing Data that will be Saved
			Money = 500,
			Level = 1,
			EXP = {0,50} -- {Current EXP, Needed EXP}
		}
	}
	setmetatable(PlayersData[plr],PlayersData_MetaTable)
	PlayersData[plr]:AddEXP(5)
end

-- Scripts

Players.PlayerAdded:Connect(PlayerAdded_Func)

– Module Script –

local M = {
		PlayersData = {}
}

M.PlayersData_MetaTable = {}
M.PlayersData_MetaTable.__index = M.PlayersData_MetaTable

	function M.PlayersData_MetaTable:AddEXP(Amount)
		local CurrentEXP = self.SaveData.EXP[1]
		local NeededEXP = self.SaveData.EXP[2]
		local Level = self.SaveData.Level
		print(self.SaveData.EXP[1])
		if CurrentEXP + Amount >= NeededEXP then
			self.SaveData.Level = Level + 1
			self.SaveData.EXP[1] = CurrentEXP + tonumber(Amount)
			self.SaveData.EXP[1] = self.SaveData.EXP[1] - NeededEXP
			print(self.SaveData.Level)
		else
			self.SaveData.EXP[1] = CurrentEXP + tonumber(Amount)
		end
		print(self.SaveData.EXP[1])
	end

return M

I finished Typing @Acreol

You’ll need to add more Math and Logic to the AddEXP Function, but you already knew that.

You can have the place file here MetaTables.rbxl (13.4 KB)

1 Like

Sure something like that but specifically for Level/Xp handling

waiting for typing…

I still see no reason for the leveler to be a class. It looks like Leveler is a class with two functions, one being a constructor: the epitome of misused OOP.

1 Like

Ok so I should have 100+ of this instantiated in the machine type initializer loop and inline it as well?

--[[
	Leveler
	
	levels start at 1
	
	Abstract class for creating a leveler
	see LevelCalculator for sample usage
	
	Constant = initial xp to go to level 0 (but it auto awards constant*base xp so you are able to get to level 1 for 0 actual xp)
	Base = base on exponent for next levels
	
	Not very optimized
	
	E.g:
	
	local l = Leveler.New(1000,1.3)
	print(l:Xp(1)) -- 0
	print(l:Xp(2)) -- 1000*1.3*1.3 - 1000*1.3 = 390 (but rounds to 391)
	
--]]

-- Constants

-- efficiency variables
local log = math.log10
local floor = math.floor
local ceil = math.ceil

-- local variables

local LevelCalculator = {}

local function level(l,xp) -- current level
	return floor(log((l.Offset+xp)/l.Constant)/l.Log)
end

local function xp(l,level) -- total xp
	return ceil(l.Constant*l.Base^level)-l.Offset
end

local function complete(l,x) -- amount complete in current level
	return x-x(l,level(l,x))
end

local function left(l,x) -- amount left until next level
	return xp(l,level(l,x)+1)-x
end

local function summary(l,x)
	local level = level(l,x)
	local nextLvl = xp(l,level+1)
	return level,nextLvl-x,x-xp(l,level) -- level,left,complete
end

local Leveler = {
	Level = level,
	Xp = xp,
	Complete = complete,
	Left = left,
	Summary = summary,
}

function Leveler.New(constant,base)
	return {
		Base = base,
		Constant = constant,
		Offset = base*constant,
		Log = log(base),
		
		Level = level,
		Xp = xp,
		Complete = complete,
		Left = left,
		Summary = summary,
	}
end

return Leveler

I know I make things sound complicated, but it’s really not:

function sample(f,m,n)
    inc = inc or 1
    local mArray = {}
    for i=m,n do
        mArray[#mArray+1]=f(i)
    end
    return mArray
end
1 Like

I misunderstood it as the inverse where given a sample of points you are doing a curve of best fit lol

1 Like

There doesn’t need to be an initializer in the first place. If that’s all in one module, then it’s only evaluated the first time you require it.

Edit: I’m seeing more what you mean. Before the code preview I was under the impression you were doing normal __index OOP for something that was completely unnecessary. Your code for that is fine.

1 Like

fine as in better than making a levelerinstance object?
>>why

Fine as in it’s the best abstraction you can do without it being obsessive. All the functions for it are organized into one location.

1 Like

So you think its a good idea to have instances of not just the leveler, but an entire player state (with methods)?

I assume this would generalize to machines and other objects as augmenting the machine with another metatable with __index inheritance?

I really like this idea actually

But in scenarios where its just the leveler (I can’t think of one specifically right now, do you think its worth it to have an individual leveler instance or just store the xp in a variable like how I do now?

Yes, Imo because every player has the Same stats and by using setmetable() I don’t have to repeat a lot of code, that’s what I really like about Metables.

Coding is all about Automation, you should automate as much as you can with the least effort but still Efficient.

I think it’s nice to write something inside a module and then forget about it so that you can move on to do the next thing without having to scroll pass it in your main script.


I’m not entirely sure I haven’t read all of the code you provided.

1 Like

Some of the things here kind of depend on how you want to structure your game, and how much memory you want to use (creating lots of objects will use a little extra). The latter isn’t very important, but you want to be consistent with the first. It’s really up to you in the end.

Personanlly, I don’t like creating objects for little reason. If I create an object, it must have two or more uses (methods, for example). Otherwise I might as well just make it a property/local variable of its parent. I know a good leveller will do more than two things, so it might be something for me to try out in the future, but for now I know my preference is with just a normal property and some related functions.

The levelling code I normally use just has a generalised stat updating function that gets called when a relevant stat is changed. Then, it just does all the ‘do u level up or no’ calculations within that function (possibly using external functions for things such as getting maximum experience based on current level). The level and other stats are just stored in a good ol’ table: {Level = 1, Experience = 0}.

Thinking objectively (pun unintended) making objects for things like handling a player’s level is a good idea, at least in terms of structure and consistency with the rest of your object oriented game.

2 Likes